Add action for editing instance metadata
This adds the Update Metadata action to the instances table to allow managing the metadata on an instance. This is very similar to the Update Metadata actions for images, flavors, etc. Implements: blueprint edit-server-metadata Change-Id: Ia09a05f5cd93898ec9d64ac7af1e6baf07e71757
This commit is contained in:
parent
1178757445
commit
79971c627b
@ -720,6 +720,14 @@ def server_unlock(request, instance_id):
|
||||
novaclient(request).servers.unlock(instance_id)
|
||||
|
||||
|
||||
def server_metadata_update(request, instance_id, metadata):
|
||||
novaclient(request).servers.set_meta(instance_id, metadata)
|
||||
|
||||
|
||||
def server_metadata_delete(request, instance_id, keys):
|
||||
novaclient(request).servers.delete_meta(instance_id, keys)
|
||||
|
||||
|
||||
def tenant_quota_get(request, tenant_id):
|
||||
return base.QuotaSet(novaclient(request).quotas.get(tenant_id))
|
||||
|
||||
|
@ -199,7 +199,7 @@ class Servers(generic.View):
|
||||
class Server(generic.View):
|
||||
"""API for retrieving a single server
|
||||
"""
|
||||
url_regex = r'nova/servers/(?P<server_id>.+|default)$'
|
||||
url_regex = r'nova/servers/(?P<server_id>[^/]+|default)$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, server_id):
|
||||
@ -210,6 +210,35 @@ class Server(generic.View):
|
||||
return api.nova.server_get(request, server_id).to_dict()
|
||||
|
||||
|
||||
@urls.register
|
||||
class ServerMetadata(generic.View):
|
||||
"""API for server metadata.
|
||||
"""
|
||||
url_regex = r'nova/servers/(?P<server_id>[^/]+|default)/metadata$'
|
||||
|
||||
@rest_utils.ajax()
|
||||
def get(self, request, server_id):
|
||||
"""Get a specific server's metadata
|
||||
|
||||
http://localhost/api/nova/servers/1/metadata
|
||||
"""
|
||||
return api.nova.server_get(request,
|
||||
server_id).to_dict().get('metadata')
|
||||
|
||||
@rest_utils.ajax()
|
||||
def patch(self, request, server_id):
|
||||
"""Update metadata items for a server
|
||||
|
||||
http://localhost/api/nova/servers/1/metadata
|
||||
"""
|
||||
updated = request.DATA['updated']
|
||||
removed = request.DATA['removed']
|
||||
if updated:
|
||||
api.nova.server_metadata_update(request, server_id, updated)
|
||||
if removed:
|
||||
api.nova.server_metadata_delete(request, server_id, removed)
|
||||
|
||||
|
||||
@urls.register
|
||||
class Extensions(generic.View):
|
||||
"""API for nova extensions.
|
||||
|
@ -719,6 +719,29 @@ class SimpleDisassociateIP(policy.PolicyTargetMixin, tables.Action):
|
||||
return shortcuts.redirect(request.get_full_path())
|
||||
|
||||
|
||||
class UpdateMetadata(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
name = "update_metadata"
|
||||
verbose_name = _("Update Metadata")
|
||||
ajax = False
|
||||
icon = "pencil"
|
||||
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
|
||||
policy_rules = (("compute", "compute:update_instance_metadata"),)
|
||||
|
||||
def __init__(self, attrs=None, **kwargs):
|
||||
kwargs['preempt'] = True
|
||||
super(UpdateMetadata, self).__init__(attrs, **kwargs)
|
||||
|
||||
def get_link_url(self, datum):
|
||||
instance_id = self.table.get_object_id(datum)
|
||||
self.attrs['ng-click'] = (
|
||||
"modal.openMetadataModal('instance', '%s', true)" % instance_id)
|
||||
return "javascript:void(0);"
|
||||
|
||||
def allowed(self, request, instance=None):
|
||||
return (instance and
|
||||
instance.status.lower() != 'error')
|
||||
|
||||
|
||||
def instance_fault_to_friendly_message(instance):
|
||||
fault = getattr(instance, 'fault', {})
|
||||
message = fault.get('message', _("Unknown"))
|
||||
@ -1177,7 +1200,7 @@ class InstancesTable(tables.DataTable):
|
||||
row_actions = (StartInstance, ConfirmResize, RevertResize,
|
||||
CreateSnapshot, SimpleAssociateIP, AssociateIP,
|
||||
SimpleDisassociateIP, AttachInterface,
|
||||
DetachInterface, EditInstance,
|
||||
DetachInterface, EditInstance, UpdateMetadata,
|
||||
DecryptInstancePassword, EditInstanceSecurityGroups,
|
||||
ConsoleLink, LogLink, TogglePause, ToggleSuspend,
|
||||
ToggleShelve, ResizeLink, LockInstance, UnlockInstance,
|
||||
|
@ -51,7 +51,8 @@
|
||||
return {
|
||||
aggregate: nova.getAggregateExtraSpecs,
|
||||
flavor: nova.getFlavorExtraSpecs,
|
||||
image: glance.getImageProps
|
||||
image: glance.getImageProps,
|
||||
instance: nova.getInstanceMetadata
|
||||
}[resource](id);
|
||||
}
|
||||
|
||||
@ -67,7 +68,8 @@
|
||||
return {
|
||||
aggregate: nova.editAggregateExtraSpecs,
|
||||
flavor: nova.editFlavorExtraSpecs,
|
||||
image: glance.editImageProps
|
||||
image: glance.editImageProps,
|
||||
instance: nova.editInstanceMetadata
|
||||
}[resource](id, updated, removed);
|
||||
}
|
||||
|
||||
@ -81,7 +83,8 @@
|
||||
resource_type: {
|
||||
aggregate: 'OS::Nova::Aggregate',
|
||||
flavor: 'OS::Nova::Flavor',
|
||||
image: 'OS::Glance::Image'
|
||||
image: 'OS::Glance::Image',
|
||||
instance: 'OS::Nova::Instance'
|
||||
}[resource]
|
||||
}, false);
|
||||
}
|
||||
|
@ -23,7 +23,9 @@
|
||||
var nova = {getAggregateExtraSpecs: function() {},
|
||||
getFlavorExtraSpecs: function() {},
|
||||
editAggregateExtraSpecs: function() {},
|
||||
editFlavorExtraSpecs: function() {} };
|
||||
editFlavorExtraSpecs: function() {},
|
||||
getInstanceMetadata: function() {},
|
||||
editInstanceMetadata: function() {} };
|
||||
|
||||
var glance = {getImageProps: function() {},
|
||||
editImageProps: function() {},
|
||||
@ -102,6 +104,26 @@
|
||||
.toHaveBeenCalledWith({ resource_type: 'OS::Glance::Image' }, false);
|
||||
});
|
||||
|
||||
it('should get instance metadata', function() {
|
||||
var expected = 'instance metadata';
|
||||
spyOn(nova, 'getInstanceMetadata').and.returnValue(expected);
|
||||
var actual = metadataService.getMetadata('instance', '1');
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should edit instance metadata', function() {
|
||||
spyOn(nova, 'editInstanceMetadata');
|
||||
metadataService.editMetadata('instance', '1', 'updated', ['removed']);
|
||||
expect(nova.editInstanceMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']);
|
||||
});
|
||||
|
||||
it('should get instance namespace', function() {
|
||||
spyOn(glance, 'getNamespaces');
|
||||
metadataService.getNamespaces('instance');
|
||||
expect(glance.getNamespaces)
|
||||
.toHaveBeenCalledWith({ resource_type: 'OS::Nova::Instance' }, false);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -3,6 +3,7 @@
|
||||
<span translate ng-if="modal.resourceType==='aggregate'">Update Aggregate Metadata</span>
|
||||
<span translate ng-if="modal.resourceType==='flavor'">Update Flavor Metadata</span>
|
||||
<span translate ng-if="modal.resourceType==='image'">Update Image Metadata</span>
|
||||
<span translate ng-if="modal.resourceType==='instance'">Update Instance Metadata</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
@ -46,7 +46,9 @@
|
||||
editFlavorExtraSpecs: editFlavorExtraSpecs,
|
||||
getAggregateExtraSpecs: getAggregateExtraSpecs,
|
||||
editAggregateExtraSpecs: editAggregateExtraSpecs,
|
||||
getServices: getServices
|
||||
getServices: getServices,
|
||||
getInstanceMetadata: getInstanceMetadata,
|
||||
editInstanceMetadata: editInstanceMetadata
|
||||
};
|
||||
|
||||
return service;
|
||||
@ -368,6 +370,40 @@
|
||||
toastService.add('error', gettext('Unable to edit the aggregate extra specs.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.app.core.openstack-service-api.nova.getInstanceMetadata
|
||||
* @description
|
||||
* Get a single instance's metadata by ID.
|
||||
* @param {string} id
|
||||
* Specifies the id of the instance to request the metadata.
|
||||
*/
|
||||
function getInstanceMetadata(id) {
|
||||
return apiService.get('/api/nova/servers/' + id + '/metadata')
|
||||
.error(function () {
|
||||
toastService.add('error', gettext('Unable to retrieve instance metadata.'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @name horizon.openstack-service-api.nova.editInstanceMetadata
|
||||
* @description
|
||||
* Update a single instance's metadata by ID.
|
||||
* @param {string} id
|
||||
* @param {object} updated New metadata.
|
||||
* @param {[]} removed Names of removed metadata items.
|
||||
*/
|
||||
function editInstanceMetadata(id, updated, removed) {
|
||||
return apiService.patch(
|
||||
'/api/nova/servers/' + id + '/metadata',
|
||||
{
|
||||
updated: updated,
|
||||
removed: removed
|
||||
}
|
||||
).error(function () {
|
||||
toastService.add('error', gettext('Unable to edit instance metadata.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}());
|
||||
|
@ -247,6 +247,28 @@
|
||||
"testInput": [
|
||||
42, {a: '1', b: '2'}, ['c', 'd']
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "getInstanceMetadata",
|
||||
"method": "get",
|
||||
"path": "/api/nova/servers/42/metadata",
|
||||
"error": "Unable to retrieve instance metadata.",
|
||||
"testInput": [
|
||||
42
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "editInstanceMetadata",
|
||||
"method": "patch",
|
||||
"path": "/api/nova/servers/42/metadata",
|
||||
"data": {
|
||||
"updated": {a: '1', b: '2'},
|
||||
"removed": ['c', 'd']
|
||||
},
|
||||
"error": "Unable to edit instance metadata.",
|
||||
"testInput": [
|
||||
42, {a: '1', b: '2'}, ['c', 'd']
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -153,6 +153,35 @@ class NovaRestTestCase(test.TestCase):
|
||||
self.assertStatusCode(response, 200)
|
||||
nc.server_get.assert_called_once_with(request, "1")
|
||||
|
||||
#
|
||||
# Server Metadata
|
||||
#
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_server_get_metadata(self, nc):
|
||||
request = self.mock_rest_request()
|
||||
meta = {'foo': 'bar'}
|
||||
nc.server_get.return_value.to_dict.return_value.get.return_value = meta
|
||||
|
||||
response = nova.ServerMetadata().get(request, "1")
|
||||
self.assertStatusCode(response, 200)
|
||||
nc.server_get.assert_called_once_with(request, "1")
|
||||
|
||||
@mock.patch.object(nova.api, 'nova')
|
||||
def test_server_edit_metadata(self, nc):
|
||||
request = self.mock_rest_request(
|
||||
body='{"updated": {"a": "1", "b": "2"}, "removed": ["c", "d"]}'
|
||||
)
|
||||
|
||||
response = nova.ServerMetadata().patch(request, '1')
|
||||
self.assertStatusCode(response, 204)
|
||||
self.assertEqual(response.content, b'')
|
||||
nc.server_metadata_update.assert_called_once_with(
|
||||
request, '1', {'a': '1', 'b': '2'}
|
||||
)
|
||||
nc.server_metadata_delete.assert_called_once_with(
|
||||
request, '1', ['c', 'd']
|
||||
)
|
||||
|
||||
#
|
||||
# Extensions
|
||||
#
|
||||
|
@ -212,6 +212,34 @@ class ComputeApiTests(test.APITestCase):
|
||||
ret_val = api.nova.server_get(self.request, server.id)
|
||||
self.assertIsInstance(ret_val, api.nova.Server)
|
||||
|
||||
def test_server_metadata_update(self):
|
||||
server = self.servers.first()
|
||||
metadata = {'foo': 'bar'}
|
||||
|
||||
novaclient = self.stub_novaclient()
|
||||
novaclient.servers = self.mox.CreateMockAnything()
|
||||
novaclient.servers.set_meta(server.id, metadata)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
ret_val = api.nova.server_metadata_update(self.request,
|
||||
server.id,
|
||||
metadata)
|
||||
self.assertIsNone(ret_val)
|
||||
|
||||
def test_server_metadata_delete(self):
|
||||
server = self.servers.first()
|
||||
keys = ['a', 'b']
|
||||
|
||||
novaclient = self.stub_novaclient()
|
||||
novaclient.servers = self.mox.CreateMockAnything()
|
||||
novaclient.servers.delete_meta(server.id, keys)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
ret_val = api.nova.server_metadata_delete(self.request,
|
||||
server.id,
|
||||
keys)
|
||||
self.assertIsNone(ret_val)
|
||||
|
||||
def _test_absolute_limits(self, values, expected_results):
|
||||
limits = self.mox.CreateMockAnything()
|
||||
limits.absolute = []
|
||||
|
@ -0,0 +1,2 @@
|
||||
features:
|
||||
- Instance metadata can be updated (https://blueprints.launchpad.net/horizon/+spec/edit-server-metadata)
|
Loading…
Reference in New Issue
Block a user