Merge "Add action for editing instance metadata"
This commit is contained in:
commit
d3da2795d3
@ -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