From 15c0847341b2a182019c865ec787c129fb21dae6 Mon Sep 17 00:00:00 2001 From: Vishvananda Ishaya Date: Wed, 11 Apr 2012 20:12:38 -0700 Subject: [PATCH] Port types and extra specs to volume api * Fixes bug 979089 * Adds pollicy for new extensions * Fixes __init__ in testing directories * Cleans up existing volume types tests * Adds tests for type management * Adds tests for extra specs management * Removed unused Quota handling * Fixed typo in db volume_type_get Change-Id: Ic80190ecf1d6d6ad0229e5af642a50c7c53bbbf9 --- etc/nova/policy.json | 4 + .../openstack/compute/contrib/volumetypes.py | 27 +-- .../volume/contrib/types_extra_specs.py | 152 +++++++++++++ .../openstack/volume/contrib/types_manage.py | 91 ++++++++ nova/db/sqlalchemy/api.py | 2 +- nova/tests/api/openstack/__init__.py | 19 ++ nova/tests/api/openstack/compute/__init__.py | 5 +- .../api/openstack/compute/contrib/__init__.py | 6 +- nova/tests/api/openstack/volume/__init__.py | 5 +- .../api/openstack/volume/contrib/__init__.py | 19 ++ .../volume/contrib/test_types_extra_specs.py | 202 ++++++++++++++++++ .../volume/contrib/test_types_manage.py | 103 +++++++++ .../openstack/volume/extensions/__init__.py | 15 ++ .../openstack/volume/extensions/foxinsocks.py | 94 ++++++++ .../api/openstack/volume/test_extensions.py | 156 ++++++++++++++ nova/tests/api/openstack/volume/test_types.py | 26 +-- nova/tests/policy.json | 7 +- 17 files changed, 883 insertions(+), 50 deletions(-) create mode 100644 nova/api/openstack/volume/contrib/types_extra_specs.py create mode 100644 nova/api/openstack/volume/contrib/types_manage.py create mode 100644 nova/tests/api/openstack/volume/contrib/__init__.py create mode 100644 nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py create mode 100644 nova/tests/api/openstack/volume/contrib/test_types_manage.py create mode 100644 nova/tests/api/openstack/volume/extensions/__init__.py create mode 100644 nova/tests/api/openstack/volume/extensions/foxinsocks.py create mode 100644 nova/tests/api/openstack/volume/test_extensions.py diff --git a/etc/nova/policy.json b/etc/nova/policy.json index e52518c5c9b9..481620a096e5 100644 --- a/etc/nova/policy.json +++ b/etc/nova/policy.json @@ -65,6 +65,10 @@ "volume:get_all_snapshots": [], + "volume_extension:types_manage": [["rule:admin_api"]], + "volume_extension:types_extra_specs": [["rule:admin_api"]], + + "network:get_all_networks": [], "network:get_network": [], "network:delete_network": [], diff --git a/nova/api/openstack/compute/contrib/volumetypes.py b/nova/api/openstack/compute/contrib/volumetypes.py index 16a423f800cc..4255ac149b6a 100644 --- a/nova/api/openstack/compute/contrib/volumetypes.py +++ b/nova/api/openstack/compute/contrib/volumetypes.py @@ -85,8 +85,6 @@ class VolumeTypesController(object): try: volume_types.create(context, name, specs) vol_type = volume_types.get_volume_type_by_name(context, name) - except exception.QuotaError as error: - self._handle_quota_error(error) except exception.NotFound: raise exc.HTTPNotFound() @@ -171,12 +169,9 @@ class VolumeTypeExtraSpecsController(object): authorize(context) self._check_body(body) specs = body.get('extra_specs') - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - specs) - except exception.QuotaError as error: - self._handle_quota_error(error) + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + specs) return body @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) @@ -190,13 +185,9 @@ class VolumeTypeExtraSpecsController(object): if len(body) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) - try: - db.volume_type_extra_specs_update_or_create(context, - vol_type_id, - body) - except exception.QuotaError as error: - self._handle_quota_error(error) - + db.volume_type_extra_specs_update_or_create(context, + vol_type_id, + body) return body @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) @@ -216,12 +207,6 @@ class VolumeTypeExtraSpecsController(object): authorize(context) db.volume_type_extra_specs_delete(context, vol_type_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 - class Volumetypes(extensions.ExtensionDescriptor): """Volume types support""" diff --git a/nova/api/openstack/volume/contrib/types_extra_specs.py b/nova/api/openstack/volume/contrib/types_extra_specs.py new file mode 100644 index 000000000000..d70e2453894d --- /dev/null +++ b/nova/api/openstack/volume/contrib/types_extra_specs.py @@ -0,0 +1,152 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The volume types extra specs extension""" + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova import db +from nova import exception +from nova.volume import volume_types + + +authorize = extensions.extension_authorizer('volume', 'types_extra_specs') + + +class VolumeTypeExtraSpecsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.make_flat_dict('extra_specs', selector='extra_specs') + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecTemplate(xmlutil.TemplateBuilder): + def construct(self): + tagname = xmlutil.Selector('key') + + def extraspec_sel(obj, do_raise=False): + # Have to extract the key and value for later use... + key, value = obj.items()[0] + return dict(key=key, value=value) + + root = xmlutil.TemplateElement(tagname, selector=extraspec_sel) + root.text = 'value' + return xmlutil.MasterTemplate(root, 1) + + +class VolumeTypeExtraSpecsController(object): + """ The volume type extra specs API controller for the OpenStack API """ + + def _get_extra_specs(self, context, type_id): + extra_specs = db.volume_type_extra_specs_get(context, type_id) + specs_dict = {} + for key, value in extra_specs.iteritems(): + specs_dict[key] = value + return dict(extra_specs=specs_dict) + + def _check_body(self, body): + if not body: + expl = _('No Request Body') + raise webob.exc.HTTPBadRequest(explanation=expl) + + def _check_type(self, context, type_id): + try: + volume_types.get_volume_type(context, type_id) + except exception.NotFound as ex: + raise webob.exc.HTTPNotFound(explanation=unicode(ex)) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def index(self, req, type_id): + """ Returns the list of extra specs for a given volume type """ + context = req.environ['nova.context'] + authorize(context) + self._check_type(context, type_id) + return self._get_extra_specs(context, type_id) + + @wsgi.serializers(xml=VolumeTypeExtraSpecsTemplate) + def create(self, req, type_id, body=None): + context = req.environ['nova.context'] + authorize(context) + self._check_type(context, type_id) + self._check_body(body) + specs = body.get('extra_specs') + if not isinstance(specs, dict): + expl = _('Malformed extra specs') + raise webob.exc.HTTPBadRequest(explanation=expl) + db.volume_type_extra_specs_update_or_create(context, + type_id, + specs) + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def update(self, req, type_id, id, body=None): + context = req.environ['nova.context'] + authorize(context) + self._check_type(context, type_id) + self._check_body(body) + if not id in body: + expl = _('Request body and URI mismatch') + raise webob.exc.HTTPBadRequest(explanation=expl) + if len(body) > 1: + expl = _('Request body contains too many items') + raise webob.exc.HTTPBadRequest(explanation=expl) + db.volume_type_extra_specs_update_or_create(context, + type_id, + body) + return body + + @wsgi.serializers(xml=VolumeTypeExtraSpecTemplate) + def show(self, req, type_id, id): + """Return a single extra spec item.""" + context = req.environ['nova.context'] + authorize(context) + self._check_type(context, type_id) + specs = self._get_extra_specs(context, type_id) + if id in specs['extra_specs']: + return {id: specs['extra_specs'][id]} + else: + raise webob.exc.HTTPNotFound() + + def delete(self, req, type_id, id): + """ Deletes an existing extra spec """ + context = req.environ['nova.context'] + self._check_type(context, type_id) + authorize(context) + db.volume_type_extra_specs_delete(context, type_id, id) + return webob.Response(status_int=202) + + +class Types_extra_specs(extensions.ExtensionDescriptor): + """Types extra specs support""" + + name = "TypesExtraSpecs" + alias = "os-types-extra-specs" + namespace = "http://docs.openstack.org/volume/ext/types-extra-specs/api/v1" + updated = "2011-08-24T00:00:00+00:00" + + def get_resources(self): + resources = [] + res = extensions.ResourceExtension('extra_specs', + VolumeTypeExtraSpecsController(), + parent=dict( + member_name='type', + collection_name='types')) + resources.append(res) + + return resources diff --git a/nova/api/openstack/volume/contrib/types_manage.py b/nova/api/openstack/volume/contrib/types_manage.py new file mode 100644 index 000000000000..e84f787ab8ab --- /dev/null +++ b/nova/api/openstack/volume/contrib/types_manage.py @@ -0,0 +1,91 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The volume types manage extension.""" + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack.volume import types +from nova.api.openstack import wsgi +from nova import exception +from nova.volume import volume_types + + +authorize = extensions.extension_authorizer('volume', 'types_manage') + + +class VolumeTypesManageController(wsgi.Controller): + """ The volume types API controller for the OpenStack API """ + + @wsgi.action("create") + @wsgi.serializers(xml=types.VolumeTypeTemplate) + def _create(self, req, body): + """Creates a new volume type.""" + context = req.environ['nova.context'] + authorize(context) + + if not body or body == "": + raise webob.exc.HTTPUnprocessableEntity() + + vol_type = body.get('volume_type', None) + if vol_type is None or vol_type == "": + raise webob.exc.HTTPUnprocessableEntity() + + name = vol_type.get('name', None) + specs = vol_type.get('extra_specs', {}) + + if name is None or name == "": + raise webob.exc.HTTPUnprocessableEntity() + + try: + volume_types.create(context, name, specs) + vol_type = volume_types.get_volume_type_by_name(context, name) + except exception.VolumeTypeExists as err: + raise webob.exc.HTTPConflict(explanation=str(err)) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return {'volume_type': vol_type} + + @wsgi.action("delete") + def _delete(self, req, id): + """ Deletes an existing volume type """ + context = req.environ['nova.context'] + authorize(context) + + try: + vol_type = volume_types.get_volume_type(context, id) + volume_types.destroy(context, vol_type['name']) + except exception.NotFound: + raise webob.exc.HTTPNotFound() + + return webob.Response(status_int=202) + + +class Types_manage(extensions.ExtensionDescriptor): + """Types manage support""" + + name = "TypesManage" + alias = "os-types-manage" + namespace = "http://docs.openstack.org/volume/ext/types-manage/api/v1" + updated = "2011-08-24T00:00:00+00:00" + + def get_controller_extensions(self): + controller = VolumeTypesManageController() + extension = extensions.ControllerExtension(self, 'types', controller) + return [extension] diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index ff1b5a3a47b1..e200688b7452 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -3866,7 +3866,7 @@ def volume_type_get(context, id, session=None): first() if not result: - raise exception.VolumeTypeNotFound(volume_type=id) + raise exception.VolumeTypeNotFound(volume_type_id=id) return _dict_with_extra_specs(result) diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py index e69de29bb2d1..7e04e7c73b39 100644 --- a/nova/tests/api/openstack/__init__.py +++ b/nova/tests/api/openstack/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/compute/__init__.py b/nova/tests/api/openstack/compute/__init__.py index 00fcfbb009e7..7e04e7c73b39 100644 --- a/nova/tests/api/openstack/compute/__init__.py +++ b/nova/tests/api/openstack/compute/__init__.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,3 +14,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/compute/contrib/__init__.py b/nova/tests/api/openstack/compute/contrib/__init__.py index 848908a953a2..7e04e7c73b39 100644 --- a/nova/tests/api/openstack/compute/contrib/__init__.py +++ b/nova/tests/api/openstack/compute/contrib/__init__.py @@ -1,6 +1,7 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2011 OpenStack LLC +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -13,3 +14,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/volume/__init__.py b/nova/tests/api/openstack/volume/__init__.py index 00fcfbb009e7..7e04e7c73b39 100644 --- a/nova/tests/api/openstack/volume/__init__.py +++ b/nova/tests/api/openstack/volume/__init__.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright 2010 OpenStack LLC. +# Copyright 2011 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -14,3 +14,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/volume/contrib/__init__.py b/nova/tests/api/openstack/volume/contrib/__init__.py new file mode 100644 index 000000000000..7e04e7c73b39 --- /dev/null +++ b/nova/tests/api/openstack/volume/contrib/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# NOTE(vish): this forces the fixtures from tests/__init.py:setup() to work +from nova.tests import * diff --git a/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py b/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py new file mode 100644 index 000000000000..c25ec8c01c8b --- /dev/null +++ b/nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py @@ -0,0 +1,202 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 Zadara Storage Inc. +# Copyright (c) 2011 OpenStack LLC. +# Copyright 2011 University of Southern California +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from lxml import etree +import webob + +from nova.api.openstack.volume.contrib import types_extra_specs +from nova import test +from nova.tests.api.openstack import fakes +import nova.wsgi + + +def return_create_volume_type_extra_specs(context, volume_type_id, + extra_specs): + return stub_volume_type_extra_specs() + + +def return_volume_type_extra_specs(context, volume_type_id): + return stub_volume_type_extra_specs() + + +def return_empty_volume_type_extra_specs(context, volume_type_id): + return {} + + +def delete_volume_type_extra_specs(context, volume_type_id, key): + pass + + +def stub_volume_type_extra_specs(): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return specs + + +def volume_type_get(context, volume_type_id): + pass + + +class VolumeTypesExtraSpecsTest(test.TestCase): + + def setUp(self): + super(VolumeTypesExtraSpecsTest, self).setUp() + self.stubs.Set(nova.db, 'volume_type_get', volume_type_get) + self.api_path = '/v1/fake/os-volume-types/1/extra_specs' + self.controller = types_extra_specs.VolumeTypeExtraSpecsController() + + def test_index(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + res_dict = self.controller.index(req, 1) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + + def test_index_no_data(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_empty_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + res_dict = self.controller.index(req, 1) + + self.assertEqual(0, len(res_dict['extra_specs'])) + + def test_show(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key5') + res_dict = self.controller.show(req, 1, 'key5') + + self.assertEqual('value5', res_dict['key5']) + + def test_show_spec_not_found(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_get', + return_empty_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key6') + self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, + req, 1, 'key6') + + def test_delete(self): + self.stubs.Set(nova.db, 'volume_type_extra_specs_delete', + delete_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key5') + self.controller.delete(req, 1, 'key5') + + def test_create(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"extra_specs": {"key1": "value1"}} + + req = fakes.HTTPRequest.blank(self.api_path) + res_dict = self.controller.create(req, 1, body) + + self.assertEqual('value1', res_dict['extra_specs']['key1']) + + def test_create_empty_body(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, '') + + def test_update_item(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + res_dict = self.controller.update(req, 1, 'key1', body) + + self.assertEqual('value1', res_dict['key1']) + + def test_update_item_empty_body(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', '') + + def test_update_item_too_many_keys(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1", "key2": "value2"} + + req = fakes.HTTPRequest.blank(self.api_path + '/key1') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'key1', body) + + def test_update_item_body_uri_mismatch(self): + self.stubs.Set(nova.db, + 'volume_type_extra_specs_update_or_create', + return_create_volume_type_extra_specs) + body = {"key1": "value1"} + + req = fakes.HTTPRequest.blank(self.api_path + '/bad') + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update, + req, 1, 'bad', body) + + +class VolumeTypeExtraSpecsSerializerTest(test.TestCase): + def test_index_create_serializer(self): + serializer = types_extra_specs.VolumeTypeExtraSpecsTemplate() + + # Just getting some input data + extra_specs = stub_volume_type_extra_specs() + text = serializer.serialize(dict(extra_specs=extra_specs)) + + print text + tree = etree.fromstring(text) + + self.assertEqual('extra_specs', tree.tag) + self.assertEqual(len(extra_specs), len(tree)) + seen = set(extra_specs.keys()) + for child in tree: + self.assertTrue(child.tag in seen) + self.assertEqual(extra_specs[child.tag], child.text) + seen.remove(child.tag) + self.assertEqual(len(seen), 0) + + def test_update_show_serializer(self): + serializer = types_extra_specs.VolumeTypeExtraSpecTemplate() + + exemplar = dict(key1='value1') + text = serializer.serialize(exemplar) + + print text + tree = etree.fromstring(text) + + self.assertEqual('key1', tree.tag) + self.assertEqual('value1', tree.text) + self.assertEqual(0, len(tree)) diff --git a/nova/tests/api/openstack/volume/contrib/test_types_manage.py b/nova/tests/api/openstack/volume/contrib/test_types_manage.py new file mode 100644 index 000000000000..6ec049dfafe1 --- /dev/null +++ b/nova/tests/api/openstack/volume/contrib/test_types_manage.py @@ -0,0 +1,103 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob + +from nova.api.openstack.volume.contrib import types_manage +from nova import exception +from nova import test +from nova.volume import volume_types +from nova.tests.api.openstack import fakes + + +def stub_volume_type(id): + specs = { + "key1": "value1", + "key2": "value2", + "key3": "value3", + "key4": "value4", + "key5": "value5"} + return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs) + + +def return_volume_types_get_volume_type(context, id): + if id == "777": + raise exception.VolumeTypeNotFound(volume_type_id=id) + return stub_volume_type(int(id)) + + +def return_volume_types_destroy(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + pass + + +def return_volume_types_create(context, name, specs): + pass + + +def return_volume_types_get_by_name(context, name): + if name == "777": + raise exception.VolumeTypeNotFoundByName(volume_type_name=name) + return stub_volume_type(int(name.split("_")[2])) + + +class VolumeTypesManageApiTest(test.TestCase): + def setUp(self): + super(VolumeTypesManageApiTest, self).setUp() + self.controller = types_manage.VolumeTypesManageController() + + def test_volume_types_delete(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + self.stubs.Set(volume_types, 'destroy', + return_volume_types_destroy) + + req = fakes.HTTPRequest.blank('/v1/fake/types/1') + self.controller._delete(req, 1) + + def test_volume_types_delete_not_found(self): + self.stubs.Set(volume_types, 'get_volume_type', + return_volume_types_get_volume_type) + self.stubs.Set(volume_types, 'destroy', + return_volume_types_destroy) + + req = fakes.HTTPRequest.blank('/v1/fake/types/777') + self.assertRaises(webob.exc.HTTPNotFound, self.controller._delete, + req, '777') + + def test_create(self): + self.stubs.Set(volume_types, 'create', + return_volume_types_create) + self.stubs.Set(volume_types, 'get_volume_type_by_name', + return_volume_types_get_by_name) + + body = {"volume_type": {"name": "vol_type_1", + "extra_specs": {"key1": "value1"}}} + req = fakes.HTTPRequest.blank('/v1/fake/types') + res_dict = self.controller._create(req, body) + + self.assertEqual(1, len(res_dict)) + self.assertEqual('vol_type_1', res_dict['volume_type']['name']) + + def test_create_empty_body(self): + self.stubs.Set(volume_types, 'create', + return_volume_types_create) + self.stubs.Set(volume_types, 'get_volume_type_by_name', + return_volume_types_get_by_name) + + req = fakes.HTTPRequest.blank('/v1/fake/types') + self.assertRaises(webob.exc.HTTPUnprocessableEntity, + self.controller._create, req, '') diff --git a/nova/tests/api/openstack/volume/extensions/__init__.py b/nova/tests/api/openstack/volume/extensions/__init__.py new file mode 100644 index 000000000000..848908a953a2 --- /dev/null +++ b/nova/tests/api/openstack/volume/extensions/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/nova/tests/api/openstack/volume/extensions/foxinsocks.py b/nova/tests/api/openstack/volume/extensions/foxinsocks.py new file mode 100644 index 000000000000..cf901472c6b6 --- /dev/null +++ b/nova/tests/api/openstack/volume/extensions/foxinsocks.py @@ -0,0 +1,94 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import webob.exc + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi + + +class FoxInSocksController(object): + + def index(self, req): + return "Try to say this Mr. Knox, sir..." + + +class FoxInSocksServerControllerExtension(wsgi.Controller): + @wsgi.action('add_tweedle') + def _add_tweedle(self, req, id, body): + + return "Tweedle Beetle Added." + + @wsgi.action('delete_tweedle') + def _delete_tweedle(self, req, id, body): + + return "Tweedle Beetle Deleted." + + @wsgi.action('fail') + def _fail(self, req, id, body): + + raise webob.exc.HTTPBadRequest(explanation='Tweedle fail') + + +class FoxInSocksFlavorGooseControllerExtension(wsgi.Controller): + @wsgi.extends + def show(self, req, resp_obj, id): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + resp_obj.obj['flavor']['googoose'] = req.GET.get('chewing') + + +class FoxInSocksFlavorBandsControllerExtension(wsgi.Controller): + @wsgi.extends + def show(self, req, resp_obj, id): + #NOTE: This only handles JSON responses. + # You can use content type header to test for XML. + resp_obj.obj['big_bands'] = 'Pig Bands!' + + +class Foxinsocks(extensions.ExtensionDescriptor): + """The Fox In Socks Extension""" + + name = "Fox In Socks" + alias = "FOXNSOX" + namespace = "http://www.fox.in.socks/api/ext/pie/v1.0" + updated = "2011-01-22T13:25:27-06:00" + + def __init__(self, ext_mgr): + ext_mgr.register(self) + + def get_resources(self): + resources = [] + resource = extensions.ResourceExtension('foxnsocks', + FoxInSocksController()) + resources.append(resource) + return resources + + def get_controller_extensions(self): + extension_list = [] + + extension_set = [ + (FoxInSocksServerControllerExtension, 'servers'), + (FoxInSocksFlavorGooseControllerExtension, 'flavors'), + (FoxInSocksFlavorBandsControllerExtension, 'flavors'), + ] + for klass, collection in extension_set: + controller = klass() + ext = extensions.ControllerExtension(self, collection, controller) + extension_list.append(ext) + + return extension_list diff --git a/nova/tests/api/openstack/volume/test_extensions.py b/nova/tests/api/openstack/volume/test_extensions.py new file mode 100644 index 000000000000..f538c8b90661 --- /dev/null +++ b/nova/tests/api/openstack/volume/test_extensions.py @@ -0,0 +1,156 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 X.commerce, a business unit of eBay Inc. +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json + +import webob +from lxml import etree +import iso8601 + +from nova.api.openstack import volume +from nova.api.openstack import xmlutil +from nova import flags +from nova import test + +FLAGS = flags.FLAGS +NS = "{http://docs.openstack.org/common/api/v1.0}" + + +class ExtensionTestCase(test.TestCase): + def setUp(self): + super(ExtensionTestCase, self).setUp() + ext_list = FLAGS.osapi_volume_extension[:] + fox = ('nova.tests.api.openstack.volume.extensions.' + 'foxinsocks.Foxinsocks') + if fox not in ext_list: + ext_list.append(fox) + self.flags(osapi_volume_extension=ext_list) + + +class ExtensionControllerTest(ExtensionTestCase): + + def setUp(self): + super(ExtensionControllerTest, self).setUp() + self.ext_list = [ + "TypesManage", + "TypesExtraSpecs", + ] + self.ext_list.sort() + + def test_list_extensions_json(self): + app = volume.APIRouter() + request = webob.Request.blank("/fake/extensions") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + # Make sure we have all the extensions, extra extensions being OK. + data = json.loads(response.body) + names = [str(x['name']) for x in data['extensions'] + if str(x['name']) in self.ext_list] + names.sort() + self.assertEqual(names, self.ext_list) + + # Ensure all the timestamps are valid according to iso8601 + for ext in data['extensions']: + iso8601.parse_date(ext['updated']) + + # Make sure that at least Fox in Sox is correct. + (fox_ext, ) = [ + x for x in data['extensions'] if x['alias'] == 'FOXNSOX'] + self.assertEqual(fox_ext, { + 'namespace': 'http://www.fox.in.socks/api/ext/pie/v1.0', + 'name': 'Fox In Socks', + 'updated': '2011-01-22T13:25:27-06:00', + 'description': 'The Fox In Socks Extension', + 'alias': 'FOXNSOX', + 'links': [] + }, + ) + + for ext in data['extensions']: + url = '/fake/extensions/%s' % ext['alias'] + request = webob.Request.blank(url) + response = request.get_response(app) + output = json.loads(response.body) + self.assertEqual(output['extension']['alias'], ext['alias']) + + def test_get_extension_json(self): + app = volume.APIRouter() + request = webob.Request.blank("/fake/extensions/FOXNSOX") + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + data = json.loads(response.body) + self.assertEqual(data['extension'], { + "namespace": "http://www.fox.in.socks/api/ext/pie/v1.0", + "name": "Fox In Socks", + "updated": "2011-01-22T13:25:27-06:00", + "description": "The Fox In Socks Extension", + "alias": "FOXNSOX", + "links": []}) + + def test_get_non_existing_extension_json(self): + app = volume.APIRouter() + request = webob.Request.blank("/fake/extensions/4") + response = request.get_response(app) + self.assertEqual(404, response.status_int) + + def test_list_extensions_xml(self): + app = volume.APIRouter() + request = webob.Request.blank("/fake/extensions") + request.accept = "application/xml" + response = request.get_response(app) + self.assertEqual(200, response.status_int) + + root = etree.XML(response.body) + self.assertEqual(root.tag.split('extensions')[0], NS) + + # Make sure we have all the extensions, extras extensions being OK. + exts = root.findall('{0}extension'.format(NS)) + self.assert_(len(exts) >= len(self.ext_list)) + + # Make sure that at least Fox in Sox is correct. + (fox_ext, ) = [x for x in exts if x.get('alias') == 'FOXNSOX'] + self.assertEqual(fox_ext.get('name'), 'Fox In Socks') + self.assertEqual(fox_ext.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(fox_ext.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(fox_ext.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension') + + xmlutil.validate_schema(root, 'extensions') + + def test_get_extension_xml(self): + app = volume.APIRouter() + request = webob.Request.blank("/fake/extensions/FOXNSOX") + request.accept = "application/xml" + response = request.get_response(app) + self.assertEqual(200, response.status_int) + xml = response.body + + root = etree.XML(xml) + self.assertEqual(root.tag.split('extension')[0], NS) + self.assertEqual(root.get('alias'), 'FOXNSOX') + self.assertEqual(root.get('name'), 'Fox In Socks') + self.assertEqual(root.get('namespace'), + 'http://www.fox.in.socks/api/ext/pie/v1.0') + self.assertEqual(root.get('updated'), '2011-01-22T13:25:27-06:00') + self.assertEqual(root.findtext('{0}description'.format(NS)), + 'The Fox In Socks Extension') + + xmlutil.validate_schema(root, 'extension') diff --git a/nova/tests/api/openstack/volume/test_types.py b/nova/tests/api/openstack/volume/test_types.py index 5016732ff9d7..4e1026c7c545 100644 --- a/nova/tests/api/openstack/volume/test_types.py +++ b/nova/tests/api/openstack/volume/test_types.py @@ -19,15 +19,10 @@ import webob from nova.api.openstack.volume import types from nova import exception from nova import test -from nova import log as logging from nova.volume import volume_types from nova.tests.api.openstack import fakes -LOG = logging.getLogger(__name__) -last_param = {} - - def stub_volume_type(id): specs = { "key1": "value1", @@ -54,16 +49,6 @@ def return_volume_types_get_volume_type(context, id): return stub_volume_type(int(id)) -def return_volume_types_destroy(context, name): - if name == "777": - raise exception.VolumeTypeNotFoundByName(volume_type_name=name) - pass - - -def return_volume_types_create(context, name, specs): - pass - - def return_volume_types_get_by_name(context, name): if name == "777": raise exception.VolumeTypeNotFoundByName(volume_type_name=name) @@ -73,14 +58,13 @@ def return_volume_types_get_by_name(context, name): class VolumeTypesApiTest(test.TestCase): def setUp(self): super(VolumeTypesApiTest, self).setUp() - fakes.stub_out_key_pair_funcs(self.stubs) self.controller = types.VolumeTypesController() def test_volume_types_index(self): self.stubs.Set(volume_types, 'get_all_types', return_volume_types_get_all_types) - req = fakes.HTTPRequest.blank('/v2/123/os-volume-types') + req = fakes.HTTPRequest.blank('/v1/fake/types') res_dict = self.controller.index(req) self.assertEqual(3, len(res_dict['volume_types'])) @@ -95,7 +79,7 @@ class VolumeTypesApiTest(test.TestCase): self.stubs.Set(volume_types, 'get_all_types', return_empty_volume_types_get_all_types) - req = fakes.HTTPRequest.blank('/v2/123/os-volume-types') + req = fakes.HTTPRequest.blank('/v1/fake/types') res_dict = self.controller.index(req) self.assertEqual(0, len(res_dict['volume_types'])) @@ -104,7 +88,7 @@ class VolumeTypesApiTest(test.TestCase): self.stubs.Set(volume_types, 'get_volume_type', return_volume_types_get_volume_type) - req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/1') + req = fakes.HTTPRequest.blank('/v1/fake/types/1') res_dict = self.controller.show(req, 1) self.assertEqual(1, len(res_dict)) @@ -115,7 +99,7 @@ class VolumeTypesApiTest(test.TestCase): self.stubs.Set(volume_types, 'get_volume_type', return_volume_types_get_volume_type) - req = fakes.HTTPRequest.blank('/v2/123/os-volume-types/777') + req = fakes.HTTPRequest.blank('/v1/fake/types/777') self.assertRaises(webob.exc.HTTPNotFound, self.controller.show, req, '777') @@ -142,7 +126,6 @@ class VolumeTypesSerializerTest(test.TestCase): vtypes = return_volume_types_get_all_types(None) text = serializer.serialize({'volume_types': vtypes.values()}) - print text tree = etree.fromstring(text) self.assertEqual('volume_types', tree.tag) @@ -158,7 +141,6 @@ class VolumeTypesSerializerTest(test.TestCase): vtype = stub_volume_type(1) text = serializer.serialize(dict(volume_type=vtype)) - print text tree = etree.fromstring(text) self._verify_volume_type(vtype, tree) diff --git a/nova/tests/policy.json b/nova/tests/policy.json index e726b8bf0f15..16c8066906b6 100644 --- a/nova/tests/policy.json +++ b/nova/tests/policy.json @@ -116,7 +116,6 @@ "compute_extension:zones": [], - "volume:create": [], "volume:get": [], "volume:get_all": [], @@ -125,7 +124,6 @@ "volume:update": [], "volume:delete_volume_metadata": [], "volume:update_volume_metadata": [], - "volume:attach": [], "volume:detach": [], "volume:reserve_volume": [], @@ -134,13 +132,16 @@ "volume:check_detach": [], "volume:initialize_connection": [], "volume:terminate_connection": [], - "volume:create_snapshot": [], "volume:delete_snapshot": [], "volume:get_snapshot": [], "volume:get_all_snapshots": [], + "volume_extension:types_manage": [], + "volume_extension:types_extra_specs": [], + + "network:get_all_networks": [], "network:get_network": [], "network:delete_network": [],