]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Volume type access extension
authorMathieu Gagné <mgagne@iweb.com>
Thu, 26 Jun 2014 00:22:12 +0000 (20:22 -0400)
committerMathieu Gagné <mgagne@iweb.com>
Mon, 1 Dec 2014 23:46:31 +0000 (18:46 -0500)
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

23 files changed:
cinder/api/contrib/types_manage.py
cinder/api/contrib/volume_type_access.py [new file with mode: 0644]
cinder/api/extensions.py
cinder/api/v1/types.py
cinder/api/v2/router.py
cinder/api/v2/types.py
cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/db/sqlalchemy/migrate_repo/versions/032_add_volume_type_projects.py [new file with mode: 0644]
cinder/db/sqlalchemy/migrate_repo/versions/032_sqlite_downgrade.sql [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/exception.py
cinder/tests/api/contrib/test_types_extra_specs.py
cinder/tests/api/contrib/test_types_manage.py
cinder/tests/api/contrib/test_volume_type_access.py [new file with mode: 0644]
cinder/tests/api/v1/test_types.py
cinder/tests/api/v2/test_types.py
cinder/tests/policy.json
cinder/tests/test_migrations.py
cinder/tests/test_volume_types.py
cinder/utils.py
cinder/volume/volume_types.py
etc/cinder/policy.json

index bbe530269ca74f37d07138f15c9e5a186ea6126c..96e47f95e8e4055de80bc18608b17986f7218fd0 100644 (file)
@@ -52,13 +52,15 @@ class VolumeTypesManageController(wsgi.Controller):
         vol_type = body['volume_type']
         name = vol_type.get('name', None)
         specs = vol_type.get('extra_specs', {})
+        is_public = vol_type.get('os-volume-type-access:is_public', True)
 
         if name is None or name == "":
             raise webob.exc.HTTPBadRequest()
 
         try:
-            volume_types.create(context, name, specs)
+            volume_types.create(context, name, specs, is_public)
             vol_type = volume_types.get_volume_type_by_name(context, name)
+            req.cache_resource(vol_type, name='types')
             notifier_info = dict(volume_types=vol_type)
             rpc.get_notifier('volumeType').info(context, 'volume_type.create',
                                                 notifier_info)
diff --git a/cinder/api/contrib/volume_type_access.py b/cinder/api/contrib/volume_type_access.py
new file mode 100644 (file)
index 0000000..5371316
--- /dev/null
@@ -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]
index 1f69793d6db69996080eb5fbf79dd93f8d8c56a7..488f9409d1e15b46d8b72740e883ec1037b18152 100644 (file)
@@ -376,12 +376,15 @@ def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
 
 
 def extension_authorizer(api_name, extension_name):
-    def authorize(context, target=None):
+    def authorize(context, target=None, action=None):
         if target is None:
             target = {'project_id': context.project_id,
                       'user_id': context.user_id}
-        action = '%s_extension:%s' % (api_name, extension_name)
-        cinder.policy.enforce(context, action, target)
+        if action is None:
+            act = '%s_extension:%s' % (api_name, extension_name)
+        else:
+            act = '%s_extension:%s:%s' % (api_name, extension_name, action)
+        cinder.policy.enforce(context, act, target)
     return authorize
 
 
index 52fa4cae26603d2ef2ae39353f7cee0a1602d468..99f77c4c7c4d304a1f3532b45d1438c298450406 100644 (file)
@@ -57,6 +57,7 @@ class VolumeTypesController(wsgi.Controller):
         """Returns the list of volume types."""
         context = req.environ['cinder.context']
         vol_types = volume_types.get_all_types(context).values()
+        req.cache_resource(vol_types, name='types')
         return self._view_builder.index(req, vol_types)
 
     @wsgi.serializers(xml=VolumeTypeTemplate)
@@ -66,6 +67,7 @@ class VolumeTypesController(wsgi.Controller):
 
         try:
             vol_type = volume_types.get_volume_type(context, id)
+            req.cache_resource(vol_type, name='types')
         except exception.NotFound:
             raise exc.HTTPNotFound()
 
index 44821c46599b312de3548e5ae102afe1dbd952ab..db9a8b455d4bb27a919abae06c8c876236dfcab4 100644 (file)
@@ -54,7 +54,8 @@ class APIRouter(cinder.api.openstack.APIRouter):
 
         self.resources['types'] = types.create_resource()
         mapper.resource("type", "types",
-                        controller=self.resources['types'])
+                        controller=self.resources['types'],
+                        member={'action': 'POST'})
 
         self.resources['snapshots'] = snapshots.create_resource(ext_mgr)
         mapper.resource("snapshot", "snapshots",
index 75f8ee75a0d04391915ca648d35b91c30733c2fa..c1b2dfc796e207837ecdc9b8b02db935970be4bd 100644 (file)
@@ -15,6 +15,7 @@
 
 """The volume type & volume types extra specs extension."""
 
+from oslo.utils import strutils
 from webob import exc
 
 from cinder.api.openstack import wsgi
@@ -22,6 +23,7 @@ from cinder.api.views import types as views_types
 from cinder.api import xmlutil
 from cinder import exception
 from cinder.i18n import _
+from cinder import utils
 from cinder.volume import volume_types
 
 
@@ -56,9 +58,9 @@ class VolumeTypesController(wsgi.Controller):
     @wsgi.serializers(xml=VolumeTypesTemplate)
     def index(self, req):
         """Returns the list of volume types."""
-        context = req.environ['cinder.context']
-        vol_types = volume_types.get_all_types(context).values()
-        return self._view_builder.index(req, vol_types)
+        limited_types = self._get_volume_types(req)
+        req.cache_resource(limited_types, name='types')
+        return self._view_builder.index(req, limited_types)
 
     @wsgi.serializers(xml=VolumeTypeTemplate)
     def show(self, req, id):
@@ -67,12 +69,47 @@ class VolumeTypesController(wsgi.Controller):
 
         try:
             vol_type = volume_types.get_volume_type(context, id)
+            req.cache_resource(vol_type, name='types')
         except exception.NotFound:
             msg = _("Volume type not found")
             raise exc.HTTPNotFound(explanation=msg)
 
         return self._view_builder.show(req, vol_type)
 
+    def _parse_is_public(self, is_public):
+        """Parse is_public into something usable.
+
+        * True: List public volume types only
+        * False: List private volume types only
+        * None: List both public and private volume types
+        """
+
+        if is_public is None:
+            # preserve default value of showing only public types
+            return True
+        elif utils.is_none_string(is_public):
+            return None
+        else:
+            try:
+                return strutils.bool_from_string(is_public, strict=True)
+            except ValueError:
+                msg = _('Invalid is_public filter [%s]') % is_public
+                raise exc.HTTPBadRequest(explanation=msg)
+
+    def _get_volume_types(self, req):
+        """Helper function that returns a list of type dicts."""
+        filters = {}
+        context = req.environ['cinder.context']
+        if context.is_admin:
+            # Only admin has query access to all volume types
+            filters['is_public'] = self._parse_is_public(
+                req.params.get('is_public', None))
+        else:
+            filters['is_public'] = True
+        limited_types = volume_types.get_all_types(
+            context, search_opts=filters).values()
+        return limited_types
+
 
 def create_resource():
     return wsgi.Resource(VolumeTypesController())
index 034504717656613a66c6a9f4afedd32d1860535c..36c66b8316f966bea986091d7bccb2cf35c8ab5e 100644 (file)
@@ -369,19 +369,41 @@ def volume_admin_metadata_update(context, volume_id, metadata, delete):
 ##################
 
 
-def volume_type_create(context, values):
+def volume_type_create(context, values, projects=None):
     """Create a new volume type."""
-    return IMPL.volume_type_create(context, values)
+    return IMPL.volume_type_create(context, values, projects)
 
 
-def volume_type_get_all(context, inactive=False):
-    """Get all volume types."""
-    return IMPL.volume_type_get_all(context, inactive)
+def volume_type_get_all(context, inactive=False, filters=None):
+    """Get all volume types.
 
+    :param context: context to query under
+    :param inactive: Include inactive volume types to the result set
+    :param filters: Filters for the query in the form of key/value.
 
-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):
@@ -435,6 +457,21 @@ def volume_get_active_by_window(context, begin, end=None, project_id=None):
     return IMPL.volume_get_active_by_window(context, begin, end, project_id)
 
 
+def volume_type_access_get_all(context, type_id):
+    """Get all volume type access of a volume type."""
+    return IMPL.volume_type_access_get_all(context, type_id)
+
+
+def volume_type_access_add(context, type_id, project_id):
+    """Add volume type access for project."""
+    return IMPL.volume_type_access_add(context, type_id, project_id)
+
+
+def volume_type_access_remove(context, type_id, project_id):
+    """Remove volume type access for project."""
+    return IMPL.volume_type_access_remove(context, type_id, project_id)
+
+
 ####################
 
 
index 7def024a62380b7c907a200be63b158d00e76700..b1dea38c03b08fee49ae07a3d85e0d4e595fc693 100644 (file)
@@ -37,6 +37,7 @@ from sqlalchemy import or_
 from sqlalchemy.orm import joinedload, joinedload_all
 from sqlalchemy.orm import RelationshipProperty
 from sqlalchemy.sql.expression import literal_column
+from sqlalchemy.sql.expression import true
 from sqlalchemy.sql import func
 
 from cinder.common import sqlalchemyutils
@@ -1871,8 +1872,8 @@ def snapshot_metadata_update(context, snapshot_id, metadata, delete):
 
 
 @require_admin_context
-def volume_type_create(context, values):
-    """Create a new instance type.
+def volume_type_create(context, values, projects=None):
+    """Create a new volume type.
 
     In order to pass in extra specs, the values dict should contain a
     'extra_specs' key/value pair:
@@ -1881,6 +1882,8 @@ def volume_type_create(context, values):
     if not values.get('id'):
         values['id'] = str(uuid.uuid4())
 
+    projects = projects or []
+
     session = get_session()
     with session.begin():
         try:
@@ -1901,20 +1904,59 @@ def volume_type_create(context, values):
             session.add(volume_type_ref)
         except Exception as e:
             raise db_exc.DBError(e)
+        for project in set(projects):
+            access_ref = models.VolumeTypeProjects()
+            access_ref.update({"volume_type_id": volume_type_ref.id,
+                               "project_id": project})
+            access_ref.save(session=session)
         return volume_type_ref
 
 
+def _volume_type_get_query(context, session=None, read_deleted=None,
+                           expected_fields=None):
+    expected_fields = expected_fields or []
+    query = model_query(context,
+                        models.VolumeTypes,
+                        session=session,
+                        read_deleted=read_deleted).\
+        options(joinedload('extra_specs'))
+
+    if 'projects' in expected_fields:
+        query = query.options(joinedload('projects'))
+
+    if not context.is_admin:
+        the_filter = [models.VolumeTypes.is_public == true()]
+        projects_attr = getattr(models.VolumeTypes, 'projects')
+        the_filter.extend([
+            projects_attr.any(project_id=context.project_id)
+        ])
+        query = query.filter(or_(*the_filter))
+
+    return query
+
+
 @require_context
 def volume_type_get_all(context, inactive=False, filters=None):
     """Returns a dict describing all volume_types with name as key."""
     filters = filters or {}
 
     read_deleted = "yes" if inactive else "no"
-    rows = model_query(context, models.VolumeTypes,
-                       read_deleted=read_deleted).\
-        options(joinedload('extra_specs')).\
-        order_by("name").\
-        all()
+
+    query = _volume_type_get_query(context, read_deleted=read_deleted)
+
+    if 'is_public' in filters and filters['is_public'] is not None:
+        the_filter = [models.VolumeTypes.is_public == filters['is_public']]
+        if filters['is_public'] and context.project_id is not None:
+            projects_attr = getattr(models.VolumeTypes, 'projects')
+            the_filter.extend([
+                projects_attr.any(project_id=context.project_id, deleted=False)
+            ])
+        if len(the_filter) > 1:
+            query = query.filter(or_(*the_filter))
+        else:
+            query = query.filter(the_filter[0])
+
+    rows = query.order_by("name").all()
 
     result = {}
     for row in rows:
@@ -1923,28 +1965,50 @@ def volume_type_get_all(context, inactive=False, filters=None):
     return result
 
 
+def _volume_type_get_id_from_volume_type_query(context, id, session=None):
+    return model_query(
+        context, models.VolumeTypes.id, read_deleted="no",
+        session=session, base_model=models.VolumeTypes).\
+        filter_by(id=id)
+
+
+def _volume_type_get_id_from_volume_type(context, id, session=None):
+    result = _volume_type_get_id_from_volume_type_query(
+        context, id, session=session).first()
+    if not result:
+        raise exception.VolumeTypeNotFound(volume_type_id=id)
+    return result[0]
+
+
 @require_context
-def _volume_type_get(context, id, session=None, inactive=False):
+def _volume_type_get(context, id, session=None, inactive=False,
+                     expected_fields=None):
+    expected_fields = expected_fields or []
     read_deleted = "yes" if inactive else "no"
-    result = model_query(context,
-                         models.VolumeTypes,
-                         session=session,
-                         read_deleted=read_deleted).\
-        options(joinedload('extra_specs')).\
+    result = _volume_type_get_query(
+        context, session, read_deleted, expected_fields).\
         filter_by(id=id).\
         first()
 
     if not result:
         raise exception.VolumeTypeNotFound(volume_type_id=id)
 
-    return _dict_with_extra_specs(result)
+    vtype = _dict_with_extra_specs(result)
+
+    if 'projects' in expected_fields:
+        vtype['projects'] = [p['project_id'] for p in result['projects']]
+
+    return vtype
 
 
 @require_context
-def volume_type_get(context, id, inactive=False):
+def volume_type_get(context, id, inactive=False, expected_fields=None):
     """Return a dict describing specific volume_type."""
 
-    return _volume_type_get(context, id, None, inactive)
+    return _volume_type_get(context, id,
+                            session=None,
+                            inactive=inactive,
+                            expected_fields=expected_fields)
 
 
 @require_context
@@ -1956,8 +2020,8 @@ def _volume_type_get_by_name(context, name, session=None):
 
     if not result:
         raise exception.VolumeTypeNotFoundByName(volume_type_name=name)
-    else:
-        return _dict_with_extra_specs(result)
+
+    return _dict_with_extra_specs(result)
 
 
 @require_context
@@ -2107,6 +2171,51 @@ def volume_get_active_by_window(context,
     return query.all()
 
 
+def _volume_type_access_query(context, session=None):
+    return model_query(context, models.VolumeTypeProjects, session=session,
+                       read_deleted="no")
+
+
+@require_admin_context
+def volume_type_access_get_all(context, type_id):
+    volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
+    return _volume_type_access_query(context).\
+        filter_by(volume_type_id=volume_type_id).all()
+
+
+@require_admin_context
+def volume_type_access_add(context, type_id, project_id):
+    """Add given tenant to the volume type access list."""
+    volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
+
+    access_ref = models.VolumeTypeProjects()
+    access_ref.update({"volume_type_id": volume_type_id,
+                       "project_id": project_id})
+
+    session = get_session()
+    with session.begin():
+        try:
+            access_ref.save(session=session)
+        except db_exc.DBDuplicateEntry:
+            raise exception.VolumeTypeAccessExists(volume_type_id=type_id,
+                                                   project_id=project_id)
+        return access_ref
+
+
+@require_admin_context
+def volume_type_access_remove(context, type_id, project_id):
+    """Remove given tenant from the volume type access list."""
+    volume_type_id = _volume_type_get_id_from_volume_type(context, type_id)
+
+    count = _volume_type_access_query(context).\
+        filter_by(volume_type_id=volume_type_id).\
+        filter_by(project_id=project_id).\
+        soft_delete(synchronize_session=False)
+    if count == 0:
+        raise exception.VolumeTypeAccessNotFound(
+            volume_type_id=type_id, project_id=project_id)
+
+
 ####################
 
 
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/032_add_volume_type_projects.py b/cinder/db/sqlalchemy/migrate_repo/versions/032_add_volume_type_projects.py
new file mode 100644 (file)
index 0000000..693e4a7
--- /dev/null
@@ -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
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/032_sqlite_downgrade.sql b/cinder/db/sqlalchemy/migrate_repo/versions/032_sqlite_downgrade.sql
new file mode 100644 (file)
index 0000000..ade3dc2
--- /dev/null
@@ -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;
index 2c5b8d079f586667c9e258ccfab4479610526e99..7acb4cacfc48d4fee84ac1fbb092e4df75b3abae 100644 (file)
@@ -203,6 +203,7 @@ class VolumeTypes(BASE, CinderBase):
     # A reference to qos_specs entity
     qos_specs_id = Column(String(36),
                           ForeignKey('quality_of_service_specs.id'))
+    is_public = Column(Boolean, default=True)
     volumes = relationship(Volume,
                            backref=backref('volume_type', uselist=False),
                            foreign_keys=id,
@@ -211,6 +212,27 @@ class VolumeTypes(BASE, CinderBase):
                            'VolumeTypes.deleted == False)')
 
 
+class VolumeTypeProjects(BASE, CinderBase):
+    """Represent projects associated volume_types."""
+    __tablename__ = "volume_type_projects"
+    __table_args__ = (schema.UniqueConstraint(
+        "volume_type_id", "project_id", "deleted",
+        name="uniq_volume_type_projects0volume_type_id0project_id0deleted"),
+    )
+    id = Column(Integer, primary_key=True)
+    volume_type_id = Column(Integer, ForeignKey('volume_types.id'),
+                            nullable=False)
+    project_id = Column(String(255))
+
+    volume_type = relationship(
+        VolumeTypes,
+        backref="projects",
+        foreign_keys=volume_type_id,
+        primaryjoin='and_('
+        'VolumeTypeProjects.volume_type_id == VolumeTypes.id,'
+        'VolumeTypeProjects.deleted == False)')
+
+
 class VolumeTypeExtraSpecs(BASE, CinderBase):
     """Represents additional specs as key/value pairs for a volume_type."""
     __tablename__ = 'volume_type_extra_specs'
index 10f6913cef20a5b15d668ac948c7edc1e7f5d213..5edad3ff835d2e2158865d1537dc3175b5a1a122 100755 (executable)
@@ -271,6 +271,11 @@ class VolumeTypeNotFoundByName(VolumeTypeNotFound):
                 "could not be found.")
 
 
+class VolumeTypeAccessNotFound(NotFound):
+    message = _("Volume type access not found for %(volume_type_id)s / "
+                "%(project_id)s combination.")
+
+
 class VolumeTypeExtraSpecsNotFound(NotFound):
     message = _("Volume Type %(volume_type_id)s has no extra specs with "
                 "key %(extra_specs_key)s.")
@@ -376,6 +381,11 @@ class VolumeTypeExists(Duplicate):
     message = _("Volume Type %(id)s already exists.")
 
 
+class VolumeTypeAccessExists(Duplicate):
+    message = _("Volume type access for %(volume_type_id)s / "
+                "%(project_id)s combination already exists.")
+
+
 class VolumeTypeEncryptionExists(Invalid):
     message = _("Volume type encryption for type %(type_id)s already exists.")
 
index f1a7cae7e3e4f7d8fd7b503b54c2e41590352767..d865314855011703808fcdab7d79d16073f4547c 100644 (file)
@@ -57,7 +57,7 @@ def stub_volume_type_extra_specs():
     return specs
 
 
-def volume_type_get(context, volume_type_id):
+def volume_type_get(context, id, inactive=False, expected_fields=None):
     pass
 
 
index ccca201d16e38fc5c36949c13a0d9631638dbe21..2fd4de350a16d7fc327c4485f60aba26d7223a82 100644 (file)
@@ -50,11 +50,11 @@ def return_volume_types_with_volumes_destroy(context, id):
     pass
 
 
-def return_volume_types_create(context, name, specs):
+def return_volume_types_create(context, name, specs, is_public):
     pass
 
 
-def return_volume_types_create_duplicate_type(context, name, specs):
+def return_volume_types_create_duplicate_type(context, name, specs, is_public):
     raise exception.VolumeTypeExists(id=name)
 
 
diff --git a/cinder/tests/api/contrib/test_volume_type_access.py b/cinder/tests/api/contrib/test_volume_type_access.py
new file mode 100644 (file)
index 0000000..9bb6269
--- /dev/null
@@ -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)
index 844ac9c37d64364f34213846e2d2ef1bf20eb23d..7b8b873cbb31d94ceeffcab3549692c03952fbc2 100644 (file)
@@ -36,13 +36,13 @@ def stub_volume_type(id):
     return dict(id=id, name='vol_type_%s' % str(id), extra_specs=specs)
 
 
-def return_volume_types_get_all_types(context):
+def return_volume_types_get_all_types(context, search_opts=None):
     return dict(vol_type_1=stub_volume_type(1),
                 vol_type_2=stub_volume_type(2),
                 vol_type_3=stub_volume_type(3))
 
 
-def return_empty_volume_types_get_all_types(context):
+def return_empty_volume_types_get_all_types(context, search_opts=None):
     return {}
 
 
index 6d56262a39458bf9e5ad7c27bbd5a0e79b11e85d..8b120a6c37f5b403c582131ca1790d095fcbb43e 100644 (file)
@@ -42,7 +42,7 @@ def stub_volume_type(id):
     )
 
 
-def return_volume_types_get_all_types(context):
+def return_volume_types_get_all_types(context, search_opts=None):
     return dict(
         vol_type_1=stub_volume_type(1),
         vol_type_2=stub_volume_type(2),
@@ -50,7 +50,7 @@ def return_volume_types_get_all_types(context):
     )
 
 
-def return_empty_volume_types_get_all_types(context):
+def return_empty_volume_types_get_all_types(context, search_opts=None):
     return {}
 
 
index 10b7a518362098c5a2f469c50259ea239e36db91..75cc24d75346ebfec2b42077f07549b72138c620 100644 (file)
@@ -45,6 +45,9 @@
     "volume_extension:volume_actions:upload_image": "",
     "volume_extension:types_manage": "",
     "volume_extension:types_extra_specs": "",
+    "volume_extension:volume_type_access": "",
+    "volume_extension:volume_type_access:addProjectAccess": "rule:admin_api",
+    "volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api",
     "volume_extension:volume_type_encryption": "rule:admin_api",
     "volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
     "volume_extension:qos_specs_manage": "",
index fef77e34fe16e59cbc28bade18fa464e89e5832e..11890439f99655db9de7c48b298085c83bdc8242 100644 (file)
@@ -1298,3 +1298,53 @@ class TestMigrations(test.TestCase):
                 execute().scalar()
 
             self.assertEqual(4, num_defaults)
+
+    def test_migration_032(self):
+        """Test adding volume_type_projects table works correctly."""
+        for (key, engine) in self.engines.items():
+            migration_api.version_control(engine,
+                                          TestMigrations.REPOSITORY,
+                                          migration.db_initial_version())
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 31)
+            metadata = sqlalchemy.schema.MetaData()
+            metadata.bind = engine
+
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 32)
+
+            self.assertTrue(engine.dialect.has_table(engine.connect(),
+                                                     "volume_type_projects"))
+
+            volume_type_projects = sqlalchemy.Table('volume_type_projects',
+                                                    metadata,
+                                                    autoload=True)
+            self.assertIsInstance(volume_type_projects.c.created_at.type,
+                                  self.time_type[engine.name])
+            self.assertIsInstance(volume_type_projects.c.updated_at.type,
+                                  self.time_type[engine.name])
+            self.assertIsInstance(volume_type_projects.c.deleted_at.type,
+                                  self.time_type[engine.name])
+            self.assertIsInstance(volume_type_projects.c.deleted.type,
+                                  self.bool_type[engine.name])
+            self.assertIsInstance(volume_type_projects.c.id.type,
+                                  sqlalchemy.types.INTEGER)
+            self.assertIsInstance(volume_type_projects.c.volume_type_id.type,
+                                  sqlalchemy.types.VARCHAR)
+            self.assertIsInstance(volume_type_projects.c.project_id.type,
+                                  sqlalchemy.types.VARCHAR)
+
+            volume_types = sqlalchemy.Table('volume_types',
+                                            metadata,
+                                            autoload=True)
+            self.assertIsInstance(volume_types.c.is_public.type,
+                                  self.bool_type[engine.name])
+
+            migration_api.downgrade(engine, TestMigrations.REPOSITORY, 31)
+            metadata = sqlalchemy.schema.MetaData()
+            metadata.bind = engine
+
+            self.assertFalse(engine.dialect.has_table(engine.connect(),
+                                                      "volume_type_projects"))
+            volume_types = sqlalchemy.Table('volume_types',
+                                            metadata,
+                                            autoload=True)
+            self.assertNotIn('is_public', volume_types.c)
index 9e962f83698f67be40473ae2e21272fc33cc6e5b..59562acacba2f02a3a45ec89d1eb367d4947fde5 100644 (file)
@@ -229,6 +229,24 @@ class VolumeTypeTestCase(test.TestCase):
                                              encryption)
         self.assertTrue(volume_types.is_encrypted(self.ctxt, volume_type_id))
 
+    def test_add_access(self):
+        project_id = '456'
+        vtype = volume_types.create(self.ctxt, 'type1')
+        vtype_id = vtype.get('id')
+
+        volume_types.add_volume_type_access(self.ctxt, vtype_id, project_id)
+        vtype_access = db.volume_type_access_get_all(self.ctxt, vtype_id)
+        self.assertIn(project_id, [a.project_id for a in vtype_access])
+
+    def test_remove_access(self):
+        project_id = '456'
+        vtype = volume_types.create(self.ctxt, 'type1', projects=['456'])
+        vtype_id = vtype.get('id')
+
+        volume_types.remove_volume_type_access(self.ctxt, vtype_id, project_id)
+        vtype_access = db.volume_type_access_get_all(self.ctxt, vtype_id)
+        self.assertNotIn(project_id, vtype_access)
+
     def test_get_volume_type_qos_specs(self):
         qos_ref = qos_specs.create(self.ctxt, 'qos-specs-1', {'k1': 'v1',
                                                               'k2': 'v2',
index eca0d9121144487ef6e2a3dd4ad439699e0dc134..cf912f9f828631f106c9cc249afb67040f06fd3f 100644 (file)
@@ -414,6 +414,14 @@ def is_valid_boolstr(val):
             val == '1' or val == '0')
 
 
+def is_none_string(val):
+    """Check if a string represents a None value."""
+    if not isinstance(val, six.string_types):
+        return False
+
+    return val.lower() == 'none'
+
+
 def monkey_patch():
     """If the CONF.monkey_patch set as True,
     this function patches a decorator
index d2841aae014eebe8f2af7a1fe7220c85cf1e4a68..72c4e965c702bcd5347b9276bdaeff0ddb0cddf9 100644 (file)
@@ -34,13 +34,16 @@ CONF = cfg.CONF
 LOG = logging.getLogger(__name__)
 
 
-def create(context, name, extra_specs=None):
+def create(context, name, extra_specs=None, is_public=True, projects=None):
     """Creates volume types."""
     extra_specs = extra_specs or {}
+    projects = projects or []
     try:
         type_ref = db.volume_type_create(context,
                                          dict(name=name,
-                                              extra_specs=extra_specs))
+                                              extra_specs=extra_specs,
+                                              is_public=is_public),
+                                         projects=projects)
     except db_exc.DBError as e:
         LOG.exception(_LE('DB error: %s') % e)
         raise exception.VolumeTypeCreateFailed(name=name,
@@ -64,7 +67,13 @@ def get_all_types(context, inactive=0, search_opts=None):
 
     """
     search_opts = search_opts or {}
-    vol_types = db.volume_type_get_all(context, inactive)
+    filters = {}
+
+    if 'is_public' in search_opts:
+        filters['is_public'] = search_opts['is_public']
+        del search_opts['is_public']
+
+    vol_types = db.volume_type_get_all(context, inactive, filters=filters)
 
     if search_opts:
         LOG.debug("Searching by: %s" % search_opts)
@@ -96,7 +105,7 @@ def get_all_types(context, inactive=0, search_opts=None):
     return vol_types
 
 
-def get_volume_type(ctxt, id):
+def get_volume_type(ctxt, id, expected_fields=None):
     """Retrieves single volume type by id."""
     if id is None:
         msg = _("id cannot be None")
@@ -105,7 +114,7 @@ def get_volume_type(ctxt, id):
     if ctxt is None:
         ctxt = context.get_admin_context()
 
-    return db.volume_type_get(ctxt, id)
+    return db.volume_type_get(ctxt, id, expected_fields=expected_fields)
 
 
 def get_volume_type_by_name(context, name):
@@ -149,6 +158,22 @@ def get_volume_type_extra_specs(volume_type_id, key=False):
         return extra_specs
 
 
+def add_volume_type_access(context, volume_type_id, project_id):
+    """Add access to volume type for project_id."""
+    if volume_type_id is None:
+        msg = _("volume_type_id cannot be None")
+        raise exception.InvalidVolumeType(reason=msg)
+    return db.volume_type_access_add(context, volume_type_id, project_id)
+
+
+def remove_volume_type_access(context, volume_type_id, project_id):
+    """Remove access to volume type for project_id."""
+    if volume_type_id is None:
+        msg = _("volume_type_id cannot be None")
+        raise exception.InvalidVolumeType(reason=msg)
+    return db.volume_type_access_remove(context, volume_type_id, project_id)
+
+
 def is_encrypted(context, volume_type_id):
     if volume_type_id is None:
         return False
index ab7fdda10acea4a699040f2c2dd84af433262ef6..36816060f96d838c70f5e0ec34d0d39c1fc78915 100644 (file)
@@ -21,6 +21,9 @@
 
     "volume_extension:types_manage": "rule:admin_api",
     "volume_extension:types_extra_specs": "rule:admin_api",
+    "volume_extension:volume_type_access": "",
+    "volume_extension:volume_type_access:addProjectAccess": "rule:admin_api",
+    "volume_extension:volume_type_access:removeProjectAccess": "rule:admin_api",
     "volume_extension:volume_type_encryption": "rule:admin_api",
     "volume_extension:volume_encryption_metadata": "rule:admin_or_owner",
     "volume_extension:extended_snapshot_attributes": "",