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)
--- /dev/null
+#
+# 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]
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
"""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)
try:
vol_type = volume_types.get_volume_type(context, id)
+ req.cache_resource(vol_type, name='types')
except exception.NotFound:
raise exc.HTTPNotFound()
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",
"""The volume type & volume types extra specs extension."""
+from oslo.utils import strutils
from webob import exc
from cinder.api.openstack import wsgi
from cinder.api import xmlutil
from cinder import exception
from cinder.i18n import _
+from cinder import utils
from cinder.volume import volume_types
@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):
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())
##################
-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.
-def volume_type_get(context, id, inactive=False):
- """Get volume type by id."""
- return IMPL.volume_type_get(context, id, inactive)
+ :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, 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):
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)
+
+
####################
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
@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:
if not values.get('id'):
values['id'] = str(uuid.uuid4())
+ projects = projects or []
+
session = get_session()
with session.begin():
try:
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:
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
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
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)
+
+
####################
--- /dev/null
+# 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
--- /dev/null
+-- 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;
# 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,
'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'
"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.")
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.")
return specs
-def volume_type_get(context, volume_type_id):
+def volume_type_get(context, id, inactive=False, expected_fields=None):
pass
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)
--- /dev/null
+#
+# 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)
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 {}
)
-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),
)
-def return_empty_volume_types_get_all_types(context):
+def return_empty_volume_types_get_all_types(context, search_opts=None):
return {}
"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": "",
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)
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',
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
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,
"""
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)
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")
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):
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
"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": "",