summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-04-30 21:18:34 +0000
committerGerrit Code Review <review@openstack.org>2012-04-30 21:18:34 +0000
commit89be02810fd02497beaf8541b35345c31c03bde0 (patch)
treeb697eda53fbb64e203c42f7abfdfcf53884a739d
parent62bcf379597c15606fca6a0255dc731a048a5029 (diff)
parent15c0847341b2a182019c865ec787c129fb21dae6 (diff)
downloadnova-89be02810fd02497beaf8541b35345c31c03bde0.tar.gz
nova-89be02810fd02497beaf8541b35345c31c03bde0.tar.xz
nova-89be02810fd02497beaf8541b35345c31c03bde0.zip
Merge "Port types and extra specs to volume api"
-rw-r--r--etc/nova/policy.json4
-rw-r--r--nova/api/openstack/compute/contrib/volumetypes.py27
-rw-r--r--nova/api/openstack/volume/contrib/types_extra_specs.py152
-rw-r--r--nova/api/openstack/volume/contrib/types_manage.py91
-rw-r--r--nova/db/sqlalchemy/api.py2
-rw-r--r--nova/tests/api/openstack/__init__.py19
-rw-r--r--nova/tests/api/openstack/compute/__init__.py5
-rw-r--r--nova/tests/api/openstack/compute/contrib/__init__.py6
-rw-r--r--nova/tests/api/openstack/volume/__init__.py5
-rw-r--r--nova/tests/api/openstack/volume/contrib/__init__.py19
-rw-r--r--nova/tests/api/openstack/volume/contrib/test_types_extra_specs.py202
-rw-r--r--nova/tests/api/openstack/volume/contrib/test_types_manage.py103
-rw-r--r--nova/tests/api/openstack/volume/extensions/__init__.py15
-rw-r--r--nova/tests/api/openstack/volume/extensions/foxinsocks.py94
-rw-r--r--nova/tests/api/openstack/volume/test_extensions.py156
-rw-r--r--nova/tests/api/openstack/volume/test_types.py26
-rw-r--r--nova/tests/policy.json7
17 files changed, 883 insertions, 50 deletions
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index e52518c5c..481620a09 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 16a423f80..4255ac149 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 000000000..d70e24538
--- /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 000000000..e84f787ab
--- /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 ea51546fd..2785b2b03 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -3883,7 +3883,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 e69de29bb..7e04e7c73 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 00fcfbb00..7e04e7c73 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 848908a95..7e04e7c73 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 00fcfbb00..7e04e7c73 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 000000000..7e04e7c73
--- /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 000000000..c25ec8c01
--- /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 000000000..6ec049dfa
--- /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 000000000..848908a95
--- /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 000000000..cf901472c
--- /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 000000000..f538c8b90
--- /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 5016732ff..4e1026c7c 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 e726b8bf0..16c806690 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": [],