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
This commit is contained in:
unicell 2012-08-13 20:19:54 +08:00
parent 5e012d8d45
commit 34c012c709
20 changed files with 838 additions and 25 deletions

View File

@ -828,11 +828,12 @@ class InstanceTypeCommands(object):
def _print_instance_types(self, name, val):
deleted = ('', ', inactive')[val["deleted"] == 1]
is_public = ('private', 'public')[val["is_public"] == 1]
print ("%s: Memory: %sMB, VCPUS: %s, Root: %sGB, Ephemeral: %sGb, "
"FlavorID: %s, Swap: %sMB, RXTX Factor: %s, ExtraSpecs %s") % (
"FlavorID: %s, Swap: %sMB, RXTX Factor: %s, %s, ExtraSpecs %s") % (
name, val["memory_mb"], val["vcpus"], val["root_gb"],
val["ephemeral_gb"], val["flavorid"], val["swap"],
val["rxtx_factor"], val["extra_specs"])
val["rxtx_factor"], is_public, val["extra_specs"])
@args('--name', dest='name', metavar='<name>',
help='Name of instance type/flavor')
@ -848,12 +849,15 @@ class InstanceTypeCommands(object):
@args('--swap', dest='swap', metavar='<swap>', help='Swap')
@args('--rxtx_factor', dest='rxtx_factor', metavar='<rxtx_factor>',
help='rxtx_factor')
@args('--is_public', dest="is_public", metavar='<is_public>',
help='Make flavor accessible to the public')
def create(self, name, memory, vcpus, root_gb, ephemeral_gb, flavorid,
swap=0, rxtx_factor=1):
swap=0, rxtx_factor=1, is_public=True):
"""Creates instance types / flavors"""
try:
instance_types.create(name, memory, vcpus, root_gb,
ephemeral_gb, flavorid, swap, rxtx_factor)
ephemeral_gb, flavorid, swap, rxtx_factor,
is_public)
except exception.InvalidInput, e:
print "Must supply valid parameters to create instance_type"
print e

View File

@ -35,6 +35,7 @@
"compute_extension:disk_config": [],
"compute_extension:extended_server_attributes": [["rule:admin_api"]],
"compute_extension:extended_status": [],
"compute_extension:flavor_access": [],
"compute_extension:flavorextradata": [],
"compute_extension:flavorextraspecs": [],
"compute_extension:flavormanage": [["rule:admin_api"]],

View File

@ -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']

View File

@ -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]

View File

@ -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']]

View File

@ -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))

View File

@ -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'])

View File

@ -27,6 +27,7 @@ from nova import db
from nova import exception
from nova import flags
from nova.openstack.common import log as logging
from nova import utils
FLAGS = flags.FLAGS
LOG = logging.getLogger(__name__)
@ -35,7 +36,7 @@ INVALID_NAME_REGEX = re.compile("[^\w\.\- ]")
def create(name, memory, vcpus, root_gb, ephemeral_gb, flavorid, swap=None,
rxtx_factor=None):
rxtx_factor=None, is_public=True):
"""Creates instance types."""
if swap is None:
@ -80,6 +81,9 @@ def create(name, memory, vcpus, root_gb, ephemeral_gb, flavorid, swap=None,
# in through json as an integer, so we convert it here.
kwargs['flavorid'] = unicode(flavorid)
# ensure is_public attribute is boolean
kwargs['is_public'] = utils.bool_from_str(is_public)
try:
return db.instance_type_create(context.get_admin_context(), kwargs)
except exception.DBError, e:
@ -97,12 +101,14 @@ def destroy(name):
raise exception.InstanceTypeNotFoundByName(instance_type_name=name)
def get_all_types(inactive=False, filters=None):
def get_all_types(ctxt=None, inactive=False, filters=None):
"""Get all non-deleted instance_types.
Pass true as argument if you want deleted instance types returned also.
"""
ctxt = context.get_admin_context()
if ctxt is None:
ctxt = context.get_admin_context()
inst_types = db.instance_type_get_all(
ctxt, inactive=inactive, filters=filters)
@ -120,30 +126,60 @@ def get_default_instance_type():
return get_instance_type_by_name(name)
def get_instance_type(instance_type_id):
def get_instance_type(instance_type_id, ctxt=None):
"""Retrieves single instance type by id."""
if instance_type_id is None:
return get_default_instance_type()
ctxt = context.get_admin_context()
if ctxt is None:
ctxt = context.get_admin_context()
return db.instance_type_get(ctxt, instance_type_id)
def get_instance_type_by_name(name):
def get_instance_type_by_name(name, ctxt=None):
"""Retrieves single instance type by name."""
if name is None:
return get_default_instance_type()
ctxt = context.get_admin_context()
if ctxt is None:
ctxt = context.get_admin_context()
return db.instance_type_get_by_name(ctxt, name)
# TODO(termie): flavor-specific code should probably be in the API that uses
# flavors.
def get_instance_type_by_flavor_id(flavorid, read_deleted="yes"):
def get_instance_type_by_flavor_id(flavorid, ctxt=None, read_deleted="yes"):
"""Retrieve instance type by flavorid.
:raises: FlavorNotFound
"""
ctxt = context.get_admin_context(read_deleted=read_deleted)
if ctxt is None:
ctxt = context.get_admin_context(read_deleted=read_deleted)
return db.instance_type_get_by_flavor_id(ctxt, flavorid)
def get_instance_type_access_by_flavor_id(flavorid, ctxt=None):
"""Retrieve instance type access list by flavor id"""
if ctxt is None:
ctxt = context.get_admin_context()
return db.instance_type_access_get_by_flavor_id(ctxt, flavorid)
def add_instance_type_access(flavorid, projectid, ctxt=None):
"""Add instance type access for project"""
if ctxt is None:
ctxt = context.get_admin_context()
return db.instance_type_access_add(ctxt, flavorid, projectid)
def remove_instance_type_access(flavorid, projectid, ctxt=None):
"""Remove instance type access for project"""
if ctxt is None:
ctxt = context.get_admin_context()
return db.instance_type_access_remove(ctxt, flavorid, projectid)

View File

@ -1445,6 +1445,21 @@ def instance_type_destroy(context, name):
return IMPL.instance_type_destroy(context, name)
def instance_type_access_get_by_flavor_id(context, flavor_id):
"""Get flavor access by flavor id."""
return IMPL.instance_type_access_get_by_flavor_id(context, flavor_id)
def instance_type_access_add(context, flavor_id, project_id):
"""Add flavor access for project."""
return IMPL.instance_type_access_add(context, flavor_id, project_id)
def instance_type_access_remove(context, flavor_id, project_id):
"""Remove flavor access for project."""
return IMPL.instance_type_access_remove(context, flavor_id, project_id)
####################

View File

@ -3872,6 +3872,19 @@ def instance_type_get_all(context, inactive=False, filters=None):
query = query.filter(
models.InstanceTypes.disabled == filters['disabled'])
if 'is_public' in filters and filters['is_public'] is not None:
the_filter = [models.InstanceTypes.is_public == filters['is_public']]
if filters['is_public'] and context.project_id is not None:
the_filter.extend([
models.InstanceTypes.projects.any(
project_id=context.project_id, deleted=False)
])
if len(the_filter) > 1:
query = query.filter(or_(*the_filter))
else:
query = query.filter(the_filter[0])
del filters['is_public']
inst_types = query.order_by("name").all()
return [_dict_with_extra_specs(i) for i in inst_types]
@ -3936,6 +3949,71 @@ def instance_type_destroy(context, name):
'updated_at': literal_column('updated_at')})
@require_context
def _instance_type_access_query(context, session=None):
return model_query(context, models.InstanceTypeProjects, session=session,
read_deleted="yes")
@require_admin_context
def instance_type_access_get_by_flavor_id(context, flavor_id):
"""Get flavor access list by flavor id"""
instance_type_ref = _instance_type_get_query(context).\
filter_by(flavorid=flavor_id).\
first()
return [r for r in instance_type_ref.projects]
@require_admin_context
def instance_type_access_add(context, flavor_id, project_id):
"""Add given tenant to the flavor access list"""
session = get_session()
with session.begin():
instance_type_ref = instance_type_get_by_flavor_id(context, flavor_id,
session=session)
instance_type_id = instance_type_ref['id']
access_ref = _instance_type_access_query(context, session=session).\
filter_by(instance_type_id=instance_type_id).\
filter_by(project_id=project_id).first()
if not access_ref:
access_ref = models.InstanceTypeProjects()
access_ref.instance_type_id = instance_type_id
access_ref.project_id = project_id
access_ref.save(session=session)
elif access_ref.deleted:
access_ref.update({'deleted': False,
'deleted_at': None})
access_ref.save(session=session)
else:
raise exception.FlavorAccessExists(flavor_id=flavor_id,
project_id=project_id)
return access_ref
@require_admin_context
def instance_type_access_remove(context, flavor_id, project_id):
"""Remove given tenant from the flavor access list"""
session = get_session()
with session.begin():
instance_type_ref = instance_type_get_by_flavor_id(context, flavor_id,
session=session)
instance_type_id = instance_type_ref['id']
access_ref = _instance_type_access_query(context, session=session).\
filter_by(instance_type_id=instance_type_id).\
filter_by(project_id=project_id).first()
if access_ref:
access_ref.update({'deleted': True,
'deleted_at': timeutils.utcnow(),
'updated_at': literal_column('updated_at')})
else:
raise exception.FlavorAccessNotFound(flavor_id=flavor_id,
project_id=project_id)
########################
# User-provided metadata

View File

@ -0,0 +1,67 @@
# Copyright 2012 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.
from sqlalchemy import Boolean, Column, DateTime, String, ForeignKey, Integer
from sqlalchemy import MetaData, String, Table
from nova.openstack.common import log as logging
LOG = logging.getLogger(__name__)
def upgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
instance_types = Table('instance_types', meta, autoload=True)
is_public = Column('is_public', Boolean)
instance_types.create_column(is_public)
instance_types.update().values(is_public=True).execute()
# New table.
instance_type_projects = Table('instance_type_projects', meta,
Column('created_at', DateTime(timezone=False)),
Column('updated_at', DateTime(timezone=False)),
Column('deleted_at', DateTime(timezone=False)),
Column('deleted', Boolean(), default=False),
Column('id', Integer, primary_key=True, nullable=False),
Column('instance_type_id',
Integer,
ForeignKey('instance_types.id'),
nullable=False),
Column('project_id', String(length=255)),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
try:
instance_type_projects.create()
except Exception:
LOG.error(_("Table |%s| not created!"), repr(instance_type_projects))
raise
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
instance_types = Table('instance_types', meta, autoload=True)
is_public = Column('is_public', Boolean)
instance_types.drop_column(is_public)
instance_type_projects = Table(
'instance_type_projects', meta, autoload=True)
instance_type_projects.drop()

View File

@ -336,6 +336,7 @@ class InstanceTypes(BASE, NovaBase):
rxtx_factor = Column(Float, nullable=False, default=1)
vcpu_weight = Column(Integer, nullable=True)
disabled = Column(Boolean, default=False)
is_public = Column(Boolean, default=True)
instances = relationship(Instance,
backref=backref('instance_type', uselist=False),
@ -821,6 +822,21 @@ class InstanceSystemMetadata(BASE, NovaBase):
primaryjoin=primary_join)
class InstanceTypeProjects(BASE, NovaBase):
"""Represent projects associated instance_types"""
__tablename__ = "instance_type_projects"
id = Column(Integer, primary_key=True)
instance_type_id = Column(Integer, ForeignKey('instance_types.id'),
nullable=False)
project_id = Column(String(255))
instance_type = relationship(InstanceTypes, backref="projects",
foreign_keys=instance_type_id,
primaryjoin='and_('
'InstanceTypeProjects.instance_type_id == InstanceTypes.id,'
'InstanceTypeProjects.deleted == False)')
class InstanceTypeExtraSpecs(BASE, NovaBase):
"""Represents additional specs as key/value pairs for an instance_type"""
__tablename__ = 'instance_type_extra_specs'

View File

@ -776,6 +776,11 @@ class FlavorNotFound(NotFound):
message = _("Flavor %(flavor_id)s could not be found.")
class FlavorAccessNotFound(NotFound):
message = _("Flavor access not found for %(flavor_id) / "
"%(project_id) combination.")
class SchedulerHostFilterNotFound(NotFound):
message = _("Scheduler Host Filter %(filter_name)s could not be found.")
@ -849,6 +854,11 @@ class InstanceTypeExists(Duplicate):
message = _("Instance Type %(name)s already exists.")
class FlavorAccessExists(Duplicate):
message = _("Flavor access alreay exists for flavor %(flavor_id)s "
"and project %(project_id)s combination.")
class VolumeTypeExists(Duplicate):
message = _("Volume Type %(name)s already exists.")

View File

@ -0,0 +1,299 @@
# Copyright 2012 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 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
from nova.compute import instance_types
from nova import context
from nova import exception
from nova import test
from nova.tests.api.openstack import fakes
def generate_instance_type(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_instance_type(0, True),
'1': generate_instance_type(1, True),
'2': generate_instance_type(2, False),
'3': generate_instance_type(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_instance_type_access_by_flavor_id(flavorid):
res = []
for access in ACCESS_LIST:
if access['flavor_id'] == flavorid:
res.append(access)
return res
def fake_get_instance_type_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_types(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()}
class FlavorAccessTest(test.TestCase):
def setUp(self):
super(FlavorAccessTest, self).setUp()
self.flavor_controller = flavors.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(instance_types, 'get_instance_type_by_flavor_id',
fake_get_instance_type_by_flavor_id)
self.stubs.Set(instance_types, 'get_all_types', fake_get_all_types)
self.stubs.Set(instance_types, 'get_instance_type_access_by_flavor_id',
fake_get_instance_type_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_add_tenant_access(self):
def stub_add_instance_type_access(flavorid, projectid, ctxt=None):
self.assertEqual('3', flavorid, "flavorid")
self.assertEqual("proj2", projectid, "projectid")
self.stubs.Set(instance_types, 'add_instance_type_access',
stub_add_instance_type_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_instance_type_access(flavorid, projectid, ctxt=None):
raise exception.FlavorAccessExists()
self.stubs.Set(instance_types, 'add_instance_type_access',
stub_add_instance_type_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_instance_type_access(flavorid, projectid, ctxt=None):
self.assertEqual('3', flavorid, "flavorid")
self.assertEqual("proj2", projectid, "projectid")
expected = {'flavor_access': [
{'flavor_id': '3', 'tenant_id': 'proj3'}]}
self.stubs.Set(instance_types, 'remove_instance_type_access',
stub_remove_instance_type_access)
body = {'removeTenantAccess': {'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_remove_tenant_access_with_bad_access(self):
def stub_remove_instance_type_access(flavorid, projectid, ctxt=None):
raise exception.FlavorAccessNotFound()
self.stubs.Set(instance_types, 'remove_instance_type_access',
stub_remove_instance_type_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_xml_declaration(self):
access_list = [{'flavor_id': '2', 'tenant_id': 'proj2'}]
serializer = flavor_access.FlavorAccessTemplate()
output = serializer.serialize(access_list)
has_dec = output.startswith("<?xml version='1.0' encoding='UTF-8'?>")
self.assertTrue(has_dec)
def test_serializer_empty(self):
access_list = []
serializer = flavor_access.FlavorAccessTemplate()
text = serializer.serialize(access_list)
tree = etree.fromstring(text)
self.assertEqual(len(tree), 0)
def test_serializer(self):
access_list = [{'flavor_id': '2', 'tenant_id': 'proj2'},
{'flavor_id': '2', 'tenant_id': 'proj3'}]
serializer = flavor_access.FlavorAccessTemplate()
text = serializer.serialize(access_list)
tree = etree.fromstring(text)
self.assertEqual('flavor_access', tree.tag)
self.assertEqual(len(access_list), len(tree))
for i in range(len(access_list)):
self.assertEqual('access', tree[i].tag)
self.assertEqual(access_list[i]['flavor_id'],
tree[i].get('flavor_id'))
self.assertEqual(access_list[i]['tenant_id'],
tree[i].get('tenant_id'))

View File

@ -46,7 +46,8 @@ def fake_get_instance_type_by_flavor_id(flavorid):
'extra_specs': {},
'deleted_at': None,
'vcpu_weight': None,
'id': 7
'id': 7,
'is_public': True
}
@ -55,7 +56,7 @@ def fake_destroy(flavorname):
def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb,
flavorid, swap, rxtx_factor):
flavorid, swap, rxtx_factor, is_public):
newflavor = fake_get_instance_type_by_flavor_id(flavorid)
newflavor["name"] = name
@ -65,6 +66,7 @@ def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb,
newflavor["ephemeral_gb"] = int(ephemeral_gb)
newflavor["swap"] = swap
newflavor["rxtx_factor"] = float(rxtx_factor)
newflavor["is_public"] = bool(is_public)
return newflavor
@ -100,6 +102,7 @@ class FlavorManageTest(test.TestCase):
"id": 1234,
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
@ -124,11 +127,12 @@ class FlavorManageTest(test.TestCase):
"id": 1235,
"swap": 512,
"rxtx_factor": 1,
"os-flavor-access:is_public": True,
}
}
def fake_create(name, memory_mb, vcpus, root_gb, ephemeral_gb,
flavorid, swap, rxtx_factor):
flavorid, swap, rxtx_factor, is_public):
raise exception.InstanceTypeExists()
self.stubs.Set(instance_types, "create", fake_create)

View File

@ -39,7 +39,8 @@ def fake_get_instance_type_by_flavor_id(flavorid):
'rxtx_factor': 1.0,
'extra_specs': {},
'deleted_at': None,
'vcpu_weight': None
'vcpu_weight': None,
'is_public': True
}
@ -72,6 +73,7 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
}
}
@ -93,6 +95,7 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
},
{
'id': '2',
@ -103,6 +106,7 @@ class FlavorextradataTest(test.TestCase):
'OS-FLV-EXT-DATA:ephemeral': 1,
'swap': 512,
'rxtx_factor': 1,
'os-flavor-access:is_public': True,
},
]

View File

@ -166,6 +166,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"DiskConfig",
"ExtendedStatus",
"ExtendedServerAttributes",
"FlavorAccess",
"FlavorExtraSpecs",
"FlavorExtraData",
"FlavorManage",

View File

@ -92,6 +92,7 @@
"compute_extension:disk_config": [],
"compute_extension:extended_server_attributes": [],
"compute_extension:extended_status": [],
"compute_extension:flavor_access": [],
"compute_extension:flavorextradata": [],
"compute_extension:flavorextraspecs": [],
"compute_extension:flavormanage": [],

View File

@ -329,8 +329,17 @@ class GenericUtilsTestCase(test.TestCase):
self.assertTrue(utils.bool_from_str('true'))
self.assertTrue(utils.bool_from_str('True'))
self.assertTrue(utils.bool_from_str('tRuE'))
self.assertTrue(utils.bool_from_str('yes'))
self.assertTrue(utils.bool_from_str('Yes'))
self.assertTrue(utils.bool_from_str('YeS'))
self.assertTrue(utils.bool_from_str('y'))
self.assertTrue(utils.bool_from_str('Y'))
self.assertFalse(utils.bool_from_str('False'))
self.assertFalse(utils.bool_from_str('false'))
self.assertFalse(utils.bool_from_str('no'))
self.assertFalse(utils.bool_from_str('No'))
self.assertFalse(utils.bool_from_str('n'))
self.assertFalse(utils.bool_from_str('N'))
self.assertFalse(utils.bool_from_str('0'))
self.assertFalse(utils.bool_from_str(None))
self.assertFalse(utils.bool_from_str('junk'))

View File

@ -918,7 +918,9 @@ def bool_from_str(val):
try:
return True if int(val) else False
except ValueError:
return val.lower() == 'true'
return val.lower() == 'true' or \
val.lower() == 'yes' or \
val.lower() == 'y'
def is_valid_ipv4(address):