From 34c012c709cc5ae577330c7d67ba060293158210 Mon Sep 17 00:00:00 2001 From: unicell Date: Mon, 13 Aug 2012 20:19:54 +0800 Subject: Implement project specific flavors API blueprint project-specific-flavors This change implements API extension to manage project specific flavor types, so that non-public flavor type can only see by projects with access rights. Change-Id: Ie2d2c605065b0c76897f843a4548a0c984a05f1a --- nova/api/openstack/compute/__init__.py | 3 +- .../api/openstack/compute/contrib/flavor_access.py | 240 +++++++++++++++++++++ .../openstack/compute/contrib/flavorextradata.py | 6 +- nova/api/openstack/compute/contrib/flavormanage.py | 3 +- nova/api/openstack/compute/flavors.py | 28 ++- 5 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 nova/api/openstack/compute/contrib/flavor_access.py (limited to 'nova/api') diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py index 70ec80dc2..9bf36bf25 100644 --- a/nova/api/openstack/compute/__init__.py +++ b/nova/api/openstack/compute/__init__.py @@ -90,7 +90,8 @@ class APIRouter(nova.api.openstack.APIRouter): self.resources['flavors'] = flavors.create_resource() mapper.resource("flavor", "flavors", controller=self.resources['flavors'], - collection={'detail': 'GET'}) + collection={'detail': 'GET'}, + member={'action': 'POST'}) self.resources['image_metadata'] = image_metadata.create_resource() image_metadata_controller = self.resources['image_metadata'] diff --git a/nova/api/openstack/compute/contrib/flavor_access.py b/nova/api/openstack/compute/contrib/flavor_access.py new file mode 100644 index 000000000..433d0c75d --- /dev/null +++ b/nova/api/openstack/compute/contrib/flavor_access.py @@ -0,0 +1,240 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 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. + +"""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 instance_types +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 FlavorextradatumTemplate(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 FlavorextradataTemplate(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): + def wrapped(obj, do_raise=False): + # wrap bare list in dict + return dict(flavor_access=obj) + + root = xmlutil.TemplateElement('flavor_access', selector=wrapped) + 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 = instance_types.\ + get_instance_type_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 = instance_types.get_instance_type_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 = instance_types.get_all_types(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=FlavorextradatumTemplate()) + + try: + flavor_ref = instance_types.get_instance_type_by_flavor_id(id) + except exception.FlavorNotFound: + explanation = _("Flavor not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + self._extend_flavor(resp_obj.obj['flavor'], flavor_ref) + + @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=FlavorextradataTemplate()) + + flavors = list(resp_obj.obj['flavors']) + flavor_refs = self._get_flavor_refs(context) + + for flavor_rval in flavors: + flavor_ref = flavor_refs[flavor_rval['id']] + self._extend_flavor(flavor_rval, flavor_ref) + + @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=FlavorextradatumTemplate()) + + try: + fid = resp_obj.obj['flavor']['id'] + flavor_ref = instance_types.get_instance_type_by_flavor_id(fid) + except exception.FlavorNotFound: + explanation = _("Flavor not found.") + raise webob.exc.HTTPNotFound(explanation=explanation) + + self._extend_flavor(resp_obj.obj['flavor'], flavor_ref) + + @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: + instance_types.add_instance_type_access(id, tenant, context) + except exception.FlavorAccessExists as err: + raise webob.exc.HTTPConflict(explanation=str(err)) + + 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: + instance_types.remove_instance_type_access(id, tenant, context) + except exception.FlavorAccessNotFound, e: + raise webob.exc.HTTPNotFound(explanation=str(e)) + + return _marshall_flavor_access(id) + + +class Flavor_access(extensions.ExtensionDescriptor): + """Flavor access supprt""" + + 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/api/openstack/compute/contrib/flavorextradata.py b/nova/api/openstack/compute/contrib/flavorextradata.py index 5a6558ad0..834b68c60 100644 --- a/nova/api/openstack/compute/contrib/flavorextradata.py +++ b/nova/api/openstack/compute/contrib/flavorextradata.py @@ -35,10 +35,10 @@ authorize = extensions.soft_extension_authorizer('compute', 'flavorextradata') class FlavorextradataController(wsgi.Controller): - def _get_flavor_refs(self): + def _get_flavor_refs(self, context): """Return a dictionary mapping flavorid to flavor_ref.""" - flavor_refs = instance_types.get_all_types() + flavor_refs = instance_types.get_all_types(context) rval = {} for name, obj in flavor_refs.iteritems(): rval[obj['flavorid']] = obj @@ -71,7 +71,7 @@ class FlavorextradataController(wsgi.Controller): resp_obj.attach(xml=FlavorextradataTemplate()) flavors = list(resp_obj.obj['flavors']) - flavor_refs = self._get_flavor_refs() + flavor_refs = self._get_flavor_refs(context) for flavor_rval in flavors: flavor_ref = flavor_refs[flavor_rval['id']] diff --git a/nova/api/openstack/compute/contrib/flavormanage.py b/nova/api/openstack/compute/contrib/flavormanage.py index 4dedcf981..3bcbf6981 100644 --- a/nova/api/openstack/compute/contrib/flavormanage.py +++ b/nova/api/openstack/compute/contrib/flavormanage.py @@ -65,11 +65,12 @@ class FlavorManageController(wsgi.Controller): ephemeral_gb = vals.get('OS-FLV-EXT-DATA:ephemeral') swap = vals.get('swap') rxtx_factor = vals.get('rxtx_factor') + is_public = vals.get('os-flavor-access:is_public') try: flavor = instance_types.create(name, memory_mb, vcpus, root_gb, ephemeral_gb, flavorid, - swap, rxtx_factor) + swap, rxtx_factor, is_public) except exception.InstanceTypeExists as err: raise webob.exc.HTTPConflict(explanation=str(err)) diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py index 56b2e18ab..1a96c1346 100644 --- a/nova/api/openstack/compute/flavors.py +++ b/nova/api/openstack/compute/flavors.py @@ -91,12 +91,36 @@ class Controller(wsgi.Controller): return self._view_builder.show(req, flavor) + def _get_is_public(self, req): + """Parse is_public into something usable.""" + is_public = req.params.get('is_public', None) + + if is_public is None: + # preserve default value of showing only public flavors + return True + elif is_public is True or \ + is_public.lower() in ['t', 'true', 'yes', '1']: + return True + elif is_public is False or \ + is_public.lower() in ['f', 'false', 'no', '0']: + return False + elif is_public.lower() == 'none': + # value to match all flavors, ignore is_public + return None + else: + msg = _('Invalid is_public filter [%s]') % req.params['is_public'] + raise webob.exc.HTTPBadRequest(explanation=msg) + def _get_flavors(self, req): """Helper function that returns a list of flavor dicts.""" filters = {} context = req.environ['nova.context'] - if not context.is_admin: + if context.is_admin: + # Only admin has query access to all flavor types + filters['is_public'] = self._get_is_public(req) + else: + filters['is_public'] = True filters['disabled'] = False if 'minRam' in req.params: @@ -113,7 +137,7 @@ class Controller(wsgi.Controller): msg = _('Invalid minDisk filter [%s]') % req.params['minDisk'] raise webob.exc.HTTPBadRequest(explanation=msg) - flavors = instance_types.get_all_types(filters=filters) + flavors = instance_types.get_all_types(context, filters=filters) flavors_list = flavors.values() sorted_flavors = sorted(flavors_list, key=lambda item: item['flavorid']) -- cgit