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']
|
vol_type = body['volume_type']
|
||||||
name = vol_type.get('name', None)
|
name = vol_type.get('name', None)
|
||||||
specs = vol_type.get('extra_specs', {})
|
specs = vol_type.get('extra_specs', {})
|
||||||
|
is_public = vol_type.get('os-volume-type-access:is_public', True)
|
||||||
|
|
||||||
if name is None or name == "":
|
if name is None or name == "":
|
||||||
raise webob.exc.HTTPBadRequest()
|
raise webob.exc.HTTPBadRequest()
|
||||||
|
|
||||||
try:
|
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)
|
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)
|
notifier_info = dict(volume_types=vol_type)
|
||||||
rpc.get_notifier('volumeType').info(context, 'volume_type.create',
|
rpc.get_notifier('volumeType').info(context, 'volume_type.create',
|
||||||
notifier_info)
|
notifier_info)
|
||||||
|
215
cinder/api/contrib/volume_type_access.py
Normal file
215
cinder/api/contrib/volume_type_access.py
Normal file
@ -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 extension_authorizer(api_name, extension_name):
|
||||||
def authorize(context, target=None):
|
def authorize(context, target=None, action=None):
|
||||||
if target is None:
|
if target is None:
|
||||||
target = {'project_id': context.project_id,
|
target = {'project_id': context.project_id,
|
||||||
'user_id': context.user_id}
|
'user_id': context.user_id}
|
||||||
action = '%s_extension:%s' % (api_name, extension_name)
|
if action is None:
|
||||||
cinder.policy.enforce(context, action, target)
|
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
|
return authorize
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,6 +57,7 @@ class VolumeTypesController(wsgi.Controller):
|
|||||||
"""Returns the list of volume types."""
|
"""Returns the list of volume types."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
vol_types = volume_types.get_all_types(context).values()
|
vol_types = volume_types.get_all_types(context).values()
|
||||||
|
req.cache_resource(vol_types, name='types')
|
||||||
return self._view_builder.index(req, vol_types)
|
return self._view_builder.index(req, vol_types)
|
||||||
|
|
||||||
@wsgi.serializers(xml=VolumeTypeTemplate)
|
@wsgi.serializers(xml=VolumeTypeTemplate)
|
||||||
@ -66,6 +67,7 @@ class VolumeTypesController(wsgi.Controller):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
vol_type = volume_types.get_volume_type(context, id)
|
vol_type = volume_types.get_volume_type(context, id)
|
||||||
|
req.cache_resource(vol_type, name='types')
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
raise exc.HTTPNotFound()
|
raise exc.HTTPNotFound()
|
||||||
|
|
||||||
|
@ -54,7 +54,8 @@ class APIRouter(cinder.api.openstack.APIRouter):
|
|||||||
|
|
||||||
self.resources['types'] = types.create_resource()
|
self.resources['types'] = types.create_resource()
|
||||||
mapper.resource("type", "types",
|
mapper.resource("type", "types",
|
||||||
controller=self.resources['types'])
|
controller=self.resources['types'],
|
||||||
|
member={'action': 'POST'})
|
||||||
|
|
||||||
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
|
self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
|
||||||
mapper.resource("snapshot", "snapshots",
|
mapper.resource("snapshot", "snapshots",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
"""The volume type & volume types extra specs extension."""
|
"""The volume type & volume types extra specs extension."""
|
||||||
|
|
||||||
|
from oslo.utils import strutils
|
||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from cinder.api.openstack import wsgi
|
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.api import xmlutil
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
|
from cinder import utils
|
||||||
from cinder.volume import volume_types
|
from cinder.volume import volume_types
|
||||||
|
|
||||||
|
|
||||||
@ -56,9 +58,9 @@ class VolumeTypesController(wsgi.Controller):
|
|||||||
@wsgi.serializers(xml=VolumeTypesTemplate)
|
@wsgi.serializers(xml=VolumeTypesTemplate)
|
||||||
def index(self, req):
|
def index(self, req):
|
||||||
"""Returns the list of volume types."""
|
"""Returns the list of volume types."""
|
||||||
context = req.environ['cinder.context']
|
limited_types = self._get_volume_types(req)
|
||||||
vol_types = volume_types.get_all_types(context).values()
|
req.cache_resource(limited_types, name='types')
|
||||||
return self._view_builder.index(req, vol_types)
|
return self._view_builder.index(req, limited_types)
|
||||||
|
|
||||||
@wsgi.serializers(xml=VolumeTypeTemplate)
|
@wsgi.serializers(xml=VolumeTypeTemplate)
|
||||||
def show(self, req, id):
|
def show(self, req, id):
|
||||||
@ -67,12 +69,47 @@ class VolumeTypesController(wsgi.Controller):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
vol_type = volume_types.get_volume_type(context, id)
|
vol_type = volume_types.get_volume_type(context, id)
|
||||||
|
req.cache_resource(vol_type, name='types')
|
||||||
except exception.NotFound:
|
except exception.NotFound:
|
||||||
msg = _("Volume type not found")
|
msg = _("Volume type not found")
|
||||||
raise exc.HTTPNotFound(explanation=msg)
|
raise exc.HTTPNotFound(explanation=msg)
|
||||||
|
|
||||||
return self._view_builder.show(req, vol_type)
|
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():
|
def create_resource():
|
||||||
return wsgi.Resource(VolumeTypesController())
|
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."""
|
"""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):
|
def volume_type_get_all(context, inactive=False, filters=None):
|
||||||
"""Get all volume types."""
|
"""Get all volume types.
|
||||||
return IMPL.volume_type_get_all(context, inactive)
|
|
||||||
|
: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):
|
def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||||
"""Get volume type by id."""
|
"""Get volume type by id.
|
||||||
return IMPL.volume_type_get(context, id, inactive)
|
|
||||||
|
: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):
|
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)
|
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 joinedload, joinedload_all
|
||||||
from sqlalchemy.orm import RelationshipProperty
|
from sqlalchemy.orm import RelationshipProperty
|
||||||
from sqlalchemy.sql.expression import literal_column
|
from sqlalchemy.sql.expression import literal_column
|
||||||
|
from sqlalchemy.sql.expression import true
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
from cinder.common import sqlalchemyutils
|
from cinder.common import sqlalchemyutils
|
||||||
@ -1871,8 +1872,8 @@ def snapshot_metadata_update(context, snapshot_id, metadata, delete):
|
|||||||
|
|
||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def volume_type_create(context, values):
|
def volume_type_create(context, values, projects=None):
|
||||||
"""Create a new instance type.
|
"""Create a new volume type.
|
||||||
|
|
||||||
In order to pass in extra specs, the values dict should contain a
|
In order to pass in extra specs, the values dict should contain a
|
||||||
'extra_specs' key/value pair:
|
'extra_specs' key/value pair:
|
||||||
@ -1881,6 +1882,8 @@ def volume_type_create(context, values):
|
|||||||
if not values.get('id'):
|
if not values.get('id'):
|
||||||
values['id'] = str(uuid.uuid4())
|
values['id'] = str(uuid.uuid4())
|
||||||
|
|
||||||
|
projects = projects or []
|
||||||
|
|
||||||
session = get_session()
|
session = get_session()
|
||||||
with session.begin():
|
with session.begin():
|
||||||
try:
|
try:
|
||||||
@ -1901,20 +1904,59 @@ def volume_type_create(context, values):
|
|||||||
session.add(volume_type_ref)
|
session.add(volume_type_ref)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise db_exc.DBError(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
|
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
|
@require_context
|
||||||
def volume_type_get_all(context, inactive=False, filters=None):
|
def volume_type_get_all(context, inactive=False, filters=None):
|
||||||
"""Returns a dict describing all volume_types with name as key."""
|
"""Returns a dict describing all volume_types with name as key."""
|
||||||
filters = filters or {}
|
filters = filters or {}
|
||||||
|
|
||||||
read_deleted = "yes" if inactive else "no"
|
read_deleted = "yes" if inactive else "no"
|
||||||
rows = model_query(context, models.VolumeTypes,
|
|
||||||
read_deleted=read_deleted).\
|
query = _volume_type_get_query(context, read_deleted=read_deleted)
|
||||||
options(joinedload('extra_specs')).\
|
|
||||||
order_by("name").\
|
if 'is_public' in filters and filters['is_public'] is not None:
|
||||||
all()
|
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 = {}
|
result = {}
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@ -1923,28 +1965,50 @@ def volume_type_get_all(context, inactive=False, filters=None):
|
|||||||
return result
|
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
|
@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"
|
read_deleted = "yes" if inactive else "no"
|
||||||
result = model_query(context,
|
result = _volume_type_get_query(
|
||||||
models.VolumeTypes,
|
context, session, read_deleted, expected_fields).\
|
||||||
session=session,
|
|
||||||
read_deleted=read_deleted).\
|
|
||||||
options(joinedload('extra_specs')).\
|
|
||||||
filter_by(id=id).\
|
filter_by(id=id).\
|
||||||
first()
|
first()
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
raise exception.VolumeTypeNotFound(volume_type_id=id)
|
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
|
@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 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
|
@require_context
|
||||||
@ -1956,7 +2020,7 @@ def _volume_type_get_by_name(context, name, session=None):
|
|||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
|
||||||
else:
|
|
||||||
return _dict_with_extra_specs(result)
|
return _dict_with_extra_specs(result)
|
||||||
|
|
||||||
|
|
||||||
@ -2107,6 +2171,51 @@ def volume_get_active_by_window(context,
|
|||||||
return query.all()
|
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
|
# A reference to qos_specs entity
|
||||||
qos_specs_id = Column(String(36),
|
qos_specs_id = Column(String(36),
|
||||||
ForeignKey('quality_of_service_specs.id'))
|
ForeignKey('quality_of_service_specs.id'))
|
||||||
|
is_public = Column(Boolean, default=True)
|
||||||
volumes = relationship(Volume,
|
volumes = relationship(Volume,
|
||||||
backref=backref('volume_type', uselist=False),
|
backref=backref('volume_type', uselist=False),
|
||||||
foreign_keys=id,
|
foreign_keys=id,
|
||||||
@ -211,6 +212,27 @@ class VolumeTypes(BASE, CinderBase):
|
|||||||
'VolumeTypes.deleted == False)')
|
'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):
|
class VolumeTypeExtraSpecs(BASE, CinderBase):
|
||||||
"""Represents additional specs as key/value pairs for a volume_type."""
|
"""Represents additional specs as key/value pairs for a volume_type."""
|
||||||
__tablename__ = 'volume_type_extra_specs'
|
__tablename__ = 'volume_type_extra_specs'
|
||||||
|
@ -271,6 +271,11 @@ class VolumeTypeNotFoundByName(VolumeTypeNotFound):
|
|||||||
"could not be found.")
|
"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):
|
class VolumeTypeExtraSpecsNotFound(NotFound):
|
||||||
message = _("Volume Type %(volume_type_id)s has no extra specs with "
|
message = _("Volume Type %(volume_type_id)s has no extra specs with "
|
||||||
"key %(extra_specs_key)s.")
|
"key %(extra_specs_key)s.")
|
||||||
@ -376,6 +381,11 @@ class VolumeTypeExists(Duplicate):
|
|||||||
message = _("Volume Type %(id)s already exists.")
|
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):
|
class VolumeTypeEncryptionExists(Invalid):
|
||||||
message = _("Volume type encryption for type %(type_id)s already exists.")
|
message = _("Volume type encryption for type %(type_id)s already exists.")
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ def stub_volume_type_extra_specs():
|
|||||||
return specs
|
return specs
|
||||||
|
|
||||||
|
|
||||||
def volume_type_get(context, volume_type_id):
|
def volume_type_get(context, id, inactive=False, expected_fields=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -50,11 +50,11 @@ def return_volume_types_with_volumes_destroy(context, id):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def return_volume_types_create(context, name, specs):
|
def return_volume_types_create(context, name, specs, is_public):
|
||||||
pass
|
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)
|
raise exception.VolumeTypeExists(id=name)
|
||||||
|
|
||||||
|
|
||||||
|
306
cinder/tests/api/contrib/test_volume_type_access.py
Normal file
306
cinder/tests/api/contrib/test_volume_type_access.py
Normal file
@ -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)
|
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),
|
return dict(vol_type_1=stub_volume_type(1),
|
||||||
vol_type_2=stub_volume_type(2),
|
vol_type_2=stub_volume_type(2),
|
||||||
vol_type_3=stub_volume_type(3))
|
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 {}
|
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(
|
return dict(
|
||||||
vol_type_1=stub_volume_type(1),
|
vol_type_1=stub_volume_type(1),
|
||||||
vol_type_2=stub_volume_type(2),
|
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 {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,6 +45,9 @@
|
|||||||
"volume_extension:volume_actions:upload_image": "",
|
"volume_extension:volume_actions:upload_image": "",
|
||||||
"volume_extension:types_manage": "",
|
"volume_extension:types_manage": "",
|
||||||
"volume_extension:types_extra_specs": "",
|
"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_type_encryption": "rule:admin_api",
|
||||||
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
||||||
"volume_extension:qos_specs_manage": "",
|
"volume_extension:qos_specs_manage": "",
|
||||||
|
@ -1298,3 +1298,53 @@ class TestMigrations(test.TestCase):
|
|||||||
execute().scalar()
|
execute().scalar()
|
||||||
|
|
||||||
self.assertEqual(4, num_defaults)
|
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)
|
encryption)
|
||||||
self.assertTrue(volume_types.is_encrypted(self.ctxt, volume_type_id))
|
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):
|
def test_get_volume_type_qos_specs(self):
|
||||||
qos_ref = qos_specs.create(self.ctxt, 'qos-specs-1', {'k1': 'v1',
|
qos_ref = qos_specs.create(self.ctxt, 'qos-specs-1', {'k1': 'v1',
|
||||||
'k2': 'v2',
|
'k2': 'v2',
|
||||||
|
@ -414,6 +414,14 @@ def is_valid_boolstr(val):
|
|||||||
val == '1' or val == '0')
|
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():
|
def monkey_patch():
|
||||||
"""If the CONF.monkey_patch set as True,
|
"""If the CONF.monkey_patch set as True,
|
||||||
this function patches a decorator
|
this function patches a decorator
|
||||||
|
@ -34,13 +34,16 @@ CONF = cfg.CONF
|
|||||||
LOG = logging.getLogger(__name__)
|
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."""
|
"""Creates volume types."""
|
||||||
extra_specs = extra_specs or {}
|
extra_specs = extra_specs or {}
|
||||||
|
projects = projects or []
|
||||||
try:
|
try:
|
||||||
type_ref = db.volume_type_create(context,
|
type_ref = db.volume_type_create(context,
|
||||||
dict(name=name,
|
dict(name=name,
|
||||||
extra_specs=extra_specs))
|
extra_specs=extra_specs,
|
||||||
|
is_public=is_public),
|
||||||
|
projects=projects)
|
||||||
except db_exc.DBError as e:
|
except db_exc.DBError as e:
|
||||||
LOG.exception(_LE('DB error: %s') % e)
|
LOG.exception(_LE('DB error: %s') % e)
|
||||||
raise exception.VolumeTypeCreateFailed(name=name,
|
raise exception.VolumeTypeCreateFailed(name=name,
|
||||||
@ -64,7 +67,13 @@ def get_all_types(context, inactive=0, search_opts=None):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
search_opts = search_opts or {}
|
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:
|
if search_opts:
|
||||||
LOG.debug("Searching by: %s" % 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
|
return vol_types
|
||||||
|
|
||||||
|
|
||||||
def get_volume_type(ctxt, id):
|
def get_volume_type(ctxt, id, expected_fields=None):
|
||||||
"""Retrieves single volume type by id."""
|
"""Retrieves single volume type by id."""
|
||||||
if id is None:
|
if id is None:
|
||||||
msg = _("id cannot be None")
|
msg = _("id cannot be None")
|
||||||
@ -105,7 +114,7 @@ def get_volume_type(ctxt, id):
|
|||||||
if ctxt is None:
|
if ctxt is None:
|
||||||
ctxt = context.get_admin_context()
|
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):
|
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
|
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):
|
def is_encrypted(context, volume_type_id):
|
||||||
if volume_type_id is None:
|
if volume_type_id is None:
|
||||||
return False
|
return False
|
||||||
|
@ -21,6 +21,9 @@
|
|||||||
|
|
||||||
"volume_extension:types_manage": "rule:admin_api",
|
"volume_extension:types_manage": "rule:admin_api",
|
||||||
"volume_extension:types_extra_specs": "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_type_encryption": "rule:admin_api",
|
||||||
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
"volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
|
||||||
"volume_extension:extended_snapshot_attributes": "",
|
"volume_extension:extended_snapshot_attributes": "",
|
||||||
|
Loading…
Reference in New Issue
Block a user