Volume type access extension
This extension adds the ability to manage volume type access: * Volume types are public by default * Private volume types can be created by setting the is_public boolean field to False at creation time. * Access to a private volume type can be controlled by adding or removing a project from it. * Private volume types without projects are only visible by users with the admin role/context. Implementation details and unit tests were mostly adapted from Nova flavor access extension. DocImpact: New volume type access extension Implements: blueprint private-volume-types Change-Id: I8faf1d8097bf8412d4e169ec3503821351795561
This commit is contained in:
parent
7e3ddf8d0d
commit
6c0f50b1ec
|
@ -52,13 +52,15 @@ class VolumeTypesManageController(wsgi.Controller):
|
|||
vol_type = body['volume_type']
|
||||
name = vol_type.get('name', None)
|
||||
specs = vol_type.get('extra_specs', {})
|
||||
is_public = vol_type.get('os-volume-type-access:is_public', True)
|
||||
|
||||
if name is None or name == "":
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
|
||||
try:
|
||||
volume_types.create(context, name, specs)
|
||||
volume_types.create(context, name, specs, is_public)
|
||||
vol_type = volume_types.get_volume_type_by_name(context, name)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
notifier_info = dict(volume_types=vol_type)
|
||||
rpc.get_notifier('volumeType').info(context, 'volume_type.create',
|
||||
notifier_info)
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
#
|
||||
# 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 volume type access extension."""
|
||||
|
||||
import six
|
||||
import webob
|
||||
|
||||
from cinder.api import extensions
|
||||
from cinder.api.openstack import wsgi
|
||||
from cinder.api import xmlutil
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder.openstack.common import uuidutils
|
||||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
soft_authorize = extensions.soft_extension_authorizer('volume',
|
||||
'volume_type_access')
|
||||
authorize = extensions.extension_authorizer('volume', 'volume_type_access')
|
||||
|
||||
|
||||
def make_volume_type(elem):
|
||||
elem.set('{%s}is_public' % Volume_type_access.namespace,
|
||||
'%s:is_public' % Volume_type_access.alias)
|
||||
|
||||
|
||||
def make_volume_type_access(elem):
|
||||
elem.set('volume_type_id')
|
||||
elem.set('project_id')
|
||||
|
||||
|
||||
class VolumeTypeTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('volume_type', selector='volume_type')
|
||||
make_volume_type(root)
|
||||
alias = Volume_type_access.alias
|
||||
namespace = Volume_type_access.namespace
|
||||
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class VolumeTypesTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('volume_types')
|
||||
elem = xmlutil.SubTemplateElement(
|
||||
root, 'volume_type', selector='volume_types')
|
||||
make_volume_type(elem)
|
||||
alias = Volume_type_access.alias
|
||||
namespace = Volume_type_access.namespace
|
||||
return xmlutil.SlaveTemplate(root, 1, nsmap={alias: namespace})
|
||||
|
||||
|
||||
class VolumeTypeAccessTemplate(xmlutil.TemplateBuilder):
|
||||
def construct(self):
|
||||
root = xmlutil.TemplateElement('volume_type_access')
|
||||
elem = xmlutil.SubTemplateElement(root, 'access',
|
||||
selector='volume_type_access')
|
||||
make_volume_type_access(elem)
|
||||
return xmlutil.MasterTemplate(root, 1)
|
||||
|
||||
|
||||
def _marshall_volume_type_access(vol_type):
|
||||
rval = []
|
||||
for project_id in vol_type['projects']:
|
||||
rval.append({'volume_type_id': vol_type['id'],
|
||||
'project_id': project_id})
|
||||
|
||||
return {'volume_type_access': rval}
|
||||
|
||||
|
||||
class VolumeTypeAccessController(object):
|
||||
"""The volume type access API controller for the OpenStack API."""
|
||||
|
||||
def __init__(self):
|
||||
super(VolumeTypeAccessController, self).__init__()
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeAccessTemplate)
|
||||
def index(self, req, type_id):
|
||||
context = req.environ['cinder.context']
|
||||
authorize(context)
|
||||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(
|
||||
context, type_id, expected_fields=['projects'])
|
||||
except exception.VolumeTypeNotFound:
|
||||
explanation = _("Volume type not found.")
|
||||
raise webob.exc.HTTPNotFound(explanation=explanation)
|
||||
|
||||
if vol_type['is_public']:
|
||||
expl = _("Access list not available for public volume types.")
|
||||
raise webob.exc.HTTPNotFound(explanation=expl)
|
||||
|
||||
return _marshall_volume_type_access(vol_type)
|
||||
|
||||
|
||||
class VolumeTypeActionController(wsgi.Controller):
|
||||
"""The volume type access API controller for the OpenStack API."""
|
||||
|
||||
def _check_body(self, body, action_name):
|
||||
if not self.is_valid_body(body, action_name):
|
||||
raise webob.exc.HTTPBadRequest()
|
||||
access = body[action_name]
|
||||
project = access.get('project')
|
||||
if not uuidutils.is_uuid_like(project):
|
||||
msg = _("Bad project format: "
|
||||
"project is not in proper format (%s)") % project
|
||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _extend_vol_type(self, vol_type_rval, vol_type_ref):
|
||||
key = "%s:is_public" % (Volume_type_access.alias)
|
||||
vol_type_rval[key] = vol_type_ref['is_public']
|
||||
|
||||
@wsgi.extends
|
||||
def show(self, req, resp_obj, id):
|
||||
context = req.environ['cinder.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=VolumeTypeTemplate())
|
||||
vol_type = req.cached_resource_by_id(id, name='types')
|
||||
self._extend_vol_type(resp_obj.obj['volume_type'], vol_type)
|
||||
|
||||
@wsgi.extends
|
||||
def index(self, req, resp_obj):
|
||||
context = req.environ['cinder.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=VolumeTypesTemplate())
|
||||
for vol_type_rval in list(resp_obj.obj['volume_types']):
|
||||
type_id = vol_type_rval['id']
|
||||
vol_type = req.cached_resource_by_id(type_id, name='types')
|
||||
self._extend_vol_type(vol_type_rval, vol_type)
|
||||
|
||||
@wsgi.extends
|
||||
def detail(self, req, resp_obj):
|
||||
context = req.environ['cinder.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=VolumeTypesTemplate())
|
||||
for vol_type_rval in list(resp_obj.obj['volume_types']):
|
||||
type_id = vol_type_rval['id']
|
||||
vol_type = req.cached_resource_by_id(type_id, name='types')
|
||||
self._extend_vol_type(vol_type_rval, vol_type)
|
||||
|
||||
@wsgi.extends(action='create')
|
||||
def create(self, req, body, resp_obj):
|
||||
context = req.environ['cinder.context']
|
||||
if soft_authorize(context):
|
||||
# Attach our slave template to the response object
|
||||
resp_obj.attach(xml=VolumeTypeTemplate())
|
||||
type_id = resp_obj.obj['volume_type']['id']
|
||||
vol_type = req.cached_resource_by_id(type_id, name='types')
|
||||
self._extend_vol_type(resp_obj.obj['volume_type'], vol_type)
|
||||
|
||||
@wsgi.action('addProjectAccess')
|
||||
def _addProjectAccess(self, req, id, body):
|
||||
context = req.environ['cinder.context']
|
||||
authorize(context, action="addProjectAccess")
|
||||
self._check_body(body, 'addProjectAccess')
|
||||
project = body['addProjectAccess']['project']
|
||||
|
||||
try:
|
||||
volume_types.add_volume_type_access(context, id, project)
|
||||
except exception.VolumeTypeAccessExists as err:
|
||||
raise webob.exc.HTTPConflict(explanation=six.text_type(err))
|
||||
except exception.VolumeTypeNotFound as err:
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
@wsgi.action('removeProjectAccess')
|
||||
def _removeProjectAccess(self, req, id, body):
|
||||
context = req.environ['cinder.context']
|
||||
authorize(context, action="removeProjectAccess")
|
||||
self._check_body(body, 'removeProjectAccess')
|
||||
project = body['removeProjectAccess']['project']
|
||||
|
||||
try:
|
||||
volume_types.remove_volume_type_access(context, id, project)
|
||||
except (exception.VolumeTypeNotFound,
|
||||
exception.VolumeTypeAccessNotFound) as err:
|
||||
raise webob.exc.HTTPNotFound(explanation=six.text_type(err))
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
|
||||
class Volume_type_access(extensions.ExtensionDescriptor):
|
||||
"""Volume type access support."""
|
||||
|
||||
name = "VolumeTypeAccess"
|
||||
alias = "os-volume-type-access"
|
||||
namespace = ("http://docs.openstack.org/volume/"
|
||||
"ext/os-volume-type-access/api/v1")
|
||||
updated = "2014-06-26T00:00:00Z"
|
||||
|
||||
def get_resources(self):
|
||||
resources = []
|
||||
res = extensions.ResourceExtension(
|
||||
Volume_type_access.alias,
|
||||
VolumeTypeAccessController(),
|
||||
parent=dict(member_name='type', collection_name='types'))
|
||||
resources.append(res)
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = VolumeTypeActionController()
|
||||
extension = extensions.ControllerExtension(self, 'types', controller)
|
||||
return [extension]
|
|
@ -376,12 +376,15 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
|
|||
|
||||
|
||||
def extension_authorizer(api_name, extension_name):
|
||||
def authorize(context, target=None):
|
||||
def authorize(context, target=None, action=None):
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
action = '%s_extension:%s' % (api_name, extension_name)
|
||||
cinder.policy.enforce(context, action, target)
|
||||
if action is None:
|
||||
act = '%s_extension:%s' % (api_name, extension_name)
|
||||
else:
|
||||
act = '%s_extension:%s:%s' % (api_name, extension_name, action)
|
||||
cinder.policy.enforce(context, act, target)
|
||||
return authorize
|
||||
|
||||
|
||||
|
|
|
@ -57,6 +57,7 @@ class VolumeTypesController(wsgi.Controller):
|
|||
"""Returns the list of volume types."""
|
||||
context = req.environ['cinder.context']
|
||||
vol_types = volume_types.get_all_types(context).values()
|
||||
req.cache_resource(vol_types, name='types')
|
||||
return self._view_builder.index(req, vol_types)
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeTemplate)
|
||||
|
@ -66,6 +67,7 @@ class VolumeTypesController(wsgi.Controller):
|
|||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
except exception.NotFound:
|
||||
raise exc.HTTPNotFound()
|
||||
|
||||
|
|
|
@ -54,7 +54,8 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||
|
||||
self.resources['types'] = types.create_resource()
|
||||
mapper.resource("type", "types",
|
||||
controller=self.resources['types'])
|
||||
controller=self.resources['types'],
|
||||
member={'action': 'POST'})
|
||||
|
||||
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
|
||||
mapper.resource("snapshot", "snapshots",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
"""The volume type & volume types extra specs extension."""
|
||||
|
||||
from oslo.utils import strutils
|
||||
from webob import exc
|
||||
|
||||
from cinder.api.openstack import wsgi
|
||||
|
@ -22,6 +23,7 @@ from cinder.api.views import types as views_types
|
|||
from cinder.api import xmlutil
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
from cinder import utils
|
||||
from cinder.volume import volume_types
|
||||
|
||||
|
||||
|
@ -56,9 +58,9 @@ class VolumeTypesController(wsgi.Controller):
|
|||
@wsgi.serializers(xml=VolumeTypesTemplate)
|
||||
def index(self, req):
|
||||
"""Returns the list of volume types."""
|
||||
context = req.environ['cinder.context']
|
||||
vol_types = volume_types.get_all_types(context).values()
|
||||
return self._view_builder.index(req, vol_types)
|
||||
limited_types = self._get_volume_types(req)
|
||||
req.cache_resource(limited_types, name='types')
|
||||
return self._view_builder.index(req, limited_types)
|
||||
|
||||
@wsgi.serializers(xml=VolumeTypeTemplate)
|
||||
def show(self, req, id):
|
||||
|
@ -67,12 +69,47 @@ class VolumeTypesController(wsgi.Controller):
|
|||
|
||||
try:
|
||||
vol_type = volume_types.get_volume_type(context, id)
|
||||
req.cache_resource(vol_type, name='types')
|
||||
except exception.NotFound:
|
||||
msg = _("Volume type not found")
|
||||
raise exc.HTTPNotFound(explanation=msg)
|
||||
|
||||
return self._view_builder.show(req, vol_type)
|
||||
|
||||
def _parse_is_public(self, is_public):
|
||||
"""Parse is_public into something usable.
|
||||
|
||||
* True: List public volume types only
|
||||
* False: List private volume types only
|
||||
* None: List both public and private volume types
|
||||
"""
|
||||
|
||||
if is_public is None:
|
||||
# preserve default value of showing only public types
|
||||
return True
|
||||
elif utils.is_none_string(is_public):
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return strutils.bool_from_string(is_public, strict=True)
|
||||
except ValueError:
|
||||
msg = _('Invalid is_public filter [%s]') % is_public
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
|
||||
def _get_volume_types(self, req):
|
||||
"""Helper function that returns a list of type dicts."""
|
||||
filters = {}
|
||||
context = req.environ['cinder.context']
|
||||
if context.is_admin:
|
||||
# Only admin has query access to all volume types
|
||||
filters['is_public'] = self._parse_is_public(
|
||||
req.params.get('is_public', None))
|
||||
else:
|
||||
filters['is_public'] = True
|
||||
limited_types = volume_types.get_all_types(
|
||||
context, search_opts=filters).values()
|
||||
return limited_types
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(VolumeTypesController())
|
||||
|
|
|
@ -369,19 +369,41 @@ def volume_admin_metadata_update(context, volume_id, metadata, delete):
|
|||
##################
|
||||
|
||||
|
||||
def volume_type_create(context, values):
|
||||
def volume_type_create(context, values, projects=None):
|
||||
"""Create a new volume type."""
|
||||
return IMPL.volume_type_create(context, values)
|
||||
return IMPL.volume_type_create(context, values, projects)
|
||||
|
||||
|
||||
def volume_type_get_all(context, inactive=False):
|
||||
"""Get all volume types."""
|
||||
return IMPL.volume_type_get_all(context, inactive)
|
||||
def volume_type_get_all(context, inactive=False, filters=None):
|
||||
"""Get all volume types.
|
||||
|
||||
:param context: context to query under
|
||||
:param inactive: Include inactive volume types to the result set
|
||||
:param filters: Filters for the query in the form of key/value.
|
||||
|
||||
:is_public: Filter volume types based on visibility:
|
||||
|
||||
* **True**: List public volume types only
|
||||
* **False**: List private volume types only
|
||||
* **None**: List both public and private volume types
|
||||
|
||||
:returns: list of matching volume types
|
||||
"""
|
||||
|
||||
return IMPL.volume_type_get_all(context, inactive, filters)
|
||||
|
||||
|
||||
def volume_type_get(context, id, inactive=False):
|
||||
"""Get volume type by id."""
|
||||
return IMPL.volume_type_get(context, id, inactive)
|
||||
def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||
"""Get volume type by id.
|
||||
|
||||
:param context: context to query under
|
||||
:param id: Volume type id to get.
|
||||
:param inactive: Consider inactive volume types when searching
|
||||
:param expected_fields: Return those additional fields.
|
||||
Supported fields are: projects.
|
||||
:returns: volume type
|
||||
"""
|
||||
return IMPL.volume_type_get(context, id, inactive, expected_fields)
|
||||
|
||||
|
||||
def volume_type_get_by_name(context, name):
|
||||
|
@ -435,6 +457,21 @@ def volume_get_active_by_window(context, begin, end=None, project_id=None):
|
|||
return IMPL.volume_get_active_by_window(context, begin, end, project_id)
|
||||
|
||||
|
||||
def volume_type_access_get_all(context, type_id):
|
||||
"""Get all volume type access of a volume type."""
|
||||
return IMPL.volume_type_access_get_all(context, type_id)
|
||||
|
||||
|
||||
def volume_type_access_add(context, type_id, project_id):
|
||||
"""Add volume type access for project."""
|
||||
return IMPL.volume_type_access_add(context, type_id, project_id)
|
||||
|
||||
|
||||
def volume_type_access_remove(context, type_id, project_id):
|
||||
"""Remove volume type access for project."""
|
||||
return IMPL.volume_type_access_remove(context, type_id, project_id)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ from sqlalchemy import or_
|
|||
from sqlalchemy.orm import joinedload, joinedload_all
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
from sqlalchemy.sql.expression import literal_column
|
||||
from sqlalchemy.sql.expression import true
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from cinder.common import sqlalchemyutils
|
||||
|
@ -1871,8 +1872,8 @@ def snapshot_metadata_update(context, snapshot_id, metadata, delete):
|
|||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_create(context, values):
|
||||
"""Create a new instance type.
|
||||
def volume_type_create(context, values, projects=None):
|
||||
"""Create a new volume type.
|
||||
|
||||
In order to pass in extra specs, the values dict should contain a
|
||||
'extra_specs' key/value pair:
|
||||
|
@ -1881,6 +1882,8 @@ def volume_type_create(context, values):
|
|||
if not values.get('id'):
|
||||
values['id'] = str(uuid.uuid4())
|
||||
|
||||
projects = projects or []
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
|
@ -1901,20 +1904,59 @@ def volume_type_create(context, values):
|
|||
session.add(volume_type_ref)
|
||||
except Exception as e:
|
||||
raise db_exc.DBError(e)
|
||||
for project in set(projects):
|
||||
access_ref = models.VolumeTypeProjects()
|
||||
access_ref.update({"volume_type_id": volume_type_ref.id,
|
||||
"project_id": project})
|
||||
access_ref.save(session=session)
|
||||
return volume_type_ref
|
||||
|
||||
|
||||
def _volume_type_get_query(context, session=None, read_deleted=None,
|
||||
expected_fields=None):
|
||||
expected_fields = expected_fields or []
|
||||
query = model_query(context,
|
||||
models.VolumeTypes,
|
||||
session=session,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs'))
|
||||
|
||||
if 'projects' in expected_fields:
|
||||
query = query.options(joinedload('projects'))
|
||||
|
||||
if not context.is_admin:
|
||||
the_filter = [models.VolumeTypes.is_public == true()]
|
||||
projects_attr = getattr(models.VolumeTypes, 'projects')
|
||||
the_filter.extend([
|
||||
projects_attr.any(project_id=context.project_id)
|
||||
])
|
||||
query = query.filter(or_(*the_filter))
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get_all(context, inactive=False, filters=None):
|
||||
"""Returns a dict describing all volume_types with name as key."""
|
||||
filters = filters or {}
|
||||
|
||||
read_deleted = "yes" if inactive else "no"
|
||||
rows = model_query(context, models.VolumeTypes,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs')).\
|
||||
order_by("name").\
|
||||
all()
|
||||
|
||||
query = _volume_type_get_query(context, read_deleted=read_deleted)
|
||||
|
||||
if 'is_public' in filters and filters['is_public'] is not None:
|
||||
the_filter = [models.VolumeTypes.is_public == filters['is_public']]
|
||||
if filters['is_public'] and context.project_id is not None:
|
||||
projects_attr = getattr(models.VolumeTypes, 'projects')
|
||||
the_filter.extend([
|
||||
projects_attr.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])
|
||||
|
||||
rows = query.order_by("name").all()
|
||||
|
||||
result = {}
|
||||
for row in rows:
|
||||
|
@ -1923,28 +1965,50 @@ def volume_type_get_all(context, inactive=False, filters=None):
|
|||
return result
|
||||
|
||||
|
||||
def _volume_type_get_id_from_volume_type_query(context, id, session=None):
|
||||
return model_query(
|
||||
context, models.VolumeTypes.id, read_deleted="no",
|
||||
session=session, base_model=models.VolumeTypes).\
|
||||
filter_by(id=id)
|
||||
|
||||
|
||||
def _volume_type_get_id_from_volume_type(context, id, session=None):
|
||||
result = _volume_type_get_id_from_volume_type_query(
|
||||
context, id, session=session).first()
|
||||
if not result:
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
return result[0]
|
||||
|
||||
|
||||
@require_context
|
||||
def _volume_type_get(context, id, session=None, inactive=False):
|
||||
def _volume_type_get(context, id, session=None, inactive=False,
|
||||
expected_fields=None):
|
||||
expected_fields = expected_fields or []
|
||||
read_deleted = "yes" if inactive else "no"
|
||||
result = model_query(context,
|
||||
models.VolumeTypes,
|
||||
session=session,
|
||||
read_deleted=read_deleted).\
|
||||
options(joinedload('extra_specs')).\
|
||||
result = _volume_type_get_query(
|
||||
context, session, read_deleted, expected_fields).\
|
||||
filter_by(id=id).\
|
||||
first()
|
||||
|
||||
if not result:
|
||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
||||
|
||||
return _dict_with_extra_specs(result)
|
||||
vtype = _dict_with_extra_specs(result)
|
||||
|
||||
if 'projects' in expected_fields:
|
||||
vtype['projects'] = [p['project_id'] for p in result['projects']]
|
||||
|
||||
return vtype
|
||||
|
||||
|
||||
@require_context
|
||||
def volume_type_get(context, id, inactive=False):
|
||||
def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||
"""Return a dict describing specific volume_type."""
|
||||
|
||||
return _volume_type_get(context, id, None, inactive)
|
||||
return _volume_type_get(context, id,
|
||||
session=None,
|
||||
inactive=inactive,
|
||||
expected_fields=expected_fields)
|
||||
|
||||
|
||||
@require_context
|
||||
|
@ -1956,8 +2020,8 @@ def _volume_type_get_by_name(context, name, session=None):
|
|||
|
||||
if not result:
|
||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||
else:
|
||||
return _dict_with_extra_specs(result)
|
||||
|
||||
return _dict_with_extra_specs(result)
|
||||
|
||||
|
||||
@require_context
|
||||
|
@ -2107,6 +2171,51 @@ def volume_get_active_by_window(context,
|
|||
return query.all()
|
||||
|
||||
|
||||
def _volume_type_access_query(context, session=None):
|
||||
return model_query(context, models.VolumeTypeProjects, session=session,
|
||||
read_deleted="no")
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_access_get_all(context, type_id):
|
||||
volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
|
||||
return _volume_type_access_query(context).\
|
||||
filter_by(volume_type_id=volume_type_id).all()
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_access_add(context, type_id, project_id):
|
||||
"""Add given tenant to the volume type access list."""
|
||||
volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
|
||||
|
||||
access_ref = models.VolumeTypeProjects()
|
||||
access_ref.update({"volume_type_id": volume_type_id,
|
||||
"project_id": project_id})
|
||||
|
||||
session = get_session()
|
||||
with session.begin():
|
||||
try:
|
||||
access_ref.save(session=session)
|
||||
except db_exc.DBDuplicateEntry:
|
||||
raise exception.VolumeTypeAccessExists(volume_type_id=type_id,
|
||||
project_id=project_id)
|
||||
return access_ref
|
||||
|
||||
|
||||
@require_admin_context
|
||||
def volume_type_access_remove(context, type_id, project_id):
|
||||
"""Remove given tenant from the volume type access list."""
|
||||
volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
|
||||
|
||||
count = _volume_type_access_query(context).\
|
||||
filter_by(volume_type_id=volume_type_id).\
|
||||
filter_by(project_id=project_id).\
|
||||
soft_delete(synchronize_session=False)
|
||||
if count == 0:
|
||||
raise exception.VolumeTypeAccessNotFound(
|
||||
volume_type_id=type_id, project_id=project_id)
|
||||
|
||||
|
||||
####################
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# 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, UniqueConstraint
|
||||
from sqlalchemy import Integer, MetaData, String, Table, ForeignKey
|
||||
|
||||
from cinder.i18n import _
|
||||
from cinder.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
volume_types = Table('volume_types', meta, autoload=True)
|
||||
is_public = Column('is_public', Boolean)
|
||||
|
||||
try:
|
||||
volume_types.create_column(is_public)
|
||||
# pylint: disable=E1120
|
||||
volume_types.update().values(is_public=True).execute()
|
||||
except Exception:
|
||||
LOG.error(_("Column |%s| not created!"), repr(is_public))
|
||||
raise
|
||||
|
||||
volume_type_projects = Table(
|
||||
'volume_type_projects', meta,
|
||||
Column('id', Integer, primary_key=True, nullable=False),
|
||||
Column('created_at', DateTime),
|
||||
Column('updated_at', DateTime),
|
||||
Column('deleted_at', DateTime),
|
||||
Column('volume_type_id', String(36),
|
||||
ForeignKey('volume_types.id')),
|
||||
Column('project_id', String(length=255)),
|
||||
Column('deleted', Boolean(create_constraint=True, name=None)),
|
||||
UniqueConstraint('volume_type_id', 'project_id', 'deleted'),
|
||||
mysql_engine='InnoDB',
|
||||
)
|
||||
|
||||
try:
|
||||
volume_type_projects.create()
|
||||
except Exception:
|
||||
LOG.error(_("Table |%s| not created!"), repr(volume_type_projects))
|
||||
raise
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
volume_types = Table('volume_types', meta, autoload=True)
|
||||
is_public = volume_types.columns.is_public
|
||||
try:
|
||||
volume_types.drop_column(is_public)
|
||||
except Exception:
|
||||
LOG.error(_("volume_types.is_public column not dropped"))
|
||||
raise
|
||||
|
||||
volume_type_projects = Table('volume_type_projects', meta, autoload=True)
|
||||
try:
|
||||
volume_type_projects.drop()
|
||||
except Exception:
|
||||
LOG.error(_("volume_type_projects table not dropped"))
|
||||
raise
|
|
@ -0,0 +1,29 @@
|
|||
-- As sqlite does not support the DROP CHECK, we need to create
|
||||
-- the table, and move all the data to it.
|
||||
|
||||
CREATE TABLE volume_types_v31 (
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME,
|
||||
deleted BOOLEAN,
|
||||
id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(255),
|
||||
qos_specs_id VARCHAR(36),
|
||||
PRIMARY KEY (id),
|
||||
CHECK (deleted IN (0, 1)),
|
||||
FOREIGN KEY(qos_specs_id) REFERENCES quality_of_service_specs (id)
|
||||
);
|
||||
|
||||
INSERT INTO volume_types_v31
|
||||
SELECT created_at,
|
||||
updated_at,
|
||||
deleted_at,
|
||||
deleted,
|
||||
id,
|
||||
name,
|
||||
qos_specs_id
|
||||
FROM volume_types;
|
||||
|
||||
DROP TABLE volume_types;
|
||||
ALTER TABLE volume_types_v31 RENAME TO volume_types;
|
||||
DROP TABLE volume_type_projects;
|
|
@ -203,6 +203,7 @@ class VolumeTypes(BASE, CinderBase):
|
|||
# A reference to qos_specs entity
|
||||
qos_specs_id = Column(String(36),
|
||||
ForeignKey('quality_of_service_specs.id'))
|
||||
is_public = Column(Boolean, default=True)
|
||||
volumes = relationship(Volume,
|
||||
backref=backref('volume_type', uselist=False),
|
||||
foreign_keys=id,
|
||||
|
@ -211,6 +212,27 @@ class VolumeTypes(BASE, CinderBase):
|
|||
'VolumeTypes.deleted == False)')
|
||||
|
||||
|
||||
class VolumeTypeProjects(BASE, CinderBase):
|
||||
"""Represent projects associated volume_types."""
|
||||
__tablename__ = "volume_type_projects"
|
||||
__table_args__ = (schema.UniqueConstraint(
|
||||
"volume_type_id", "project_id", "deleted",
|
||||
name="uniq_volume_type_projects0volume_type_id0project_id0deleted"),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
volume_type_id = Column(Integer, ForeignKey('volume_types.id'),
|
||||
nullable=False)
|
||||
project_id = Column(String(255))
|
||||
|
||||
volume_type = relationship(
|
||||
VolumeTypes,
|
||||
backref="projects",
|
||||
foreign_keys=volume_type_id,
|
||||
primaryjoin='and_('
|
||||
'VolumeTypeProjects.volume_type_id == VolumeTypes.id,'
|
||||
'VolumeTypeProjects.deleted == False)')
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecs(BASE, CinderBase):
|
||||
"""Represents additional specs as key/value pairs for a volume_type."""
|
||||
__tablename__ = 'volume_type_extra_specs'
|
||||
|
|
|
@ -271,6 +271,11 @@ class VolumeTypeNotFoundByName(VolumeTypeNotFound):
|
|||
"could not be found.")
|
||||
|
||||
|
||||
class VolumeTypeAccessNotFound(NotFound):
|
||||
message = _("Volume type access not found for %(volume_type_id)s / "
|
||||
"%(project_id)s combination.")
|
||||
|
||||
|
||||
class VolumeTypeExtraSpecsNotFound(NotFound):
|
||||
message = _("Volume Type %(volume_type_id)s has no extra specs with "
|
||||
"key %(extra_specs_key)s.")
|
||||
|
@ -376,6 +381,11 @@ class VolumeTypeExists(Duplicate):
|
|||
message = _("Volume Type %(id)s already exists.")
|
||||
|
||||
|
||||
class VolumeTypeAccessExists(Duplicate):
|
||||
message = _("Volume type access for %(volume_type_id)s / "
|
||||
"%(project_id)s combination already exists.")
|
||||
|
||||
|
||||
class VolumeTypeEncryptionExists(Invalid):
|
||||
message = _("Volume type encryption for type %(type_id)s already exists.")
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ def stub_volume_type_extra_specs():
|
|||
return specs
|
||||
|
||||
|
||||
def volume_type_get(context, volume_type_id):
|
||||
def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -50,11 +50,11 @@ def return_volume_types_with_volumes_destroy(context, id):
|
|||
pass
|
||||
|
||||
|
||||
def return_volume_types_create(context, name, specs):
|
||||
def return_volume_types_create(context, name, specs, is_public):
|
||||
pass
|
||||
|
||||
|
||||
def return_volume_types_create_duplicate_type(context, name, specs):
|
||||
def return_volume_types_create_duplicate_type(context, name, specs, is_public):
|
||||
raise exception.VolumeTypeExists(id=name)
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
#
|
||||
# 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
|
||||
|
||||
import webob
|
||||
|
||||
from cinder.api.contrib import volume_type_access as type_access
|
||||
from cinder.api.v2 import types as types_api_v2
|
||||
from cinder import context
|
||||
from cinder import db
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.tests.api import fakes
|
||||
|
||||
|
||||
def generate_type(type_id, is_public):
|
||||
return {
|
||||
'id': type_id,
|
||||
'name': u'test',
|
||||
'deleted': False,
|
||||
'created_at': datetime.datetime(2012, 1, 1, 1, 1, 1, 1),
|
||||
'updated_at': None,
|
||||
'deleted_at': None,
|
||||
'is_public': bool(is_public)
|
||||
}
|
||||
|
||||
|
||||
VOLUME_TYPES = {
|
||||
'0': generate_type('0', True),
|
||||
'1': generate_type('1', True),
|
||||
'2': generate_type('2', False),
|
||||
'3': generate_type('3', False)}
|
||||
|
||||
PROJ1_UUID = '11111111-1111-1111-1111-111111111111'
|
||||
PROJ2_UUID = '22222222-2222-2222-2222-222222222222'
|
||||
PROJ3_UUID = '33333333-3333-3333-3333-333333333333'
|
||||
|
||||
ACCESS_LIST = [{'volume_type_id': '2', 'project_id': PROJ2_UUID},
|
||||
{'volume_type_id': '2', 'project_id': PROJ3_UUID},
|
||||
{'volume_type_id': '3', 'project_id': PROJ3_UUID}]
|
||||
|
||||
|
||||
def fake_volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||
vol = VOLUME_TYPES[id]
|
||||
if expected_fields and 'projects' in expected_fields:
|
||||
vol['projects'] = [a['project_id']
|
||||
for a in ACCESS_LIST if a['volume_type_id'] == id]
|
||||
return vol
|
||||
|
||||
|
||||
def _has_type_access(type_id, project_id):
|
||||
for access in ACCESS_LIST:
|
||||
if access['volume_type_id'] == type_id and \
|
||||
access['project_id'] == project_id:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def fake_volume_type_get_all(context, inactive=False, filters=None):
|
||||
if filters is None or filters['is_public'] is None:
|
||||
return VOLUME_TYPES
|
||||
res = {}
|
||||
for k, v in VOLUME_TYPES.iteritems():
|
||||
if filters['is_public'] and _has_type_access(k, context.project_id):
|
||||
res.update({k: v})
|
||||
continue
|
||||
if v['is_public'] == filters['is_public']:
|
||||
res.update({k: v})
|
||||
return res
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
obj = {'volume_type': {'id': '0'},
|
||||
'volume_types': [
|
||||
{'id': '0'},
|
||||
{'id': '2'}]}
|
||||
|
||||
def attach(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class FakeRequest(object):
|
||||
environ = {"cinder.context": context.get_admin_context()}
|
||||
|
||||
def cached_resource_by_id(self, resource_id, name=None):
|
||||
return VOLUME_TYPES[resource_id]
|
||||
|
||||
|
||||
class VolumeTypeAccessTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(VolumeTypeAccessTest, self).setUp()
|
||||
self.type_controller_v2 = types_api_v2.VolumeTypesController()
|
||||
self.type_access_controller = type_access.VolumeTypeAccessController()
|
||||
self.type_action_controller = type_access.VolumeTypeActionController()
|
||||
self.req = FakeRequest()
|
||||
self.context = self.req.environ['cinder.context']
|
||||
self.stubs.Set(db, 'volume_type_get',
|
||||
fake_volume_type_get)
|
||||
self.stubs.Set(db, 'volume_type_get_all',
|
||||
fake_volume_type_get_all)
|
||||
|
||||
def assertVolumeTypeListEqual(self, expected, observed):
|
||||
self.assertEqual(len(expected), len(observed))
|
||||
expected = sorted(expected, key=lambda item: item['id'])
|
||||
observed = sorted(observed, key=lambda item: item['id'])
|
||||
for d1, d2 in zip(expected, observed):
|
||||
self.assertEqual(d1['id'], d2['id'])
|
||||
|
||||
def test_list_type_access_public(self):
|
||||
"""Querying os-volume-type-access on public type should return 404."""
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/os-volume-type-access',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.type_access_controller.index,
|
||||
req, '1')
|
||||
|
||||
def test_list_type_access_private(self):
|
||||
expected = {'volume_type_access': [
|
||||
{'volume_type_id': '2', 'project_id': PROJ2_UUID},
|
||||
{'volume_type_id': '2', 'project_id': PROJ3_UUID}]}
|
||||
result = self.type_access_controller.index(self.req, '2')
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_list_with_no_context(self):
|
||||
req = fakes.HTTPRequest.blank('/v2/flavors/fake/flavors')
|
||||
|
||||
def fake_authorize(context, target=None, action=None):
|
||||
raise exception.PolicyNotAuthorized(action='index')
|
||||
self.stubs.Set(type_access, 'authorize', fake_authorize)
|
||||
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_access_controller.index,
|
||||
req, 'fake')
|
||||
|
||||
def test_list_type_with_admin_default_proj1(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||
use_admin_context=True)
|
||||
req.environ['cinder.context'].project_id = PROJ1_UUID
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_admin_default_proj2(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}, {'id': '2'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||
use_admin_context=True)
|
||||
req.environ['cinder.context'].project_id = PROJ2_UUID
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_true(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_false(self):
|
||||
expected = {'volume_types': [{'id': '2'}, {'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_false_proj2(self):
|
||||
expected = {'volume_types': [{'id': '2'}, {'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=True)
|
||||
req.environ['cinder.context'].project_id = PROJ2_UUID
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_admin_ispublic_none(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}, {'id': '2'},
|
||||
{'id': '3'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=none',
|
||||
use_admin_context=True)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_no_admin_default(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_true(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=true',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_false(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=false',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_list_type_with_no_admin_ispublic_none(self):
|
||||
expected = {'volume_types': [{'id': '0'}, {'id': '1'}]}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types?is_public=none',
|
||||
use_admin_context=False)
|
||||
result = self.type_controller_v2.index(req)
|
||||
self.assertVolumeTypeListEqual(expected['volume_types'],
|
||||
result['volume_types'])
|
||||
|
||||
def test_show(self):
|
||||
resp = FakeResponse()
|
||||
self.type_action_controller.show(self.req, resp, '0')
|
||||
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': True},
|
||||
resp.obj['volume_type'])
|
||||
self.type_action_controller.show(self.req, resp, '2')
|
||||
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': False},
|
||||
resp.obj['volume_type'])
|
||||
|
||||
def test_detail(self):
|
||||
resp = FakeResponse()
|
||||
self.type_action_controller.detail(self.req, resp)
|
||||
self.assertEqual(
|
||||
[{'id': '0', 'os-volume-type-access:is_public': True},
|
||||
{'id': '2', 'os-volume-type-access:is_public': False}],
|
||||
resp.obj['volume_types'])
|
||||
|
||||
def test_create(self):
|
||||
resp = FakeResponse()
|
||||
self.type_action_controller.create(self.req, {}, resp)
|
||||
self.assertEqual({'id': '0', 'os-volume-type-access:is_public': True},
|
||||
resp.obj['volume_type'])
|
||||
|
||||
def test_add_project_access(self):
|
||||
def stub_add_volume_type_access(context, type_id, project_id):
|
||||
self.assertEqual('3', type_id, "type_id")
|
||||
self.assertEqual(PROJ2_UUID, project_id, "project_id")
|
||||
self.stubs.Set(db, 'volume_type_access_add',
|
||||
stub_add_volume_type_access)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
result = self.type_action_controller._addProjectAccess(req, '3', body)
|
||||
self.assertEqual(202, result.status_code)
|
||||
|
||||
def test_add_project_access_with_no_admin_user(self):
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=False)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_action_controller._addProjectAccess,
|
||||
req, '2', body)
|
||||
|
||||
def test_add_project_access_with_already_added_access(self):
|
||||
def stub_add_volume_type_access(context, type_id, project_id):
|
||||
raise exception.VolumeTypeAccessExists(volume_type_id=type_id,
|
||||
project_id=project_id)
|
||||
self.stubs.Set(db, 'volume_type_access_add',
|
||||
stub_add_volume_type_access)
|
||||
body = {'addProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPConflict,
|
||||
self.type_action_controller._addProjectAccess,
|
||||
req, '3', body)
|
||||
|
||||
def test_remove_project_access_with_bad_access(self):
|
||||
def stub_remove_volume_type_access(context, type_id, project_id):
|
||||
raise exception.VolumeTypeAccessNotFound(volume_type_id=type_id,
|
||||
project_id=project_id)
|
||||
self.stubs.Set(db, 'volume_type_access_remove',
|
||||
stub_remove_volume_type_access)
|
||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=True)
|
||||
self.assertRaises(webob.exc.HTTPNotFound,
|
||||
self.type_action_controller._removeProjectAccess,
|
||||
req, '3', body)
|
||||
|
||||
def test_remove_project_access_with_no_admin_user(self):
|
||||
req = fakes.HTTPRequest.blank('/v2/fake/types/2/action',
|
||||
use_admin_context=False)
|
||||
body = {'removeProjectAccess': {'project': PROJ2_UUID}}
|
||||
self.assertRaises(exception.PolicyNotAuthorized,
|
||||
self.type_action_controller._removeProjectAccess,
|
||||
req, '2', body)
|
|
@ -36,13 +36,13 @@ def stub_volume_type(id):
|
|||
return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs)
|
||||
|
||||
|
||||
def return_volume_types_get_all_types(context):
|
||||
def return_volume_types_get_all_types(context, search_opts=None):
|
||||
return dict(vol_type_1=stub_volume_type(1),
|
||||
vol_type_2=stub_volume_type(2),
|
||||
vol_type_3=stub_volume_type(3))
|
||||
|
||||
|
||||
def return_empty_volume_types_get_all_types(context):
|
||||
def return_empty_volume_types_get_all_types(context, search_opts=None):
|
||||
return {}
|
||||
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ def stub_volume_type(id):
|
|||
)
|
||||
|
||||
|
||||
def return_volume_types_get_all_types(context):
|
||||
def return_volume_types_get_all_types(context, search_opts=None):
|
||||
return dict(
|
||||
vol_type_1=stub_volume_type(1),
|
||||
vol_type_2=stub_volume_type(2),
|
||||
|
@ -50,7 +50,7 @@ def return_volume_types_get_all_types(context):
|
|||
)
|
||||
|
||||
|
||||
def return_empty_volume_types_get_all_types(context):
|
||||
def return_empty_volume_types_get_all_types(context, search_opts=None):
|
||||
return {}
|
||||
|
||||
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
"volume_extension:volume_actions:upload_image": "",
|
||||
"volume_extension:types_manage": "",
|
||||
"volume_extension:types_extra_specs": "",
|
||||
"volume_extension:volume_type_access": "",
|
||||
"volume_extension:volume_type_access:addProjectAccess": "rule:admin_api",
|
||||
"volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api",
|
||||
"volume_extension:volume_type_encryption": "rule:admin_api",
|
||||
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
||||
"volume_extension:qos_specs_manage": "",
|
||||
|
|
|
@ -1298,3 +1298,53 @@ class TestMigrations(test.TestCase):
|
|||
execute().scalar()
|
||||
|
||||
self.assertEqual(4, num_defaults)
|
||||
|
||||
def test_migration_032(self):
|
||||
"""Test adding volume_type_projects table works correctly."""
|
||||
for (key, engine) in self.engines.items():
|
||||
migration_api.version_control(engine,
|
||||
TestMigrations.REPOSITORY,
|
||||
migration.db_initial_version())
|
||||
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 31)
|
||||
metadata = sqlalchemy.schema.MetaData()
|
||||
metadata.bind = engine
|
||||
|
||||
migration_api.upgrade(engine, TestMigrations.REPOSITORY, 32)
|
||||
|
||||
self.assertTrue(engine.dialect.has_table(engine.connect(),
|
||||
"volume_type_projects"))
|
||||
|
||||
volume_type_projects = sqlalchemy.Table('volume_type_projects',
|
||||
metadata,
|
||||
autoload=True)
|
||||
self.assertIsInstance(volume_type_projects.c.created_at.type,
|
||||
self.time_type[engine.name])
|
||||
self.assertIsInstance(volume_type_projects.c.updated_at.type,
|
||||
self.time_type[engine.name])
|
||||
self.assertIsInstance(volume_type_projects.c.deleted_at.type,
|
||||
self.time_type[engine.name])
|
||||
self.assertIsInstance(volume_type_projects.c.deleted.type,
|
||||
self.bool_type[engine.name])
|
||||
self.assertIsInstance(volume_type_projects.c.id.type,
|
||||
sqlalchemy.types.INTEGER)
|
||||
self.assertIsInstance(volume_type_projects.c.volume_type_id.type,
|
||||
sqlalchemy.types.VARCHAR)
|
||||
self.assertIsInstance(volume_type_projects.c.project_id.type,
|
||||
sqlalchemy.types.VARCHAR)
|
||||
|
||||
volume_types = sqlalchemy.Table('volume_types',
|
||||
metadata,
|
||||
autoload=True)
|
||||
self.assertIsInstance(volume_types.c.is_public.type,
|
||||
self.bool_type[engine.name])
|
||||
|
||||
migration_api.downgrade(engine, TestMigrations.REPOSITORY, 31)
|
||||
metadata = sqlalchemy.schema.MetaData()
|
||||
metadata.bind = engine
|
||||
|
||||
self.assertFalse(engine.dialect.has_table(engine.connect(),
|
||||
"volume_type_projects"))
|
||||
volume_types = sqlalchemy.Table('volume_types',
|
||||
metadata,
|
||||
autoload=True)
|
||||
self.assertNotIn('is_public', volume_types.c)
|
||||
|
|
|
@ -229,6 +229,24 @@ class VolumeTypeTestCase(test.TestCase):
|
|||
encryption)
|
||||
self.assertTrue(volume_types.is_encrypted(self.ctxt, volume_type_id))
|
||||
|
||||
def test_add_access(self):
|
||||
project_id = '456'
|
||||
vtype = volume_types.create(self.ctxt, 'type1')
|
||||
vtype_id = vtype.get('id')
|
||||
|
||||
volume_types.add_volume_type_access(self.ctxt, vtype_id, project_id)
|
||||
vtype_access = db.volume_type_access_get_all(self.ctxt, vtype_id)
|
||||
self.assertIn(project_id, [a.project_id for a in vtype_access])
|
||||
|
||||
def test_remove_access(self):
|
||||
project_id = '456'
|
||||
vtype = volume_types.create(self.ctxt, 'type1', projects=['456'])
|
||||
vtype_id = vtype.get('id')
|
||||
|
||||
volume_types.remove_volume_type_access(self.ctxt, vtype_id, project_id)
|
||||
vtype_access = db.volume_type_access_get_all(self.ctxt, vtype_id)
|
||||
self.assertNotIn(project_id, vtype_access)
|
||||
|
||||
def test_get_volume_type_qos_specs(self):
|
||||
qos_ref = qos_specs.create(self.ctxt, 'qos-specs-1', {'k1': 'v1',
|
||||
'k2': 'v2',
|
||||
|
|
|
@ -414,6 +414,14 @@ def is_valid_boolstr(val):
|
|||
val == '1' or val == '0')
|
||||
|
||||
|
||||
def is_none_string(val):
|
||||
"""Check if a string represents a None value."""
|
||||
if not isinstance(val, six.string_types):
|
||||
return False
|
||||
|
||||
return val.lower() == 'none'
|
||||
|
||||
|
||||
def monkey_patch():
|
||||
"""If the CONF.monkey_patch set as True,
|
||||
this function patches a decorator
|
||||
|
|
|
@ -34,13 +34,16 @@ CONF = cfg.CONF
|
|||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create(context, name, extra_specs=None):
|
||||
def create(context, name, extra_specs=None, is_public=True, projects=None):
|
||||
"""Creates volume types."""
|
||||
extra_specs = extra_specs or {}
|
||||
projects = projects or []
|
||||
try:
|
||||
type_ref = db.volume_type_create(context,
|
||||
dict(name=name,
|
||||
extra_specs=extra_specs))
|
||||
extra_specs=extra_specs,
|
||||
is_public=is_public),
|
||||
projects=projects)
|
||||
except db_exc.DBError as e:
|
||||
LOG.exception(_LE('DB error: %s') % e)
|
||||
raise exception.VolumeTypeCreateFailed(name=name,
|
||||
|
@ -64,7 +67,13 @@ def get_all_types(context, inactive=0, search_opts=None):
|
|||
|
||||
"""
|
||||
search_opts = search_opts or {}
|
||||
vol_types = db.volume_type_get_all(context, inactive)
|
||||
filters = {}
|
||||
|
||||
if 'is_public' in search_opts:
|
||||
filters['is_public'] = search_opts['is_public']
|
||||
del search_opts['is_public']
|
||||
|
||||
vol_types = db.volume_type_get_all(context, inactive, filters=filters)
|
||||
|
||||
if search_opts:
|
||||
LOG.debug("Searching by: %s" % search_opts)
|
||||
|
@ -96,7 +105,7 @@ def get_all_types(context, inactive=0, search_opts=None):
|
|||
return vol_types
|
||||
|
||||
|
||||
def get_volume_type(ctxt, id):
|
||||
def get_volume_type(ctxt, id, expected_fields=None):
|
||||
"""Retrieves single volume type by id."""
|
||||
if id is None:
|
||||
msg = _("id cannot be None")
|
||||
|
@ -105,7 +114,7 @@ def get_volume_type(ctxt, id):
|
|||
if ctxt is None:
|
||||
ctxt = context.get_admin_context()
|
||||
|
||||
return db.volume_type_get(ctxt, id)
|
||||
return db.volume_type_get(ctxt, id, expected_fields=expected_fields)
|
||||
|
||||
|
||||
def get_volume_type_by_name(context, name):
|
||||
|
@ -149,6 +158,22 @@ def get_volume_type_extra_specs(volume_type_id, key=False):
|
|||
return extra_specs
|
||||
|
||||
|
||||
def add_volume_type_access(context, volume_type_id, project_id):
|
||||
"""Add access to volume type for project_id."""
|
||||
if volume_type_id is None:
|
||||
msg = _("volume_type_id cannot be None")
|
||||
raise exception.InvalidVolumeType(reason=msg)
|
||||
return db.volume_type_access_add(context, volume_type_id, project_id)
|
||||
|
||||
|
||||
def remove_volume_type_access(context, volume_type_id, project_id):
|
||||
"""Remove access to volume type for project_id."""
|
||||
if volume_type_id is None:
|
||||
msg = _("volume_type_id cannot be None")
|
||||
raise exception.InvalidVolumeType(reason=msg)
|
||||
return db.volume_type_access_remove(context, volume_type_id, project_id)
|
||||
|
||||
|
||||
def is_encrypted(context, volume_type_id):
|
||||
if volume_type_id is None:
|
||||
return False
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
|
||||
"volume_extension:types_manage": "rule:admin_api",
|
||||
"volume_extension:types_extra_specs": "rule:admin_api",
|
||||
"volume_extension:volume_type_access": "",
|
||||
"volume_extension:volume_type_access:addProjectAccess": "rule:admin_api",
|
||||
"volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api",
|
||||
"volume_extension:volume_type_encryption": "rule:admin_api",
|
||||
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
||||
"volume_extension:extended_snapshot_attributes": "",
|
||||
|
|
Loading…
Reference in New Issue