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:
parent
5e012d8d45
commit
34c012c709
|
@ -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
|
||||
|
|
|
@ -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"]],
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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]
|
|
@ -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']]
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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'
|
||||
|
|
|
@ -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.")
|
||||
|
||||
|
|
|
@ -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'))
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
@ -166,6 +166,7 @@ class ExtensionControllerTest(ExtensionTestCase):
|
|||
"DiskConfig",
|
||||
"ExtendedStatus",
|
||||
"ExtendedServerAttributes",
|
||||
"FlavorAccess",
|
||||
"FlavorExtraSpecs",
|
||||
"FlavorExtraData",
|
||||
"FlavorManage",
|
||||
|
|
|
@ -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": [],
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue