diff options
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/flavor_access.py | 224 | ||||
-rw-r--r-- | nova/tests/api/openstack/compute/plugins/v3/test_flavor_access.py | 308 |
2 files changed, 532 insertions, 0 deletions
diff --git a/nova/api/openstack/compute/plugins/v3/flavor_access.py b/nova/api/openstack/compute/plugins/v3/flavor_access.py new file mode 100644 index 000000000..bea92d883 --- /dev/null +++ b/nova/api/openstack/compute/plugins/v3/flavor_access.py @@ -0,0 +1,224 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack Foundation +# 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. + +"""The flavor access extension.""" + +import webob + +from nova.api.openstack import extensions +from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil +from nova.compute import flavors +from nova import exception + + +authorize = extensions.soft_extension_authorizer('compute', 'flavor_access') + + +def make_flavor(elem): + elem.set('{%s}is_public' % Flavor_access.namespace, + '%s:is_public' % Flavor_access.alias) + + +def make_flavor_access(elem): + elem.set('flavor_id') + elem.set('tenant_id') + + +class FlavorTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavor', selector='flavor') + make_flavor(root) + alias = Flavor_access.alias + namespace = Flavor_access.namespace + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) + + +class FlavorsTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavors') + elem = xmlutil.SubTemplateElement(root, 'flavor', selector='flavors') + make_flavor(elem) + alias = Flavor_access.alias + namespace = Flavor_access.namespace + return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace}) + + +class FlavorAccessTemplate(xmlutil.TemplateBuilder): + def construct(self): + root = xmlutil.TemplateElement('flavor_access') + elem = xmlutil.SubTemplateElement(root, 'access', + selector='flavor_access') + make_flavor_access(elem) + return xmlutil.MasterTemplate(root, 1) + + +def _marshall_flavor_access(flavor_id): + rval = [] + try: + access_list = flavors.\ + get_flavor_access_by_flavor_id(flavor_id) + except exception.FlavorNotFound: + explanation = _("Flavor not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + for access in access_list: + rval.append({'flavor_id': flavor_id, + 'tenant_id': access['project_id']}) + + return {'flavor_access': rval} + + +class FlavorAccessController(object): + """The flavor access API controller for the OpenStack API.""" + + def __init__(self): + super(FlavorAccessController, self).__init__() + + @wsgi.serializers(xml=FlavorAccessTemplate) + def index(self, req, flavor_id): + context = req.environ['nova.context'] + authorize(context) + + try: + flavor = flavors.get_flavor_by_flavor_id(flavor_id) + except exception.FlavorNotFound: + explanation = _("Flavor not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + # public flavor to all projects + if flavor['is_public']: + explanation = _("Access list not available for public flavors.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + # private flavor to listed projects only + return _marshall_flavor_access(flavor_id) + + +class FlavorActionController(wsgi.Controller): + """The flavor access API controller for the OpenStack API.""" + + def _check_body(self, body): + if body is None or body == "": + raise webob.exc.HTTPBadRequest(explanation=_("No request body")) + + def _get_flavor_refs(self, context): + """Return a dictionary mapping flavorid to flavor_ref.""" + + flavor_refs = flavors.get_all_flavors(context) + rval = {} + for name, obj in flavor_refs.iteritems(): + rval[obj['flavorid']] = obj + return rval + + def _extend_flavor(self, flavor_rval, flavor_ref): + key = "%s:is_public" % (Flavor_access.alias) + flavor_rval[key] = flavor_ref['is_public'] + + @wsgi.extends + def show(self, req, resp_obj, id): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=FlavorTemplate()) + db_flavor = req.get_db_flavor(id) + + self._extend_flavor(resp_obj.obj['flavor'], db_flavor) + + @wsgi.extends + def detail(self, req, resp_obj): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=FlavorsTemplate()) + + flavors = list(resp_obj.obj['flavors']) + for flavor_rval in flavors: + db_flavor = req.get_db_flavor(flavor_rval['id']) + self._extend_flavor(flavor_rval, db_flavor) + + @wsgi.extends(action='create') + def create(self, req, body, resp_obj): + context = req.environ['nova.context'] + if authorize(context): + # Attach our slave template to the response object + resp_obj.attach(xml=FlavorTemplate()) + + db_flavor = req.get_db_flavor(resp_obj.obj['flavor']['id']) + + self._extend_flavor(resp_obj.obj['flavor'], db_flavor) + + @wsgi.serializers(xml=FlavorAccessTemplate) + @wsgi.action("addTenantAccess") + def _addTenantAccess(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + self._check_body(body) + + vals = body['addTenantAccess'] + tenant = vals['tenant'] + + try: + flavors.add_flavor_access(id, tenant, context) + except exception.FlavorAccessExists as err: + raise webob.exc.HTTPConflict(explanation=err.format_message()) + + return _marshall_flavor_access(id) + + @wsgi.serializers(xml=FlavorAccessTemplate) + @wsgi.action("removeTenantAccess") + def _removeTenantAccess(self, req, id, body): + context = req.environ['nova.context'] + authorize(context) + self._check_body(body) + + vals = body['removeTenantAccess'] + tenant = vals['tenant'] + + try: + flavors.remove_flavor_access(id, tenant, context) + except exception.FlavorAccessNotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.format_message()) + + return _marshall_flavor_access(id) + + +class Flavor_access(extensions.ExtensionDescriptor): + """Flavor access support.""" + + name = "FlavorAccess" + alias = "os-flavor-access" + namespace = ("http://docs.openstack.org/compute/ext/" + "flavor_access/api/v2") + updated = "2012-08-01T00:00:00+00:00" + + def get_resources(self): + resources = [] + + res = extensions.ResourceExtension( + 'os-flavor-access', + controller=FlavorAccessController(), + parent=dict(member_name='flavor', collection_name='flavors')) + resources.append(res) + + return resources + + def get_controller_extensions(self): + extension = extensions.ControllerExtension( + self, 'flavors', FlavorActionController()) + + return [extension] diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_flavor_access.py b/nova/tests/api/openstack/compute/plugins/v3/test_flavor_access.py new file mode 100644 index 000000000..d072e0784 --- /dev/null +++ b/nova/tests/api/openstack/compute/plugins/v3/test_flavor_access.py @@ -0,0 +1,308 @@ +# Copyright 2012 OpenStack Foundation +# 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 datetime + +from lxml import etree +from webob import exc + +from nova.api.openstack.compute.contrib import flavor_access +from nova.api.openstack.compute import flavors as flavors_api +from nova.compute import flavors +from nova import context +from nova import exception +from nova import test +from nova.tests.api.openstack import fakes + + +def generate_flavor(flavorid, ispublic): + return { + 'id': flavorid, + 'flavorid': str(flavorid), + 'root_gb': 1, + 'ephemeral_gb': 1, + 'name': u'test', + 'deleted': False, + 'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1), + 'updated_at': None, + 'memory_mb': 512, + 'vcpus': 1, + 'swap': 512, + 'rxtx_factor': 1.0, + 'extra_specs': {}, + 'deleted_at': None, + 'vcpu_weight': None, + 'is_public': bool(ispublic) + } + + +INSTANCE_TYPES = { + '0': generate_flavor(0, True), + '1': generate_flavor(1, True), + '2': generate_flavor(2, False), + '3': generate_flavor(3, False)} + + +ACCESS_LIST = [{'flavor_id': '2', 'project_id': 'proj2'}, + {'flavor_id': '2', 'project_id': 'proj3'}, + {'flavor_id': '3', 'project_id': 'proj3'}] + + +def fake_get_flavor_access_by_flavor_id(flavorid): + res = [] + for access in ACCESS_LIST: + if access['flavor_id'] == flavorid: + res.append(access) + return res + + +def fake_get_flavor_by_flavor_id(flavorid): + return INSTANCE_TYPES[flavorid] + + +def _has_flavor_access(flavorid, projectid): + for access in ACCESS_LIST: + if access['flavor_id'] == flavorid and \ + access['project_id'] == projectid: + return True + return False + + +def fake_get_all_flavors(context, inactive=0, filters=None): + if filters == None or filters['is_public'] == None: + return INSTANCE_TYPES + + res = {} + for k, v in INSTANCE_TYPES.iteritems(): + if filters['is_public'] and _has_flavor_access(k, context.project_id): + res.update({k: v}) + continue + if v['is_public'] == filters['is_public']: + res.update({k: v}) + + return res + + +class FakeRequest(object): + environ = {"nova.context": context.get_admin_context()} + + def get_db_flavor(self, flavor_id): + return INSTANCE_TYPES[flavor_id] + + +class FakeResponse(object): + obj = {'flavor': {'id': '0'}, + 'flavors': [ + {'id': '0'}, + {'id': '2'}] + } + + def attach(self, **kwargs): + pass + + +class FlavorAccessTest(test.TestCase): + def setUp(self): + super(FlavorAccessTest, self).setUp() + self.flavor_controller = flavors_api.Controller() + self.flavor_access_controller = flavor_access.FlavorAccessController() + self.flavor_action_controller = flavor_access.FlavorActionController() + self.req = FakeRequest() + self.context = self.req.environ['nova.context'] + self.stubs.Set(flavors, 'get_flavor_by_flavor_id', + fake_get_flavor_by_flavor_id) + self.stubs.Set(flavors, 'get_all_flavors', fake_get_all_flavors) + self.stubs.Set(flavors, 'get_flavor_access_by_flavor_id', + fake_get_flavor_access_by_flavor_id) + + def _verify_flavor_list(self, result, expected): + # result already sorted by flavor_id + self.assertEqual(len(result), len(expected)) + + for d1, d2 in zip(result, expected): + self.assertEqual(d1['id'], d2['id']) + + def test_list_flavor_access_public(self): + # query os-flavor-access on public flavor should return 404 + req = fakes.HTTPRequest.blank('/v2/fake/flavors/os-flavor-access', + use_admin_context=True) + self.assertRaises(exc.HTTPNotFound, + self.flavor_access_controller.index, + self.req, '1') + + def test_list_flavor_access_private(self): + expected = {'flavor_access': [ + {'flavor_id': '2', 'tenant_id': 'proj2'}, + {'flavor_id': '2', 'tenant_id': 'proj3'}]} + result = self.flavor_access_controller.index(self.req, '2') + self.assertEqual(result, expected) + + def test_list_flavor_with_admin_default_proj1(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors', + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj1' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_default_proj2(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors', + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj2' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_true(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=true', + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_false(self): + expected = {'flavors': [{'id': '2'}, {'id': '3'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=false', + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_false_proj2(self): + expected = {'flavors': [{'id': '2'}, {'id': '3'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=false', + use_admin_context=True) + req.environ['nova.context'].project_id = 'proj2' + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_admin_ispublic_none(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}, {'id': '2'}, + {'id': '3'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=none', + use_admin_context=True) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_default(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors', + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_true(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=true', + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_false(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=false', + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_list_flavor_with_no_admin_ispublic_none(self): + expected = {'flavors': [{'id': '0'}, {'id': '1'}]} + req = fakes.HTTPRequest.blank('/v2/fake/flavors?is_public=none', + use_admin_context=False) + result = self.flavor_controller.index(req) + self._verify_flavor_list(result['flavors'], expected['flavors']) + + def test_show(self): + resp = FakeResponse() + self.flavor_action_controller.show(self.req, resp, '0') + self.assertEqual({'id': '0', 'os-flavor-access:is_public': True}, + resp.obj['flavor']) + self.flavor_action_controller.show(self.req, resp, '2') + self.assertEqual({'id': '0', 'os-flavor-access:is_public': False}, + resp.obj['flavor']) + + def test_detail(self): + resp = FakeResponse() + self.flavor_action_controller.detail(self.req, resp) + self.assertEqual([{'id': '0', 'os-flavor-access:is_public': True}, + {'id': '2', 'os-flavor-access:is_public': False}], + resp.obj['flavors']) + + def test_create(self): + resp = FakeResponse() + self.flavor_action_controller.create(self.req, {}, resp) + self.assertEqual({'id': '0', 'os-flavor-access:is_public': True}, + resp.obj['flavor']) + + def test_add_tenant_access(self): + def stub_add_flavor_access(flavorid, projectid, ctxt=None): + self.assertEqual('3', flavorid, "flavorid") + self.assertEqual("proj2", projectid, "projectid") + self.stubs.Set(flavors, 'add_flavor_access', + stub_add_flavor_access) + expected = {'flavor_access': + [{'flavor_id': '3', 'tenant_id': 'proj3'}]} + body = {'addTenantAccess': {'tenant': 'proj2'}} + req = fakes.HTTPRequest.blank('/v2/fake/flavors/2/action', + use_admin_context=True) + result = self.flavor_action_controller.\ + _addTenantAccess(req, '3', body) + self.assertEqual(result, expected) + + def test_add_tenant_access_with_already_added_access(self): + def stub_add_flavor_access(flavorid, projectid, ctxt=None): + raise exception.FlavorAccessExists(flavor_id=flavorid, + project_id=projectid) + self.stubs.Set(flavors, 'add_flavor_access', + stub_add_flavor_access) + body = {'addTenantAccess': {'tenant': 'proj2'}} + req = fakes.HTTPRequest.blank('/v2/fake/flavors/2/action', + use_admin_context=True) + self.assertRaises(exc.HTTPConflict, + self.flavor_action_controller._addTenantAccess, + self.req, '3', body) + + def test_remove_tenant_access_with_bad_access(self): + def stub_remove_flavor_access(flavorid, projectid, ctxt=None): + raise exception.FlavorAccessNotFound(flavor_id=flavorid, + project_id=projectid) + self.stubs.Set(flavors, 'remove_flavor_access', + stub_remove_flavor_access) + body = {'removeTenantAccess': {'tenant': 'proj2'}} + req = fakes.HTTPRequest.blank('/v2/fake/flavors/2/action', + use_admin_context=True) + self.assertRaises(exc.HTTPNotFound, + self.flavor_action_controller._removeTenantAccess, + self.req, '3', body) + + +class FlavorAccessSerializerTest(test.TestCase): + def test_serializer_empty(self): + serializer = flavor_access.FlavorAccessTemplate() + text = serializer.serialize(dict(flavor_access=[])) + tree = etree.fromstring(text) + self.assertEqual(len(tree), 0) + + def test_serializer(self): + expected = ("<?xml version='1.0' encoding='UTF-8'?>\n" + '<flavor_access>' + '<access tenant_id="proj2" flavor_id="2"/>' + '<access tenant_id="proj3" flavor_id="2"/>' + '</flavor_access>') + access_list = [{'flavor_id': '2', 'tenant_id': 'proj2'}, + {'flavor_id': '2', 'tenant_id': 'proj3'}] + + serializer = flavor_access.FlavorAccessTemplate() + text = serializer.serialize(dict(flavor_access=access_list)) + self.assertEqual(text, expected) |