summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorunicell <unicell@gmail.com>2012-08-13 20:19:54 +0800
committerunicell <unicell@gmail.com>2012-08-27 23:45:05 +0800
commit34c012c709cc5ae577330c7d67ba060293158210 (patch)
treee04765cbb3e09e9047c51aa9e03d4b1db15ac80c
parent5e012d8d45935b68a5ce5d50ed043d4bb8066cf8 (diff)
downloadnova-34c012c709cc5ae577330c7d67ba060293158210.tar.gz
nova-34c012c709cc5ae577330c7d67ba060293158210.tar.xz
nova-34c012c709cc5ae577330c7d67ba060293158210.zip
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
-rwxr-xr-xbin/nova-manage12
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/compute/__init__.py3
-rw-r--r--nova/api/openstack/compute/contrib/flavor_access.py240
-rw-r--r--nova/api/openstack/compute/contrib/flavorextradata.py6
-rw-r--r--nova/api/openstack/compute/contrib/flavormanage.py3
-rw-r--r--nova/api/openstack/compute/flavors.py28
-rw-r--r--nova/compute/instance_types.py54
-rw-r--r--nova/db/api.py15
-rw-r--r--nova/db/sqlalchemy/api.py78
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/132_add_instance_type_projects.py67
-rw-r--r--nova/db/sqlalchemy/models.py16
-rw-r--r--nova/exception.py10
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_flavor_access.py299
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_flavor_manage.py10
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_flavorextradata.py6
-rw-r--r--nova/tests/api/openstack/compute/test_extensions.py1
-rw-r--r--nova/tests/policy.json1
-rw-r--r--nova/tests/test_utils.py9
-rw-r--r--nova/utils.py4
20 files changed, 838 insertions, 25 deletions
diff --git a/bin/nova-manage b/bin/nova-manage
index 264c13426..7b3769629 100755
--- a/bin/nova-manage
+++ b/bin/nova-manage
@@ -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
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 8bc2f6f5f..2a722b9a1 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -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"]],
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'])
diff --git a/nova/compute/instance_types.py b/nova/compute/instance_types.py
index dfcf235dd..e31933240 100644
--- a/nova/compute/instance_types.py
+++ b/nova/compute/instance_types.py
@@ -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)
diff --git a/nova/db/api.py b/nova/db/api.py
index d4be854b3..f718f0047 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -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)
+
+
####################
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index da1376c30..3f6c504ab 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -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
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/132_add_instance_type_projects.py b/nova/db/sqlalchemy/migrate_repo/versions/132_add_instance_type_projects.py
new file mode 100644
index 000000000..312ebbfc1
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/132_add_instance_type_projects.py
@@ -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()
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index a37b1c215..65d5eb5e0 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -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'
diff --git a/nova/exception.py b/nova/exception.py
index 83c3f9c03..95a43a05f 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -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.")
diff --git a/nova/tests/api/openstack/compute/contrib/test_flavor_access.py b/nova/tests/api/openstack/compute/contrib/test_flavor_access.py
new file mode 100644
index 000000000..0bf1f1b66
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_flavor_access.py
@@ -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'))
diff --git a/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py b/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py
index 140efc325..84ff8a1ea 100644
--- a/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py
+++ b/nova/tests/api/openstack/compute/contrib/test_flavor_manage.py
@@ -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)
diff --git a/nova/tests/api/openstack/compute/contrib/test_flavorextradata.py b/nova/tests/api/openstack/compute/contrib/test_flavorextradata.py
index 4f24b08ad..3c4a84e2a 100644
--- a/nova/tests/api/openstack/compute/contrib/test_flavorextradata.py
+++ b/nova/tests/api/openstack/compute/contrib/test_flavorextradata.py
@@ -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,
},
]
diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py
index 4110d3ea3..200f56a56 100644
--- a/nova/tests/api/openstack/compute/test_extensions.py
+++ b/nova/tests/api/openstack/compute/test_extensions.py
@@ -166,6 +166,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"DiskConfig",
"ExtendedStatus",
"ExtendedServerAttributes",
+ "FlavorAccess",
"FlavorExtraSpecs",
"FlavorExtraData",
"FlavorManage",
diff --git a/nova/tests/policy.json b/nova/tests/policy.json
index 61ec0152a..08577ed04 100644
--- a/nova/tests/policy.json
+++ b/nova/tests/policy.json
@@ -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": [],
diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py
index b4e1ebb3d..c7a323875 100644
--- a/nova/tests/test_utils.py
+++ b/nova/tests/test_utils.py
@@ -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'))
diff --git a/nova/utils.py b/nova/utils.py
index c1a12cade..7eb7aa662 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -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):