Merge "Add action for editing instance metadata"

This commit is contained in:
Jenkins 2016-01-08 04:40:19 +00:00 committed by Gerrit Code Review
commit d3da2795d3
11 changed files with 210 additions and 7 deletions

View File

@ -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))

View File

@ -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.

View File

@ -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,

View File

@ -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);
}

View File

@ -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);
});
});
})();

View File

@ -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">

View File

@ -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.'));
});
}
}
}());

View File

@ -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']
]
}
];

View File

@ -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
#

View File

@ -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 = []

View File

@ -0,0 +1,2 @@
features:
- Instance metadata can be updated (https://blueprints.launchpad.net/horizon/+spec/edit-server-metadata)