diff options
| -rw-r--r-- | nova/api/openstack/server_metadata.py | 29 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 12 | ||||
| -rw-r--r-- | nova/compute/api.py | 37 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 5 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_server_metadata.py | 59 | ||||
| -rw-r--r-- | nova/tests/integrated/test_servers.py | 88 |
6 files changed, 151 insertions, 79 deletions
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py index 5c1390b9c..01ca79eec 100644 --- a/nova/api/openstack/server_metadata.py +++ b/nova/api/openstack/server_metadata.py @@ -18,6 +18,7 @@ from webob import exc from nova import compute +from nova import quota from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults @@ -44,10 +45,14 @@ class Controller(common.OpenstackController): def create(self, req, server_id): context = req.environ['nova.context'] - body = self._deserialize(req.body, req.get_content_type()) - self.compute_api.update_or_create_instance_metadata(context, - server_id, - body['metadata']) + data = self._deserialize(req.body, req.get_content_type()) + metadata = data.get('metadata') + try: + self.compute_api.update_or_create_instance_metadata(context, + server_id, + metadata) + except quota.QuotaError as error: + self._handle_quota_error(error) return req.body def update(self, req, server_id, id): @@ -59,9 +64,13 @@ class Controller(common.OpenstackController): if len(body) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) - self.compute_api.update_or_create_instance_metadata(context, - server_id, - body) + try: + self.compute_api.update_or_create_instance_metadata(context, + server_id, + body) + except quota.QuotaError as error: + self._handle_quota_error(error) + return req.body def show(self, req, server_id, id): @@ -77,3 +86,9 @@ class Controller(common.OpenstackController): """ Deletes an existing metadata """ context = req.environ['nova.context'] self.compute_api.delete_instance_metadata(context, server_id, id) + + def _handle_quota_error(self, error): + """Reraise quota errors as api-specific http exceptions""" + if error.code == "MetadataLimitExceeded": + raise exc.HTTPBadRequest(explanation=error.message) + raise error diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 61e08211d..8451052a8 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -132,16 +132,6 @@ class Controller(common.OpenstackController): kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_id) - # Metadata is a list, not a Dictionary, because we allow duplicate keys - # (even though JSON can't encode this) - # In future, we may not allow duplicate keys. - # However, the CloudServers API is not definitive on this front, - # and we want to be compatible. - metadata = [] - if env['server'].get('metadata'): - for k, v in env['server']['metadata'].items(): - metadata.append({'key': k, 'value': v}) - personality = env['server'].get('personality') injected_files = [] if personality: @@ -170,7 +160,7 @@ class Controller(common.OpenstackController): display_description=name, key_name=key_name, key_data=key_data, - metadata=metadata, + metadata=env['server'].get('metadata', {}), injected_files=injected_files) except quota.QuotaError as error: self._handle_quota_error(error) diff --git a/nova/compute/api.py b/nova/compute/api.py index e5065c3a5..effe68d6d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -105,16 +105,40 @@ class API(base.Base): if len(content) > content_limit: raise quota.QuotaError(code="OnsetFileContentLimitExceeded") + def _check_metadata_properties_quota(self, context, metadata={}): + """Enforce quota limits on metadata properties""" + num_metadata = len(metadata) + quota_metadata = quota.allowed_metadata_items(context, num_metadata) + if quota_metadata < num_metadata: + pid = context.project_id + msg = _("Quota exceeeded for %(pid)s, tried to set " + "%(num_metadata)s metadata properties") % locals() + LOG.warn(msg) + raise quota.QuotaError(msg, "MetadataLimitExceeded") + + # Because metadata is stored in the DB, we hard-code the size limits + # In future, we may support more variable length strings, so we act + # as if this is quota-controlled for forwards compatibility + for k, v in metadata.iteritems(): + if len(k) > 255 or len(v) > 255: + pid = context.project_id + msg = _("Quota exceeeded for %(pid)s, metadata property " + "key or value too long") % locals() + LOG.warn(msg) + raise quota.QuotaError(msg, "MetadataLimitExceeded") + def create(self, context, instance_type, image_id, kernel_id=None, ramdisk_id=None, min_count=1, max_count=1, display_name='', display_description='', key_name=None, key_data=None, security_group='default', - availability_zone=None, user_data=None, metadata=[], + availability_zone=None, user_data=None, metadata={}, injected_files=None): - """Create the number of instances requested if quota and - other arguments check out ok.""" + """Create number of instances requested, given quotas. + Create the number of instances requested if quota and + other arguments check out ok. + """ if not instance_type: instance_type = instance_types.get_default_instance_type() @@ -128,9 +152,7 @@ class API(base.Base): "run %s more instances of this type.") % num_instances, "InstanceLimitExceeded") - self._check_metadata_quota(context, metadata) - self._check_metadata_item_length(context, metadata) - + self._check_metadata_properties_quota(context, metadata) self._check_injected_file_quota(context, injected_files) image = self.image_service.show(context, image_id) @@ -750,5 +772,8 @@ class API(base.Base): def update_or_create_instance_metadata(self, context, instance_id, metadata): """Updates or creates instance metadata""" + combined_metadata = self.get_instance_metadata(context, instance_id) + combined_metadata.update(metadata) + self._check_metadata_properties_quota(context, combined_metadata) self.db.instance_metadata_update_or_create(context, instance_id, metadata) diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e675022e9..570530792 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -770,9 +770,10 @@ def instance_create(context, values): metadata = values.get('metadata') metadata_refs = [] if metadata: - for metadata_item in metadata: + for k, v in metadata.iteritems(): metadata_ref = models.InstanceMetadata() - metadata_ref.update(metadata_item) + metadata_ref['key'] = k + metadata_ref['value'] = v metadata_refs.append(metadata_ref) values['metadata'] = metadata_refs diff --git a/nova/tests/api/openstack/test_server_metadata.py b/nova/tests/api/openstack/test_server_metadata.py index c8d456472..862e14b7c 100644 --- a/nova/tests/api/openstack/test_server_metadata.py +++ b/nova/tests/api/openstack/test_server_metadata.py @@ -21,11 +21,19 @@ import unittest import webob +from nova import flags from nova.api import openstack from nova.tests.api.openstack import fakes import nova.wsgi +FLAGS = flags.FLAGS + + +def return_create_instance_metadata_max(context, server_id, metadata): + return stub_max_server_metadata() + + def return_create_instance_metadata(context, server_id, metadata): return stub_server_metadata() @@ -53,6 +61,13 @@ def stub_server_metadata(): return metadata +def stub_max_server_metadata(): + metadata = {"metadata": {}} + for num in range(FLAGS.quota_metadata_items): + metadata['metadata']['key%i' % num] = "blah" + return metadata + + class ServerMetaDataTest(unittest.TestCase): def setUp(self): @@ -69,7 +84,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_index(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', - return_server_metadata) + return_server_metadata) req = webob.Request.blank('/v1.1/servers/1/meta') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) @@ -79,7 +94,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_index_no_data(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', - return_empty_server_metadata) + return_empty_server_metadata) req = webob.Request.blank('/v1.1/servers/1/meta') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) @@ -89,7 +104,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_show(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', - return_server_metadata) + return_server_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/key5') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) @@ -99,7 +114,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_show_meta_not_found(self): self.stubs.Set(nova.db.api, 'instance_metadata_get', - return_empty_server_metadata) + return_empty_server_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/key6') req.environ['api.version'] = '1.1' res = req.get_response(fakes.wsgi_app()) @@ -108,7 +123,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_delete(self): self.stubs.Set(nova.db.api, 'instance_metadata_delete', - delete_server_metadata) + delete_server_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/key5') req.environ['api.version'] = '1.1' req.method = 'DELETE' @@ -117,7 +132,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_create(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', - return_create_instance_metadata) + return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/meta') req.environ['api.version'] = '1.1' req.method = 'POST' @@ -130,7 +145,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', - return_create_instance_metadata) + return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/key1') req.environ['api.version'] = '1.1' req.method = 'PUT' @@ -143,7 +158,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_too_many_keys(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', - return_create_instance_metadata) + return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/key1') req.environ['api.version'] = '1.1' req.method = 'PUT' @@ -154,7 +169,7 @@ class ServerMetaDataTest(unittest.TestCase): def test_update_item_body_uri_mismatch(self): self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', - return_create_instance_metadata) + return_create_instance_metadata) req = webob.Request.blank('/v1.1/servers/1/meta/bad') req.environ['api.version'] = '1.1' req.method = 'PUT' @@ -162,3 +177,29 @@ class ServerMetaDataTest(unittest.TestCase): req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) + + def test_too_many_metadata_items_on_create(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata) + data = {"metadata": {}} + for num in range(FLAGS.quota_metadata_items + 1): + data['metadata']['key%i' % num] = "blah" + json_string = str(data).replace("\'", "\"") + req = webob.Request.blank('/v1.1/servers/1/meta') + req.environ['api.version'] = '1.1' + req.method = 'POST' + req.body = json_string + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, res.status_int) + + def test_to_many_metadata_items_on_update_item(self): + self.stubs.Set(nova.db.api, 'instance_metadata_update_or_create', + return_create_instance_metadata_max) + req = webob.Request.blank('/v1.1/servers/1/meta/key1') + req.environ['api.version'] = '1.1' + req.method = 'PUT' + req.body = '{"a new key": "a new value"}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(400, res.status_int) diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 749ea8955..8175659a3 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -134,50 +134,50 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # Should be gone self.assertFalse(found_server) -# TODO(justinsb): Enable this unit test when the metadata bug is fixed -# def test_create_server_with_metadata(self): -# """Creates a server with metadata""" -# -# # Build the server data gradually, checking errors along the way -# server = self._build_minimal_create_server_request() -# -# for metadata_count in range(30): -# metadata = {} -# for i in range(metadata_count): -# metadata['key_%s' % i] = 'value_%s' % i -# server['metadata'] = metadata -# -# post = {'server': server} -# created_server = self.api.post_server(post) -# LOG.debug("created_server: %s" % created_server) -# self.assertTrue(created_server['id']) -# created_server_id = created_server['id'] -# # Reenable when bug fixed -# # self.assertEqual(metadata, created_server.get('metadata')) -# -# # Check it's there -# found_server = self.api.get_server(created_server_id) -# self.assertEqual(created_server_id, found_server['id']) -# self.assertEqual(metadata, found_server.get('metadata')) -# -# # The server should also be in the all-servers details list -# servers = self.api.get_servers(detail=True) -# server_map = dict((server['id'], server) for server in servers) -# found_server = server_map.get(created_server_id) -# self.assertTrue(found_server) -# # Details do include metadata -# self.assertEqual(metadata, found_server.get('metadata')) -# -# # The server should also be in the all-servers summary list -# servers = self.api.get_servers(detail=False) -# server_map = dict((server['id'], server) for server in servers) -# found_server = server_map.get(created_server_id) -# self.assertTrue(found_server) -# # Summary should not include metadata -# self.assertFalse(found_server.get('metadata')) -# -# # Cleanup -# self._delete_server(created_server_id) + def test_create_server_with_metadata(self): + """Creates a server with metadata""" + + # Build the server data gradually, checking errors along the way + server = self._build_minimal_create_server_request() + + metadata = {} + for i in range(30): + metadata['key_%s' % i] = 'value_%s' % i + + server['metadata'] = metadata + + post = {'server': server} + created_server = self.api.post_server(post) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Reenable when bug fixed + self.assertEqual(metadata, created_server.get('metadata')) + # Check it's there + + found_server = self.api.get_server(created_server_id) + self.assertEqual(created_server_id, found_server['id']) + self.assertEqual(metadata, found_server.get('metadata')) + + # The server should also be in the all-servers details list + servers = self.api.get_servers(detail=True) + server_map = dict((server['id'], server) for server in servers) + found_server = server_map.get(created_server_id) + self.assertTrue(found_server) + # Details do include metadata + self.assertEqual(metadata, found_server.get('metadata')) + + # The server should also be in the all-servers summary list + servers = self.api.get_servers(detail=False) + server_map = dict((server['id'], server) for server in servers) + found_server = server_map.get(created_server_id) + self.assertTrue(found_server) + # Summary should not include metadata + self.assertFalse(found_server.get('metadata')) + + # Cleanup + self._delete_server(created_server_id) if __name__ == "__main__": |
