]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Implement QoS support for volumes
authorZhiteng Huang <zhiteng.huang@intel.com>
Sat, 18 May 2013 14:21:28 +0000 (22:21 +0800)
committerZhiteng Huang <zhithuang@ebaysf.com>
Fri, 23 Aug 2013 02:21:46 +0000 (10:21 +0800)
This patch is to implement generic Quality-of-Service(QoS) support for volumes.
The goal is to add an interface so that cloud/Cinder admins can use to set
volume QoS, which can be enforced either in hypervisor or on Cinder back-end
or both. QoS specifications are added as a standalone (only visible to admin)
entity.  So admin can create/update/delete and associate/disassociate QoS
specifications to other entities, in this case volume types.

Note that while it's possible for Cinder to set the granularity of QoS control
to every single volume, this patch puts the control granularity to the level
of volumes of the same type to minimize the impact of other Cinder parts.
In other words, the design is to bond QoS with volume types. So Cinder admin
can associate volume types with QoS specifications, and volumes of same volume
type share the same QoS specifications.

QoS can mean a lot different things that it's unlikely we can come up with a
interpretation that all vendors can agree on.  So the approach this
implementation takes is to make Quality-of-Service specs as free-from, i.e.
expressed as key/value pairs.

Changes:
 - Add a quality_of_service_specs table, using adjacency list relation to store
 a specs entry and its detailed specs in key/values. Note that to be able to
 distinguish where should the QoS specs be consumed, each QoS specs entity
 will have a 'consumer' (i.e. fixed key) with the value of where admin would
 like the QoS policy to be enforced/consumed, currently these three values are
 considered valid: 'front-end' (Nova Compute), 'back-end' (Cinder back-end),
 'both'. The default value for 'consumer' is 'back-end';
 - Add a new API extension 'qos_specs_manage' to allow list/create/update/
 delete/associate/disassociate of QoS specs;
 - Add volume/qos_specs internal API for qos specs manipulation;
 - Add 'qos_specs' info to data structure when
 initialize_connection() is called.
 - Add 'qos_specs' to request_specs and filter properties for
 a volume create request.

 TODO
 - Modify 'type_manage' API extension to be able to accept qos info.
 - Modify volume_types.create() to accept qos info and do the checks.

DocImpact

implement blueprint: pass-ratelimit-info-to-nova

Change-Id: Iabc61b941aaff10395b30e2045e3421369a317e2

17 files changed:
cinder/api/contrib/qos_specs_manage.py [new file with mode: 0644]
cinder/db/api.py
cinder/db/sqlalchemy/api.py
cinder/db/sqlalchemy/migrate_repo/versions/018_add_qos_specs.py [new file with mode: 0644]
cinder/db/sqlalchemy/models.py
cinder/exception.py
cinder/scheduler/filter_scheduler.py
cinder/tests/api/contrib/test_qos_specs_manage.py [new file with mode: 0644]
cinder/tests/db/test_qos_specs.py [new file with mode: 0644]
cinder/tests/policy.json
cinder/tests/test_migrations.py
cinder/tests/test_qos_specs.py [new file with mode: 0644]
cinder/tests/test_volume_types.py
cinder/volume/flows/create_volume.py
cinder/volume/manager.py
cinder/volume/qos_specs.py [new file with mode: 0644]
cinder/volume/volume_types.py

diff --git a/cinder/api/contrib/qos_specs_manage.py b/cinder/api/contrib/qos_specs_manage.py
new file mode 100644 (file)
index 0000000..425b46d
--- /dev/null
@@ -0,0 +1,376 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 eBay Inc.
+# Copyright (c) 2013 OpenStack LLC.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""The QoS specs extension"""
+
+import webob
+
+from cinder.api import extensions
+from cinder.api.openstack import wsgi
+from cinder.api import xmlutil
+from cinder import db
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder.openstack.common.notifier import api as notifier_api
+from cinder.volume import qos_specs
+from cinder.volume import volume_types
+
+
+LOG = logging.getLogger(__name__)
+
+authorize = extensions.extension_authorizer('volume', 'qos_specs_manage')
+
+
+class QoSSpecsTemplate(xmlutil.TemplateBuilder):
+    def construct(self):
+        root = xmlutil.make_flat_dict('qos_specs', selector='qos_specs')
+        return xmlutil.MasterTemplate(root, 1)
+
+
+class QoSSpecTemplate(xmlutil.TemplateBuilder):
+    # FIXME(zhiteng) Need to handle consumer
+    def construct(self):
+        tagname = xmlutil.Selector('key')
+
+        def qosspec_sel(obj, do_raise=False):
+            # Have to extract the key and value for later use...
+            key, value = obj.items()[0]
+            return dict(key=key, value=value)
+
+        root = xmlutil.TemplateElement(tagname, selector=qosspec_sel)
+        root.text = 'value'
+        return xmlutil.MasterTemplate(root, 1)
+
+
+def _check_specs(context, specs_id):
+    try:
+        qos_specs.get_qos_specs(context, specs_id)
+    except exception.NotFound as ex:
+        raise webob.exc.HTTPNotFound(explanation=unicode(ex))
+
+
+class QoSSpecsController(wsgi.Controller):
+    """The volume type extra specs API controller for the OpenStack API."""
+
+    @staticmethod
+    def _notify_qos_specs_error(context, method, payload):
+        notifier_api.notify(context,
+                            'QoSSpecs',
+                            method,
+                            notifier_api.ERROR,
+                            payload)
+
+    @wsgi.serializers(xml=QoSSpecsTemplate)
+    def index(self, req):
+        """Returns the list of qos_specs."""
+        context = req.environ['cinder.context']
+        authorize(context)
+        specs = qos_specs.get_all_specs(context)
+        return specs
+
+    @wsgi.serializers(xml=QoSSpecsTemplate)
+    def create(self, req, body=None):
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        if not self.is_valid_body(body, 'qos_specs'):
+            raise webob.exc.HTTPBadRequest()
+
+        specs = body['qos_specs']
+        name = specs.get('name', None)
+        if name is None or name == "":
+            msg = _("Please specify a name for QoS specs.")
+            raise webob.exc.HTTPBadRequest(explanation=msg)
+
+        try:
+            specs_ref = qos_specs.create(context, name, specs)
+            qos_specs.get_qos_specs_by_name(context, name)
+            notifier_info = dict(name=name, specs=specs)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'QoSSpecs.create',
+                                notifier_api.INFO, notifier_info)
+        except exception.InvalidInput as err:
+            notifier_err = dict(name=name, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.create',
+                                         notifier_err)
+            raise webob.exc.HTTPBadRequest(explanation=str(err))
+        except exception.QoSSpecsExists as err:
+            notifier_err = dict(name=name, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.create',
+                                         notifier_err)
+            raise webob.exc.HTTPConflict(explanation=str(err))
+        except exception.QoSSpecsCreateFailed as err:
+            notifier_err = dict(name=name, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.create',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=str(err))
+
+        return body
+
+    @wsgi.serializers(xml=QoSSpecsTemplate)
+    def update(self, req, id, body=None):
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        if not self.is_valid_body(body, 'qos_specs'):
+            raise webob.exc.HTTPBadRequest()
+        specs = body['qos_specs']
+        try:
+            qos_specs.update(context, id, specs)
+            notifier_info = dict(id=id, specs=specs)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.update',
+                                notifier_api.INFO, notifier_info)
+        except exception.QoSSpecsNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.update',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=str(err))
+        except exception.InvalidQoSSpecs as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.update',
+                                         notifier_err)
+            raise webob.exc.HTTPBadRequest(explanation=str(err))
+        except exception.QoSSpecsUpdateFailed as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.update',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=str(err))
+        return body
+
+    @wsgi.serializers(xml=QoSSpecsTemplate)
+    def show(self, req, id):
+        """Return a single qos spec item."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        try:
+            spec = qos_specs.get_qos_specs(context, id)
+        except exception.NotFound:
+            raise webob.exc.HTTPNotFound()
+
+        return spec
+
+    def delete(self, req, id):
+        """Deletes an existing qos specs."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        force = req.params.get('force', None)
+
+        LOG.debug("qos_specs_manage.delete(): id: %s, force: %s" % (id, force))
+
+        try:
+            qos_specs.get_qos_specs(context, id)
+            qos_specs.delete(context, id, force)
+            notifier_info = dict(id=id)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.delete',
+                                notifier_api.INFO, notifier_info)
+        except exception.NotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.delete',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound()
+        except exception.QoSSpecsInUse as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.delete',
+                                         notifier_err)
+            if force:
+                msg = _('Failed to disassociate qos specs.')
+                raise webob.exc.HTTPInternalServerError(explanation=msg)
+            msg = _('Qos specs still in use.')
+            raise webob.exc.HTTPBadRequest(explanation=msg)
+
+        return webob.Response(status_int=202)
+
+    @wsgi.serializers(xml=QoSSpecsTemplate)
+    def associations(self, req, id):
+        """List all associations of given qos specs."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        LOG.debug("assocications(): id: %s" % id)
+
+        try:
+            associates = qos_specs.get_associations(context, id)
+            notifier_info = dict(id=id)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.associations',
+                                notifier_api.INFO, notifier_info)
+        except exception.QoSSpecsNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.associations',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.CinderException as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.associations',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=err)
+
+        return associates
+
+    def associate(self, req, id):
+        """Associate a qos specs with a volume type."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        type_id = req.params.get('vol_type_id', None)
+
+        if not type_id:
+            msg = _('Volume Type id must not be None.')
+            notifier_err = dict(id=id, error_message=msg)
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.delete',
+                                         notifier_err)
+            raise webob.exc.HTTPBadRequest(explanation=msg)
+        LOG.debug("associcate(): id: %s, type_id: %s" % (id, type_id))
+
+        try:
+            qos_specs.get_qos_specs(context, id)
+            qos_specs.associate_qos_with_type(context, id, type_id)
+            notifier_info = dict(id=id, type_id=type_id)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.associate',
+                                notifier_api.INFO, notifier_info)
+        except exception.VolumeTypeNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.associate',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.QoSSpecsNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.associate',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.QoSSpecsAssociateFailed as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.associate',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=err)
+
+        return webob.Response(status_int=202)
+
+    def disassociate(self, req, id):
+        """Disassociate a qos specs from a volume type."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        type_id = req.params.get('vol_type_id', None)
+
+        if not type_id:
+            msg = _('Volume Type id must not be None.')
+            notifier_err = dict(id=id, error_message=msg)
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.delete',
+                                         notifier_err)
+            raise webob.exc.HTTPBadRequest(explanation=msg)
+        LOG.debug("disassocicate(): id: %s, type_id: %s" % (id, type_id))
+
+        try:
+            qos_specs.get_qos_specs(context, id)
+            qos_specs.disassociate_qos_specs(context, id, type_id)
+            notifier_info = dict(id=id, type_id=type_id)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.disassociate',
+                                notifier_api.INFO, notifier_info)
+        except exception.VolumeTypeNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.disassociate',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.QoSSpecsNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.disassociate',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.QoSSpecsDisassociateFailed as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.disassociate',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=err)
+
+        return webob.Response(status_int=202)
+
+    def disassociate_all(self, req, id):
+        """Disassociate a qos specs from all volume types."""
+        context = req.environ['cinder.context']
+        authorize(context)
+
+        LOG.debug("disassocicate_all(): id: %s" % id)
+
+        try:
+            qos_specs.get_qos_specs(context, id)
+            qos_specs.disassociate_all(context, id)
+            notifier_info = dict(id=id)
+            notifier_api.notify(context, 'QoSSpecs',
+                                'qos_specs.disassociate_all',
+                                notifier_api.INFO, notifier_info)
+        except exception.QoSSpecsNotFound as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.disassociate_all',
+                                         notifier_err)
+            raise webob.exc.HTTPNotFound(explanation=err)
+        except exception.QoSSpecsDisassociateFailed as err:
+            notifier_err = dict(id=id, error_message=str(err))
+            self._notify_qos_specs_error(context,
+                                         'qos_specs.disassociate_all',
+                                         notifier_err)
+            raise webob.exc.HTTPInternalServerError(explanation=err)
+
+        return webob.Response(status_int=202)
+
+
+class Qos_specs_manage(extensions.ExtensionDescriptor):
+    """QoS specs support"""
+
+    name = "Qos_specs_manage"
+    alias = "qos-specs"
+    namespace = "http://docs.openstack.org/volume/ext/qos-specs/api/v1"
+    updated = "2013-08-02T00:00:00+00:00"
+
+    def get_resources(self):
+        resources = []
+        res = extensions.ResourceExtension(
+            Qos_specs_manage.alias,
+            QoSSpecsController(),
+            member_actions={"associations": "GET",
+                            "associate": "GET",
+                            "disassociate": "GET",
+                            "disassociate_all": "GET"})
+
+        resources.append(res)
+
+        return resources
index 54616635cfb0fe4d3b4a30b7c927573274d2f41e..0f7d3322d933f4cd6e833491b5475a2de035e55e 100644 (file)
@@ -365,6 +365,34 @@ def volume_type_get_by_name(context, name):
     return IMPL.volume_type_get_by_name(context, name)
 
 
+def volume_type_qos_associations_get(context, qos_specs_id, inactive=False):
+    """Get volume types that are associated with specific qos specs."""
+    return IMPL.volume_type_qos_associations_get(context,
+                                                 qos_specs_id,
+                                                 inactive)
+
+
+def volume_type_qos_associate(context, type_id, qos_specs_id):
+    """Associate a volume type with specific qos specs."""
+    return IMPL.volume_type_qos_associate(context, type_id, qos_specs_id)
+
+
+def volume_type_qos_disassociate(context, qos_specs_id, type_id):
+    """Disassociate a volume type from specific qos specs."""
+    return IMPL.volume_type_qos_disassociate(context, qos_specs_id, type_id)
+
+
+def volume_type_qos_disassociate_all(context, qos_specs_id):
+    """Disassociate all volume types from specific qos specs."""
+    return IMPL.volume_type_qos_disassociate_all(context,
+                                                 qos_specs_id)
+
+
+def volume_type_qos_specs_get(context, type_id):
+    """Get all qos specs for given volume type."""
+    return IMPL.volume_type_qos_specs_get(context, type_id)
+
+
 def volume_type_destroy(context, id):
     """Delete a volume type."""
     return IMPL.volume_type_destroy(context, id)
@@ -426,11 +454,65 @@ def volume_type_encryption_volume_get(context, volume_type_id, session=None):
                                                   session)
 
 
+def volume_encryption_metadata_get(context, volume_id, session=None):
+    return IMPL.volume_encryption_metadata_get(context, volume_id, session)
+
+
 ###################
 
 
-def volume_encryption_metadata_get(context, volume_id, session=None):
-    return IMPL.volume_encryption_metadata_get(context, volume_id, session)
+def qos_specs_create(context, values):
+    """Create a qos_specs."""
+    return IMPL.qos_specs_create(context, values)
+
+
+def qos_specs_get(context, qos_specs_id):
+    """Get all specification for a given qos_specs."""
+    return IMPL.qos_specs_get(context, qos_specs_id)
+
+
+def qos_specs_get_all(context, inactive=False, filters=None):
+    """Get all qos_specs."""
+    return IMPL.qos_specs_get_all(context, inactive, filters)
+
+
+def qos_specs_get_by_name(context, name):
+    """Get all specification for a given qos_specs."""
+    return IMPL.qos_specs_get_by_name(context, name)
+
+
+def qos_specs_associations_get(context, qos_specs_id):
+    """Get all associated volume types for a given qos_specs."""
+    return IMPL.qos_specs_associations_get(context, qos_specs_id)
+
+
+def qos_specs_associate(context, qos_specs_id, type_id):
+    """Associate qos_specs from volume type."""
+    return IMPL.qos_specs_associate(context, qos_specs_id, type_id)
+
+
+def qos_specs_disassociate(context, qos_specs_id, type_id):
+    """Disassociate qos_specs from volume type."""
+    return IMPL.qos_specs_disassociate(context, qos_specs_id, type_id)
+
+
+def qos_specs_disassociate_all(context, qos_specs_id):
+    """Disassociate qos_specs from all entities."""
+    return IMPL.qos_specs_disassociate_all(context, qos_specs_id)
+
+
+def qos_specs_delete(context, qos_specs_id):
+    """Delete the qos_specs."""
+    IMPL.qos_specs_delete(context, qos_specs_id)
+
+
+def qos_specs_update(context, qos_specs_id, specs):
+    """Update qos specs.
+
+    This adds or modifies the key/value pairs specified in the
+    specs dict argument for a given qos_specs.
+    """
+    IMPL.qos_specs_update(context, qos_specs_id, specs)
 
 
 ###################
index 307a895fa1beae33b9b570bc270aa789e41acbe5..b6c42a6675b6256f08626775a0428c5ea8964ca4 100644 (file)
@@ -28,7 +28,7 @@ import warnings
 from oslo.config import cfg
 from sqlalchemy.exc import IntegrityError
 from sqlalchemy import or_
-from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import joinedload, joinedload_all
 from sqlalchemy.sql.expression import literal_column
 from sqlalchemy.sql import func
 
@@ -430,16 +430,15 @@ def _metadata_refs(metadata_dict, meta_class):
 
 
 def _dict_with_extra_specs(inst_type_query):
-    """Takes an instance, volume, or instance type query returned
-    by sqlalchemy and returns it as a dictionary, converting the
-    extra_specs entry from a list of dicts:
+    """Convert type query result to dict with extra_spec and rate_limit.
 
-    'extra_specs' : [{'key': 'k1', 'value': 'v1', ...}, ...]
+    Takes a volume type query returned by sqlalchemy and returns it
+    as a dictionary, converting the extra_specs entry from a list
+    of dicts:
 
+    'extra_specs' : [{'key': 'k1', 'value': 'v1', ...}, ...]
     to a single dict:
-
     'extra_specs' : {'k1': 'v1'}
-
     """
     inst_type_dict = dict(inst_type_query)
     extra_specs = dict([(x['key'], x['value'])
@@ -1587,11 +1586,11 @@ def snapshot_metadata_update(context, snapshot_id, metadata, delete):
 
 @require_admin_context
 def volume_type_create(context, values):
-    """Create a new instance type. In order to pass in extra specs,
-    the values dict should contain a 'extra_specs' key/value pair:
+    """Create a new instance type.
 
+    In order to pass in extra specs, the values dict should contain a
+    'extra_specs' key/value pair:
     {'extra_specs' : {'k1': 'v1', 'k2': 'v2', ...}}
-
     """
     if not values.get('id'):
         values['id'] = str(uuid.uuid4())
@@ -1633,8 +1632,6 @@ def volume_type_get_all(context, inactive=False, filters=None):
         order_by("name").\
         all()
 
-    # TODO(sirp): this patern of converting rows to a result with extra_specs
-    # is repeated quite a bit, might be worth creating a method for it
     result = {}
     for row in rows:
         result[row['name']] = _dict_with_extra_specs(row)
@@ -1686,6 +1683,88 @@ def volume_type_get_by_name(context, name):
     return _volume_type_get_by_name(context, name)
 
 
+@require_admin_context
+def volume_type_qos_associations_get(context, qos_specs_id, inactive=False):
+    read_deleted = "yes" if inactive else "no"
+    return model_query(context, models.VolumeTypes,
+                       read_deleted=read_deleted). \
+        filter_by(qos_specs_id=qos_specs_id).all()
+
+
+@require_admin_context
+def volume_type_qos_associate(context, type_id, qos_specs_id):
+    session = get_session()
+    with session.begin():
+        _volume_type_get(context, type_id, session)
+
+        session.query(models.VolumeTypes). \
+            filter_by(id=type_id). \
+            update({'qos_specs_id': qos_specs_id,
+                    'updated_at': timeutils.utcnow()})
+
+
+@require_admin_context
+def volume_type_qos_disassociate(context, qos_specs_id, type_id):
+    """Disassociate volume type from qos specs."""
+    session = get_session()
+    with session.begin():
+        _volume_type_get(context, type_id, session)
+
+        session.query(models.VolumeTypes). \
+            filter_by(id=type_id). \
+            filter_by(qos_specs_id=qos_specs_id). \
+            update({'qos_specs_id': None,
+                    'updated_at': timeutils.utcnow()})
+
+
+@require_admin_context
+def volume_type_qos_disassociate_all(context, qos_specs_id):
+    """Disassociate all volume types associated with specified qos specs."""
+    session = get_session()
+    with session.begin():
+        session.query(models.VolumeTypes). \
+            filter_by(qos_specs_id=qos_specs_id). \
+            update({'qos_specs_id': None,
+                    'updated_at': timeutils.utcnow()})
+
+
+@require_admin_context
+def volume_type_qos_specs_get(context, type_id):
+    """Return all qos specs for given volume type.
+
+    result looks like:
+        {
+         'qos_specs':
+                     {
+                        'id': 'qos-specs-id',
+                        'name': 'qos_specs_name',
+                        'consumer': 'Consumer',
+                        'key1': 'value1',
+                        'key2': 'value2',
+                        'key3': 'value3'
+                     }
+        }
+
+    """
+    session = get_session()
+    with session.begin():
+        _volume_type_get(context, type_id, session)
+
+        row = session.query(models.VolumeTypes). \
+            options(joinedload('qos_specs')). \
+            filter_by(id=type_id). \
+            first()
+
+        # row.qos_specs is a list of QualityOfServiceSpecs ref
+        specs = {}
+        for item in row.qos_specs:
+            if item.key == 'QoS_Specs_Name':
+                if item.specs:
+                    specs = _dict_with_children_specs(item.specs)
+
+        return {'qos_specs': specs}
+
+
 @require_admin_context
 def volume_type_destroy(context, id):
     session = get_session()
@@ -1799,6 +1878,270 @@ def volume_type_extra_specs_update_or_create(context, volume_type_id,
 ####################
 
 
+@require_admin_context
+def qos_specs_create(context, values):
+    """Create a new QoS specs.
+
+    :param values dictionary that contains specifications for QoS
+          e.g. {'name': 'Name',
+                'qos_specs': {
+                    'consumer': 'front-end',
+                    'total_iops_sec': 1000,
+                    'total_bytes_sec': 1024000
+                    }
+                }
+    """
+    specs_id = str(uuid.uuid4())
+
+    session = get_session()
+    with session.begin():
+        try:
+            _qos_specs_get_by_name(context, values['name'], session)
+            raise exception.QoSSpecsExists(specs_id=values['name'])
+        except exception.QoSSpecsNotFound:
+            pass
+        try:
+            # Insert a root entry for QoS specs
+            specs_root = models.QualityOfServiceSpecs()
+            root = dict(id=specs_id)
+            # 'QoS_Specs_Name' is a internal reserved key to store
+            # the name of QoS specs
+            root['key'] = 'QoS_Specs_Name'
+            root['value'] = values['name']
+            LOG.debug("qos_specs_create(): root %s", root)
+            specs_root.update(root)
+            specs_root.save(session=session)
+
+            # Insert all specification entries for QoS specs
+            for k, v in values['qos_specs'].iteritems():
+                item = dict(key=k, value=v, specs_id=specs_id)
+                item['id'] = str(uuid.uuid4())
+                spec_entry = models.QualityOfServiceSpecs()
+                spec_entry.update(item)
+                spec_entry.save(session=session)
+        except Exception as e:
+            raise db_exc.DBError(e)
+
+        return specs_root
+
+
+@require_admin_context
+def _qos_specs_get_by_name(context, name, session=None, inactive=False):
+    read_deleted = 'yes' if inactive else 'no'
+    results = model_query(context, models.QualityOfServiceSpecs,
+                          read_deleted=read_deleted, session=session). \
+        filter_by(key='QoS_Specs_Name'). \
+        filter_by(value=name). \
+        options(joinedload('specs')).all()
+
+    if not results:
+        raise exception.QoSSpecsNotFound(specs_id=name)
+
+    return results
+
+
+@require_admin_context
+def _qos_specs_get_ref(context, qos_specs_id, session=None, inactive=False):
+    read_deleted = 'yes' if inactive else 'no'
+    result = model_query(context, models.QualityOfServiceSpecs,
+                         read_deleted=read_deleted, session=session). \
+        filter_by(id=qos_specs_id). \
+        options(joinedload_all('specs')).all()
+
+    if not result:
+        raise exception.QoSSpecsNotFound(specs_id=qos_specs_id)
+
+    return result
+
+
+def _dict_with_children_specs(specs):
+    """Convert specs list to a dict."""
+    result = {}
+    for spec in specs:
+        result.update({spec['key']: spec['value']})
+
+    return result
+
+
+def _dict_with_qos_specs(rows):
+    """Convert qos specs query results to dict with name as key.
+
+    Qos specs query results are a list of quality_of_service_specs refs,
+    some are root entry of a qos specs (key == 'QoS_Specs_Name') and the
+    rest are children entry, a.k.a detailed specs for a qos specs. This
+    funtion converts query results to a dict using spec name as key.
+    """
+    result = {}
+    for row in rows:
+        if row['key'] == 'QoS_Specs_Name':
+            result[row['value']] = dict(id=row['id'])
+            if row.specs:
+                spec_dict = _dict_with_children_specs(row.specs)
+                result[row['value']].update(spec_dict)
+
+    return result
+
+
+@require_admin_context
+def qos_specs_get(context, qos_specs_id, inactive=False):
+    rows = _qos_specs_get_ref(context, qos_specs_id, None, inactive)
+
+    return _dict_with_qos_specs(rows)
+
+
+@require_admin_context
+def qos_specs_get_all(context, inactive=False, filters=None):
+    """Returns dicts describing all qos_specs.
+
+    Results is like:
+        {'qos-spec-1': {'id': SPECS-UUID,
+                        'key1': 'value1',
+                        'key2': 'value2',
+                        ...
+                        'consumer': 'back-end'}
+         'qos-spec-2': {'id': SPECS-UUID,
+                        'key1': 'value1',
+                        'key2': 'value2',
+                        ...
+                        'consumer': 'back-end'}
+        }
+    """
+    filters = filters or {}
+    #TODO(zhiteng) Add filters for 'consumer'
+
+    read_deleted = "yes" if inactive else "no"
+    rows = model_query(context, models.QualityOfServiceSpecs,
+                       read_deleted=read_deleted). \
+        options(joinedload_all('specs')).all()
+
+    return _dict_with_qos_specs(rows)
+
+
+@require_admin_context
+def qos_specs_get_by_name(context, name, inactive=False):
+    rows = _qos_specs_get_by_name(context, name, None, inactive)
+
+    return _dict_with_qos_specs(rows)
+
+
+@require_admin_context
+def qos_specs_associations_get(context, qos_specs_id):
+    """Return all entities associated with specified qos specs.
+
+    For now, the only entity that is possible to associate with
+    a qos specs is volume type, so this is just a wrapper of
+    volume_type_qos_associations_get(). But it's possible to
+    extend qos specs association to other entities, such as volumes,
+    sometime in future.
+    """
+    rows = _qos_specs_get_ref(context, qos_specs_id, None)
+    if not rows:
+        raise exception.QoSSpecsNotFound(specs_id=qos_specs_id)
+
+    return volume_type_qos_associations_get(context, qos_specs_id)
+
+
+@require_admin_context
+def qos_specs_associate(context, qos_specs_id, type_id):
+    """Associate volume type from specified qos specs."""
+    return volume_type_qos_associate(context, type_id, qos_specs_id)
+
+
+@require_admin_context
+def qos_specs_disassociate(context, qos_specs_id, type_id):
+    """Disassociate volume type from specified qos specs."""
+    return volume_type_qos_disassociate(context, qos_specs_id, type_id)
+
+
+@require_admin_context
+def qos_specs_disassociate_all(context, qos_specs_id):
+    """Disassociate all entities associated with specified qos specs.
+
+    For now, the only entity that is possible to associate with
+    a qos specs is volume type, so this is just a wrapper of
+    volume_type_qos_disassociate_all(). But it's possible to
+    extend qos specs association to other entities, such as volumes,
+    sometime in future.
+    """
+    return volume_type_qos_disassociate_all(context, qos_specs_id)
+
+
+@require_admin_context
+def qos_specs_item_delete(context, qos_specs_id, key):
+    _qos_specs_get_item(context, qos_specs_id, key)
+    _qos_specs_get_ref(context, qos_specs_id, None). \
+        filter_by(key=key). \
+        update({'deleted': True,
+                'deleted_at': timeutils.utcnow(),
+                'updated_at': literal_column('updated_at')})
+
+
+@require_admin_context
+def qos_specs_delete(context, qos_specs_id):
+    session = get_session()
+    with session.begin():
+        _qos_specs_get_ref(context, qos_specs_id, session)
+        session.query(models.QualityOfServiceSpecs).\
+            filter(or_(models.QualityOfServiceSpecs.id == qos_specs_id,
+                       models.QualityOfServiceSpecs.specs_id ==
+                       qos_specs_id)).\
+            update({'deleted': True,
+                    'deleted_at': timeutils.utcnow(),
+                    'updated_at': literal_column('updated_at')})
+
+
+@require_admin_context
+def _qos_specs_get_item(context, qos_specs_id, key, session=None):
+    result = model_query(context, models.QualityOfServiceSpecs,
+                         session=session). \
+        filter(models.QualityOfServiceSpecs.key == key). \
+        filter(models.QualityOfServiceSpecs.specs_id == qos_specs_id). \
+        first()
+
+    if not result:
+        raise exception.QoSSpecsKeyNotFound(
+            specs_key=key,
+            specs_id=qos_specs_id)
+
+    return result
+
+
+@require_admin_context
+def qos_specs_update(context, qos_specs_id, specs):
+    """Make updates to a existing qos specs.
+
+    Perform add, update or delete key/values to a qos specs.
+    """
+
+    session = get_session()
+    with session.begin():
+        # make sure qos specs exists
+        _qos_specs_get_ref(context, qos_specs_id, session)
+        spec_ref = None
+        for key in specs.keys():
+            try:
+                spec_ref = _qos_specs_get_item(
+                    context, qos_specs_id, key, session)
+            except exception.QoSSpecsKeyNotFound as e:
+                spec_ref = models.QualityOfServiceSpecs()
+            id = None
+            if spec_ref.get('id', None):
+                id = spec_ref['id']
+            else:
+                id = str(uuid.uuid4())
+            value = dict(id=id, key=key, value=specs[key],
+                         specs_id=qos_specs_id,
+                         deleted=False)
+            LOG.debug('qos_specs_update() value: %s' % value)
+            spec_ref.update(value)
+            spec_ref.save(session=session)
+
+        return specs
+
+
+####################
+
+
 @require_context
 def volume_type_encryption_get(context, volume_type_id, session=None):
     return model_query(context, models.Encryption, session=session,
diff --git a/cinder/db/sqlalchemy/migrate_repo/versions/018_add_qos_specs.py b/cinder/db/sqlalchemy/migrate_repo/versions/018_add_qos_specs.py
new file mode 100644 (file)
index 0000000..7ef9546
--- /dev/null
@@ -0,0 +1,84 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (C) 2013 eBay Inc.
+# Copyright (C) 2013 OpenStack, LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from sqlalchemy import Boolean, Column, DateTime
+from sqlalchemy import ForeignKey, MetaData, Integer, String, Table
+
+from cinder.openstack.common import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+def upgrade(migrate_engine):
+    """Add volume_type_rate_limit table."""
+    meta = MetaData()
+    meta.bind = migrate_engine
+
+    quality_of_service_specs = Table(
+        'quality_of_service_specs', meta,
+        Column('created_at', DateTime(timezone=False)),
+        Column('updated_at', DateTime(timezone=False)),
+        Column('deleted_at', DateTime(timezone=False)),
+        Column('deleted', Boolean(create_constraint=True, name=None)),
+        Column('id', String(36), primary_key=True, nullable=False),
+        Column('specs_id', String(36),
+               ForeignKey('quality_of_service_specs.id')),
+        Column('key', String(255)),
+        Column('value', String(255)),
+        mysql_engine='InnoDB'
+    )
+
+    try:
+        quality_of_service_specs.create()
+    except Exception:
+        LOG.error(_("Table quality_of_service_specs not created!"))
+        raise
+
+    volume_types = Table('volume_types', meta, autoload=True)
+    qos_specs_id = Column('qos_specs_id', String(36),
+                          ForeignKey('quality_of_service_specs.id'))
+
+    try:
+        volume_types.create_column(qos_specs_id)
+        volume_types.update().values(qos_specs_id=None).execute()
+    except Exception:
+        LOG.error(_("Added qos_specs_id column to volume type table failed."))
+        raise
+
+
+def downgrade(migrate_engine):
+    """Remove volume_type_rate_limit table."""
+    meta = MetaData()
+    meta.bind = migrate_engine
+
+    qos_specs = Table('quality_of_service_specs', meta, autoload=True)
+
+    try:
+        qos_specs.drop()
+
+    except Exception:
+        LOG.error(_("Dropping quality_of_service_specs table failed."))
+        raise
+
+    volume_types = Table('volume_types', meta, autoload=True)
+    qos_specs_id = Column('qos_specs_id', String(36))
+    try:
+        volume_types.drop_column(qos_specs_id)
+    except Exception:
+        LOG.error(_("Dropping qos_specs_id column failed."))
+        raise
index 9da28cd34b0589d4a45e2c20cc30b74d4814a7f7..013d73358b1639592755d6335a2cf8c4636a63eb 100644 (file)
@@ -26,6 +26,7 @@ from sqlalchemy import Column, Integer, String, Text, schema
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy import ForeignKey, DateTime, Boolean
 from sqlalchemy.orm import relationship, backref
+from sqlalchemy.orm.collections import attribute_mapped_collection
 
 from oslo.config import cfg
 
@@ -149,7 +150,9 @@ class VolumeTypes(BASE, CinderBase):
     __tablename__ = "volume_types"
     id = Column(String(36), primary_key=True)
     name = Column(String(255))
-
+    # A reference to qos_specs entity
+    qos_specs_id = Column(String(36),
+                          ForeignKey('quality_of_service_specs.id'))
     volumes = relationship(Volume,
                            backref=backref('volume_type', uselist=False),
                            foreign_keys=id,
@@ -177,6 +180,63 @@ class VolumeTypeExtraSpecs(BASE, CinderBase):
     )
 
 
+class QualityOfServiceSpecs(BASE, CinderBase):
+    """Represents QoS specs as key/value pairs.
+
+    QoS specs is standalone entity that can be associated/disassociated
+    with volume types (one to many relation).  Adjacency list relationship
+    pattern is used in this model in order to represent following hierarchical
+    data with in flat table, e.g, following structure
+
+    qos-specs-1  'Rate-Limit'
+         |
+         +------>  consumer = 'front-end'
+         +------>  total_bytes_sec = 1048576
+         +------>  total_iops_sec = 500
+
+    qos-specs-2  'QoS_Level1'
+         |
+         +------>  consumer = 'back-end'
+         +------>  max-iops =  1000
+         +------>  min-iops = 200
+
+    is represented by:
+
+      id       specs_id       key                  value
+    ------     --------   -------------            -----
+    UUID-1     NULL       QoSSpec_Name           Rate-Limit
+    UUID-2     UUID-1       consumer             front-end
+    UUID-3     UUID-1     total_bytes_sec        1048576
+    UUID-4     UUID-1     total_iops_sec           500
+    UUID-5     NULL       QoSSpec_Name           QoS_Level1
+    UUID-6     UUID-5       consumer             back-end
+    UUID-7     UUID-5       max-iops               1000
+    UUID-8     UUID-5       min-iops               200
+    """
+    __tablename__ = 'quality_of_service_specs'
+    id = Column(String(36), primary_key=True)
+    specs_id = Column(String(36), ForeignKey(id))
+    key = Column(String(255))
+    value = Column(String(255))
+
+    specs = relationship(
+        "QualityOfServiceSpecs",
+        cascade="all, delete-orphan",
+        backref=backref("qos_spec", remote_side=id),
+    )
+
+    vol_types = relationship(
+        VolumeTypes,
+        backref=backref('qos_specs'),
+        foreign_keys=id,
+        primaryjoin='and_('
+                    'or_(VolumeTypes.qos_specs_id == '
+                    'QualityOfServiceSpecs.id,'
+                    'VolumeTypes.qos_specs_id == '
+                    'QualityOfServiceSpecs.specs_id),'
+                    'QualityOfServiceSpecs.deleted == False)')
+
+
 class VolumeGlanceMetadata(BASE, CinderBase):
     """Glance metadata for a bootable volume."""
     __tablename__ = 'volume_glance_metadata'
index 1d6141345bffffe92b7dc5ee38a2542efec417c6..8939e68fc5241f39640955b03a841293636a61cf 100644 (file)
@@ -657,3 +657,44 @@ class CoraidESMConfigureError(CoraidException):
 
 class CoraidESMNotAvailable(CoraidException):
     message = _('Coraid ESM not available with reason: %(reason)s.')
+
+
+class QoSSpecsExists(Duplicate):
+    message = _("QoS Specs %(specs_id)s already exists.")
+
+
+class QoSSpecsCreateFailed(CinderException):
+    message = _("Failed to create qos_specs: "
+                "%(name)s with specs %(qos_specs)s.")
+
+
+class QoSSpecsUpdateFailed(CinderException):
+    message = _("Failed to update qos_specs: "
+                "%(specs_id)s with specs %(qos_specs)s.")
+
+
+class QoSSpecsNotFound(NotFound):
+    message = _("No such QoS spec %(specs_id)s.")
+
+
+class QoSSpecsAssociateFailed(CinderException):
+    message = _("Failed to associate qos_specs: "
+                "%(specs_id)s with type %(type_id)s.")
+
+
+class QoSSpecsDisassociateFailed(CinderException):
+    message = _("Failed to disassociate qos_specs: "
+                "%(specs_id)s with type %(type_id)s.")
+
+
+class QoSSpecsKeyNotFound(NotFound):
+    message = _("QoS spec %(specs_id)s has no spec with "
+                "key %(specs_key)s.")
+
+
+class InvalidQoSSpecs(Invalid):
+    message = _("Invalid qos specs") + ": %(reason)s"
+
+
+class QoSSpecsInUse(CinderException):
+    message = _("QoS Specs %(specs_id)s is still associated with entities.")
index 83a135ef7ec26c9c089b0cce02b41bf299e3ce8b..ef813f0959b012f6cd1b20b0ce8624cc0d1986b5 100644 (file)
@@ -58,6 +58,7 @@ class FilterScheduler(driver.Scheduler):
         filter_properties['availability_zone'] = vol.get('availability_zone')
         filter_properties['user_id'] = vol.get('user_id')
         filter_properties['metadata'] = vol.get('metadata')
+        filter_properties['qos_specs'] = vol.get('qos_specs')
 
     def schedule_create_volume(self, context, request_spec, filter_properties):
         weighed_host = self._schedule(context, request_spec,
diff --git a/cinder/tests/api/contrib/test_qos_specs_manage.py b/cinder/tests/api/contrib/test_qos_specs_manage.py
new file mode 100644 (file)
index 0000000..0bf8e58
--- /dev/null
@@ -0,0 +1,484 @@
+# Copyright 2013 eBay Inc.
+# Copyright 2013 OpenStack LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import webob
+
+from cinder.api.contrib import qos_specs_manage
+from cinder import exception
+from cinder.openstack.common.notifier import api as notifier_api
+from cinder.openstack.common.notifier import test_notifier
+from cinder import test
+from cinder.tests.api import fakes
+from cinder.volume import qos_specs
+
+
+def stub_qos_specs(id):
+    specs = {"key1": "value1",
+             "key2": "value2",
+             "key3": "value3",
+             "key4": "value4",
+             "key5": "value5"}
+    specs.update(dict(id=str(id)))
+    return specs
+
+
+def stub_qos_associates(id):
+    return {str(id): {'FakeVolTypeName': 'FakeVolTypeID'}}
+
+
+def return_qos_specs_get_all(context):
+    return dict(
+        qos_specs_1=stub_qos_specs(1),
+        qos_specs_2=stub_qos_specs(2),
+        qos_specs_3=stub_qos_specs(3)
+    )
+
+
+def return_qos_specs_get_qos_specs(context, id):
+    if id == "777":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    name = 'qos_specs_%s' % id
+    return {name: stub_qos_specs(int(id))}
+
+
+def return_qos_specs_delete(context, id, force):
+    if id == "777":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    elif id == "666":
+        raise exception.QoSSpecsInUse(specs_id=id)
+    pass
+
+
+def return_qos_specs_update(context, id, specs):
+    if id == "777":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    elif id == "888":
+        raise exception.InvalidQoSSpecs(reason=str(id))
+    elif id == "999":
+        raise exception.QoSSpecsUpdateFailed(specs_id=id,
+                                             qos_specs=specs)
+    pass
+
+
+def return_qos_specs_create(context, name, specs):
+    if name == "666":
+        raise exception.QoSSpecsExists(specs_id=name)
+    elif name == "555":
+        raise exception.QoSSpecsCreateFailed(name=id, qos_specs=specs)
+    pass
+
+
+def return_qos_specs_get_by_name(context, name):
+    if name == "777":
+        raise exception.QoSSpecsNotFound(specs_id=name)
+
+    return stub_qos_specs(int(name.split("_")[2]))
+
+
+def return_get_qos_associations(context, id):
+    if id == "111":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    elif id == "222":
+        raise exception.CinderException()
+
+    return stub_qos_associates(id)
+
+
+def return_associate_qos_specs(context, id, type_id):
+    if id == "111":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    elif id == "222":
+        raise exception.QoSSpecsAssociateFailed(specs_id=id,
+                                                type_id=type_id)
+    elif id == "333":
+        raise exception.QoSSpecsDisassociateFailed(specs_id=id,
+                                                   type_id=type_id)
+
+    if type_id == "1234":
+        raise exception.VolumeTypeNotFound(
+            volume_type_id=type_id)
+
+    pass
+
+
+def return_disassociate_all(context, id):
+    if id == "111":
+        raise exception.QoSSpecsNotFound(specs_id=id)
+    elif id == "222":
+        raise exception.QoSSpecsDisassociateFailed(specs_id=id,
+                                                   type_id=None)
+
+
+class QoSSpecManageApiTest(test.TestCase):
+    def setUp(self):
+        super(QoSSpecManageApiTest, self).setUp()
+        self.flags(connection_type='fake',
+                   host='fake',
+                   notification_driver=[test_notifier.__name__])
+        self.controller = qos_specs_manage.QoSSpecsController()
+        """to reset notifier drivers left over from other api/contrib tests"""
+        notifier_api._reset_drivers()
+        test_notifier.NOTIFICATIONS = []
+
+    def tearDown(self):
+        notifier_api._reset_drivers()
+        super(QoSSpecManageApiTest, self).tearDown()
+
+    def test_index(self):
+        self.stubs.Set(qos_specs, 'get_all_specs',
+                       return_qos_specs_get_all)
+
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs')
+        res_dict = self.controller.index(req)
+
+        self.assertEqual(3, len(res_dict.keys()))
+
+        expected_names = ['qos_specs_1', 'qos_specs_2', 'qos_specs_3']
+        self.assertEqual(set(res_dict.keys()), set(expected_names))
+        for key in res_dict.keys():
+            self.assertEqual('value1', res_dict[key]['key1'])
+
+    def test_qos_specs_delete(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'delete',
+                       return_qos_specs_delete)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/1')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.controller.delete(req, 1)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_qos_specs_delete_not_found(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'delete',
+                       return_qos_specs_delete)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/777')
+        self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete,
+                          req, '777')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_qos_specs_delete_inuse(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'delete',
+                       return_qos_specs_delete)
+
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/666')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.delete,
+                          req, '666')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_qos_specs_delete_inuse_force(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'delete',
+                       return_qos_specs_delete)
+
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/666?force=True')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.delete,
+                          req, '666')
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_create(self):
+        self.stubs.Set(qos_specs, 'create',
+                       return_qos_specs_create)
+        self.stubs.Set(qos_specs, 'get_qos_specs_by_name',
+                       return_qos_specs_get_by_name)
+
+        body = {"qos_specs": {"name": "qos_specs_1",
+                              "key1": "value1"}}
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs')
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        res_dict = self.controller.create(req, body)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+        self.assertEqual(1, len(res_dict))
+        self.assertEqual('qos_specs_1', res_dict['qos_specs']['name'])
+
+    def test_create_conflict(self):
+        self.stubs.Set(qos_specs, 'create',
+                       return_qos_specs_create)
+        self.stubs.Set(qos_specs, 'get_qos_specs_by_name',
+                       return_qos_specs_get_by_name)
+
+        body = {"qos_specs": {"name": "666",
+                              "key1": "value1"}}
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs')
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.assertRaises(webob.exc.HTTPConflict,
+                          self.controller.create, req, body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_create_failed(self):
+        self.stubs.Set(qos_specs, 'create',
+                       return_qos_specs_create)
+        self.stubs.Set(qos_specs, 'get_qos_specs_by_name',
+                       return_qos_specs_get_by_name)
+
+        body = {"qos_specs": {"name": "555",
+                              "key1": "value1"}}
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs')
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.create, req, body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def _create_qos_specs_bad_body(self, body):
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs')
+        req.method = 'POST'
+        self.assertRaises(webob.exc.HTTPBadRequest,
+                          self.controller.create, req, body)
+
+    def test_create_no_body(self):
+        self._create_qos_specs_bad_body(body=None)
+
+    def test_create_missing_specs_name(self):
+        body = {'foo': {'a': 'b'}}
+        self._create_qos_specs_bad_body(body=body)
+
+    def test_create_malformed_entity(self):
+        body = {'qos_specs': 'string'}
+        self._create_qos_specs_bad_body(body=body)
+
+    def test_update(self):
+        self.stubs.Set(qos_specs, 'update',
+                       return_qos_specs_update)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/555')
+        body = {'qos_specs': {'key1': 'value1',
+                              'key2': 'value2'}}
+        res = self.controller.update(req, '555', body)
+        self.assertDictMatch(res, body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_update_not_found(self):
+        self.stubs.Set(qos_specs, 'update',
+                       return_qos_specs_update)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/777')
+        body = {'qos_specs': {'key1': 'value1',
+                              'key2': 'value2'}}
+        self.assertRaises(webob.exc.HTTPNotFound, self.controller.update,
+                          req, '777', body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_update_invalid_input(self):
+        self.stubs.Set(qos_specs, 'update',
+                       return_qos_specs_update)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/888')
+        body = {'qos_specs': {'key1': 'value1',
+                              'key2': 'value2'}}
+        self.assertRaises(webob.exc.HTTPBadRequest, self.controller.update,
+                          req, '888', body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_update_failed(self):
+        self.stubs.Set(qos_specs, 'update',
+                       return_qos_specs_update)
+
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 0)
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/999')
+        body = {'qos_specs': {'key1': 'value1',
+                              'key2': 'value2'}}
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.update,
+                          req, '999', body)
+        self.assertEquals(len(test_notifier.NOTIFICATIONS), 1)
+
+    def test_show(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+
+        req = fakes.HTTPRequest.blank('/v2/fake/qos-specs/1')
+        res_dict = self.controller.show(req, '1')
+
+        self.assertEqual(1, len(res_dict))
+        self.assertEqual('1', res_dict['qos_specs_1']['id'])
+
+    def test_get_associations(self):
+        self.stubs.Set(qos_specs, 'get_associations',
+                       return_get_qos_associations)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/associations')
+        res = self.controller.associations(req, '1')
+
+        self.assertEqual('1', res.keys()[0])
+        self.assertEqual('FakeVolTypeName', res['1'].keys()[0])
+        self.assertEqual('FakeVolTypeID',
+                         res['1']['FakeVolTypeName'])
+
+    def test_get_associations_not_found(self):
+        self.stubs.Set(qos_specs, 'get_associations',
+                       return_get_qos_associations)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/111/associations')
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.associations,
+                          req, '111')
+
+    def test_get_associations_failed(self):
+        self.stubs.Set(qos_specs, 'get_associations',
+                       return_get_qos_associations)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/222/associations')
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.associations,
+                          req, '222')
+
+    def test_associate(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'associate_qos_with_type',
+                       return_associate_qos_specs)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/associate?vol_type_id=111')
+        res = self.controller.associate(req, '1')
+
+        self.assertEqual(res.status_int, 202)
+
+    def test_associate_no_type(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'associate_qos_with_type',
+                       return_associate_qos_specs)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/associate')
+
+        self.assertRaises(webob.exc.HTTPBadRequest,
+                          self.controller.associate, req, '1')
+
+    def test_associate_not_found(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'associate_qos_with_type',
+                       return_associate_qos_specs)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/111/associate?vol_type_id=12')
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.associate, req, '111')
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/associate?vol_type_id=1234')
+
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.associate, req, '1')
+
+    def test_associate_fail(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'associate_qos_with_type',
+                       return_associate_qos_specs)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/222/associate?vol_type_id=1000')
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.associate, req, '222')
+
+    def test_disassociate(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_qos_specs',
+                       return_associate_qos_specs)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/disassociate?vol_type_id=111')
+        res = self.controller.disassociate(req, '1')
+        self.assertEqual(res.status_int, 202)
+
+    def test_disassociate_no_type(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_qos_specs',
+                       return_associate_qos_specs)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/disassociate')
+
+        self.assertRaises(webob.exc.HTTPBadRequest,
+                          self.controller.disassociate, req, '1')
+
+    def test_disassociate_not_found(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_qos_specs',
+                       return_associate_qos_specs)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/111/disassociate?vol_type_id=12')
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.disassociate, req, '111')
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/disassociate?vol_type_id=1234')
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.disassociate, req, '1')
+
+    def test_disassociate_failed(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_qos_specs',
+                       return_associate_qos_specs)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/333/disassociate?vol_type_id=1000')
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.disassociate, req, '333')
+
+    def test_disassociate_all(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_all',
+                       return_disassociate_all)
+
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/1/disassociate_all')
+        res = self.controller.disassociate_all(req, '1')
+        self.assertEqual(res.status_int, 202)
+
+    def test_disassociate_all_not_found(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_all',
+                       return_disassociate_all)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/111/disassociate_all')
+        self.assertRaises(webob.exc.HTTPNotFound,
+                          self.controller.disassociate_all, req, '111')
+
+    def test_disassociate_all_failed(self):
+        self.stubs.Set(qos_specs, 'get_qos_specs',
+                       return_qos_specs_get_qos_specs)
+        self.stubs.Set(qos_specs, 'disassociate_all',
+                       return_disassociate_all)
+        req = fakes.HTTPRequest.blank(
+            '/v2/fake/qos-specs/222/disassociate_all')
+        self.assertRaises(webob.exc.HTTPInternalServerError,
+                          self.controller.disassociate_all, req, '222')
diff --git a/cinder/tests/db/test_qos_specs.py b/cinder/tests/db/test_qos_specs.py
new file mode 100644 (file)
index 0000000..7d8f1d4
--- /dev/null
@@ -0,0 +1,204 @@
+# Copyright (C) 2013 eBay Inc.
+# Copyright (C) 2013 OpenStack LLC.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""Tests for qaulity_of_service_specs table."""
+
+
+import time
+
+from cinder import context
+from cinder import db
+from cinder import exception
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder.volume import volume_types
+
+
+LOG = logging.getLogger(__name__)
+
+
+def fake_qos_specs_get_by_name(context, name, session=None, inactive=False):
+    pass
+
+
+class QualityOfServiceSpecsTableTestCase(test.TestCase):
+    """Test case for QualityOfServiceSpecs model."""
+
+    def setUp(self):
+        super(QualityOfServiceSpecsTableTestCase, self).setUp()
+        self.ctxt = context.RequestContext(user_id='user_id',
+                                           project_id='project_id',
+                                           is_admin=True)
+
+    def tearDown(self):
+        super(QualityOfServiceSpecsTableTestCase, self).tearDown()
+
+    def _create_qos_specs(self, name, values=None):
+        """Create a transfer object."""
+        if values:
+            specs = dict(name=name, qos_specs=values)
+        else:
+            specs = {'name': name,
+                     'qos_specs': {
+                         'consumer': 'back-end',
+                         'key1': 'value1',
+                         'key2': 'value2'}}
+        return db.qos_specs_create(self.ctxt, specs)['id']
+
+    def test_qos_specs_create(self):
+        # If there is qos specs with the same name exists,
+        # a QoSSpecsExists exception will be raised.
+        name = 'QoSSpecsCreationTest'
+        self._create_qos_specs(name)
+        self.assertRaises(exception.QoSSpecsExists,
+                          db.qos_specs_create, self.ctxt, dict(name=name))
+
+        specs_id = self._create_qos_specs('NewName')
+        query_id = db.qos_specs_get_by_name(
+            self.ctxt, 'NewName')['NewName']['id']
+        self.assertEquals(specs_id, query_id)
+
+    def test_qos_specs_get(self):
+        value = dict(consumer='front-end',
+                     key1='foo', key2='bar')
+        specs_id = self._create_qos_specs('Name1', value)
+
+        fake_id = 'fake-UUID'
+        self.assertRaises(exception.QoSSpecsNotFound,
+                          db.qos_specs_get, self.ctxt, fake_id)
+
+        specs = db.qos_specs_get(self.ctxt, specs_id)
+        value.update(dict(id=specs_id))
+        expected = dict(Name1=value)
+        self.assertDictMatch(specs, expected)
+
+    def test_qos_specs_get_all(self):
+        value1 = dict(consumer='front-end',
+                      key1='v1', key2='v2')
+        value2 = dict(consumer='back-end',
+                      key3='v3', key4='v4')
+        value3 = dict(consumer='back-end',
+                      key5='v5', key6='v6')
+
+        spec_id1 = self._create_qos_specs('Name1', value1)
+        spec_id2 = self._create_qos_specs('Name2', value2)
+        spec_id3 = self._create_qos_specs('Name3', value3)
+
+        specs = db.qos_specs_get_all(self.ctxt)
+        self.assertEquals(len(specs), 3,
+                          "Unexpected number of qos specs records")
+
+        value1.update({'id': spec_id1})
+        value2.update({'id': spec_id2})
+        value3.update({'id': spec_id3})
+        self.assertDictMatch(specs['Name1'], value1)
+        self.assertDictMatch(specs['Name2'], value2)
+        self.assertDictMatch(specs['Name3'], value3)
+
+    def test_qos_specs_get_by_name(self):
+        name = str(int(time.time()))
+        value = dict(consumer='front-end',
+                     foo='Foo', bar='Bar')
+        specs_id = self._create_qos_specs(name, value)
+        specs = db.qos_specs_get_by_name(self.ctxt, name)
+        value.update(dict(id=specs_id))
+        expected = {name: value}
+        self.assertDictMatch(specs, expected)
+
+    def test_qos_specs_delete(self):
+        name = str(int(time.time()))
+        specs_id = self._create_qos_specs(name)
+
+        db.qos_specs_delete(self.ctxt, specs_id)
+        self.assertRaises(exception.QoSSpecsNotFound, db.qos_specs_get,
+                          self.ctxt, specs_id)
+
+    def test_associate_type_with_qos(self):
+        self.assertRaises(exception.VolumeTypeNotFound,
+                          db.volume_type_qos_associate,
+                          self.ctxt, 'Fake-VOLID', 'Fake-QOSID')
+        type_id = volume_types.create(self.ctxt, 'TypeName')['id']
+        specs_id = self._create_qos_specs('FakeQos')
+        db.volume_type_qos_associate(self.ctxt, type_id, specs_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 1)
+        self.assertEquals(res[0]['id'], type_id)
+        self.assertEquals(res[0]['qos_specs_id'], specs_id)
+
+    def test_qos_associations_get(self):
+        self.assertRaises(exception.QoSSpecsNotFound,
+                          db.qos_specs_associations_get,
+                          self.ctxt, 'Fake-UUID')
+
+        type_id = volume_types.create(self.ctxt, 'TypeName')['id']
+        specs_id = self._create_qos_specs('FakeQos')
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 0)
+
+        db.volume_type_qos_associate(self.ctxt, type_id, specs_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 1)
+        self.assertEquals(res[0]['id'], type_id)
+        self.assertEquals(res[0]['qos_specs_id'], specs_id)
+
+        type0_id = volume_types.create(self.ctxt, 'Type0Name')['id']
+        db.volume_type_qos_associate(self.ctxt, type0_id, specs_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 2)
+        self.assertEquals(res[0]['qos_specs_id'], specs_id)
+        self.assertEquals(res[1]['qos_specs_id'], specs_id)
+
+    def test_qos_specs_disassociate(self):
+        type_id = volume_types.create(self.ctxt, 'TypeName')['id']
+        specs_id = self._create_qos_specs('FakeQos')
+        db.volume_type_qos_associate(self.ctxt, type_id, specs_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(res[0]['id'], type_id)
+        self.assertEquals(res[0]['qos_specs_id'], specs_id)
+
+        db.qos_specs_disassociate(self.ctxt, specs_id, type_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 0)
+        res = db.volume_type_get(self.ctxt, type_id)
+        self.assertEquals(res['qos_specs_id'], None)
+
+    def test_qos_specs_disassociate_all(self):
+        specs_id = self._create_qos_specs('FakeQos')
+        type1_id = volume_types.create(self.ctxt, 'Type1Name')['id']
+        type2_id = volume_types.create(self.ctxt, 'Type2Name')['id']
+        type3_id = volume_types.create(self.ctxt, 'Type3Name')['id']
+        db.volume_type_qos_associate(self.ctxt, type1_id, specs_id)
+        db.volume_type_qos_associate(self.ctxt, type2_id, specs_id)
+        db.volume_type_qos_associate(self.ctxt, type3_id, specs_id)
+
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 3)
+
+        db.qos_specs_disassociate_all(self.ctxt, specs_id)
+        res = db.qos_specs_associations_get(self.ctxt, specs_id)
+        self.assertEquals(len(res), 0)
+
+    def test_qos_specs_update(self):
+        name = 'FakeName'
+        specs_id = self._create_qos_specs(name)
+        value = dict(key2='new_value2', key3='value3')
+
+        self.assertRaises(exception.QoSSpecsNotFound, db.qos_specs_update,
+                          self.ctxt, 'Fake-UUID', value)
+        db.qos_specs_update(self.ctxt, specs_id, value)
+        specs = db.qos_specs_get(self.ctxt, specs_id)
+        self.assertEqual(specs[name]['key2'], 'new_value2')
+        self.assertEqual(specs[name]['key3'], 'value3')
index 98f126f847429123b00a95b9bb46e9cb3cf621f1..631331f2114168a6e324fcd5e849b204cca0be4f 100644 (file)
@@ -39,6 +39,7 @@
     "volume_extension:types_extra_specs": [],
     "volume_extension:volume_type_encryption": [["rule:admin_api"]],
     "volume_extension:volume_encryption_metadata": [["rule:admin_or_owner"]],
+    "volume_extension:qos_specs_manage": [],
     "volume_extension:extended_snapshot_attributes": [],
     "volume_extension:volume_image_metadata": [],
     "volume_extension:volume_host_attribute": [["rule:admin_api"]],
@@ -51,7 +52,7 @@
     "volume:accept_transfer": [],
     "volume:delete_transfer": [],
     "volume:get_all_transfers": [],
-    
+
     "backup:create" : [],
     "backup:delete": [],
     "backup:get": [],
index 9073aca620ebd4dd175e792b448dfacc00fdd852..b3791d8422e153427ee6673b68d8232e92bad141 100644 (file)
@@ -646,7 +646,6 @@ class TestMigrations(test.TestCase):
             metadata.bind = engine
 
             migration_api.upgrade(engine, TestMigrations.REPOSITORY, 10)
-
             self.assertTrue(engine.dialect.has_table(engine.connect(),
                                                      "transfers"))
             transfers = sqlalchemy.Table('transfers',
@@ -845,12 +844,12 @@ class TestMigrations(test.TestCase):
 
     def test_migration_017(self):
         """Test that added encryption information works correctly."""
+
+            # upgrade schema
         for (key, engine) in self.engines.items():
             migration_api.version_control(engine,
                                           TestMigrations.REPOSITORY,
                                           migration.INIT_VERSION)
-
-            # upgrade schema
             migration_api.upgrade(engine, TestMigrations.REPOSITORY, 16)
             metadata = sqlalchemy.schema.MetaData()
             metadata.bind = engine
@@ -897,3 +896,41 @@ class TestMigrations(test.TestCase):
 
             self.assertFalse(engine.dialect.has_table(engine.connect(),
                                                       'encryption'))
+
+    def test_migration_018(self):
+        """Test that added qos_specs table works correctly."""
+        for (key, engine) in self.engines.items():
+            migration_api.version_control(engine,
+                                          TestMigrations.REPOSITORY,
+                                          migration.INIT_VERSION)
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 17)
+            metadata = sqlalchemy.schema.MetaData()
+            metadata.bind = engine
+
+            migration_api.upgrade(engine, TestMigrations.REPOSITORY, 18)
+            self.assertTrue(engine.dialect.has_table(
+                engine.connect(), "quality_of_service_specs"))
+            qos_specs = sqlalchemy.Table('quality_of_service_specs',
+                                         metadata,
+                                         autoload=True)
+            self.assertTrue(isinstance(qos_specs.c.created_at.type,
+                                       sqlalchemy.types.DATETIME))
+            self.assertTrue(isinstance(qos_specs.c.updated_at.type,
+                                       sqlalchemy.types.DATETIME))
+            self.assertTrue(isinstance(qos_specs.c.deleted_at.type,
+                                       sqlalchemy.types.DATETIME))
+            self.assertTrue(isinstance(qos_specs.c.deleted.type,
+                                       sqlalchemy.types.BOOLEAN))
+            self.assertTrue(isinstance(qos_specs.c.id.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue(isinstance(qos_specs.c.specs_id.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue(isinstance(qos_specs.c.key.type,
+                                       sqlalchemy.types.VARCHAR))
+            self.assertTrue(isinstance(qos_specs.c.value.type,
+                                       sqlalchemy.types.VARCHAR))
+
+            migration_api.downgrade(engine, TestMigrations.REPOSITORY, 17)
+
+            self.assertFalse(engine.dialect.has_table(
+                engine.connect(), "quality_of_service_specs"))
diff --git a/cinder/tests/test_qos_specs.py b/cinder/tests/test_qos_specs.py
new file mode 100644 (file)
index 0000000..9143665
--- /dev/null
@@ -0,0 +1,293 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 eBay Inc.
+# Copyright (c) 2013 OpenStack LLC.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+"""
+Unit Tests for qos specs internal API
+"""
+
+import time
+
+from cinder import context
+from cinder import db
+from cinder import exception
+from cinder.openstack.common.db import exception as db_exc
+from cinder.openstack.common import log as logging
+from cinder import test
+from cinder.volume import qos_specs
+from cinder.volume import volume_types
+
+
+LOG = logging.getLogger(__name__)
+
+
+def fake_db_qos_specs_create(context, values):
+    if values['name'] == 'DupQoSName':
+        raise exception.QoSSpecsExists(specs_id=values['name'])
+    elif values['name'] == 'FailQoSName':
+        raise db_exc.DBError()
+
+    pass
+
+
+class QoSSpecsTestCase(test.TestCase):
+    """Test cases for qos specs code."""
+    def setUp(self):
+        super(QoSSpecsTestCase, self).setUp()
+        self.ctxt = context.get_admin_context()
+
+    def _create_qos_specs(self, name, values=None):
+        """Create a transfer object."""
+        if values:
+            specs = dict(name=name, qos_specs=values)
+        else:
+            specs = {'name': name,
+                     'qos_specs': {
+                         'consumer': 'back-end',
+                         'key1': 'value1',
+                         'key2': 'value2'}}
+        return db.qos_specs_create(self.ctxt, specs)['id']
+
+    def test_create(self):
+        input = {'key1': 'value1',
+                 'key2': 'value2',
+                 'key3': 'value3'}
+        ref = qos_specs.create(self.ctxt, 'FakeName', input)
+        specs = qos_specs.get_qos_specs(self.ctxt, ref['id'])
+        input.update(dict(consumer='back-end'))
+        input.update(dict(id=ref['id']))
+        expected = {'FakeName': input}
+        self.assertDictMatch(specs, expected)
+
+        self.stubs.Set(db, 'qos_specs_create',
+                       fake_db_qos_specs_create)
+
+        # Restore input back to original state
+        del input['id']
+        del input['consumer']
+        # qos specs must have unique name
+        self.assertRaises(exception.QoSSpecsExists,
+                          qos_specs.create, self.ctxt, 'DupQoSName', input)
+
+        input.update({'consumer': 'FakeConsumer'})
+        # consumer must be one of: front-end, back-end, both
+        self.assertRaises(exception.InvalidQoSSpecs,
+                          qos_specs.create, self.ctxt, 'QoSName', input)
+
+        del input['consumer']
+        # able to catch DBError
+        self.assertRaises(exception.QoSSpecsCreateFailed,
+                          qos_specs.create, self.ctxt, 'FailQoSName', input)
+
+    def test_update(self):
+        def fake_db_update(context, specs_id, values):
+            raise db_exc.DBError()
+
+        input = {'key1': 'value1',
+                 'consumer': 'WrongPlace'}
+        # consumer must be one of: front-end, back-end, both
+        self.assertRaises(exception.InvalidQoSSpecs,
+                          qos_specs.update, self.ctxt, 'fake_id', input)
+
+        del input['consumer']
+        # qos specs must exists
+        self.assertRaises(exception.QoSSpecsNotFound,
+                          qos_specs.update, self.ctxt, 'fake_id', input)
+
+        specs_id = self._create_qos_specs('Name', input)
+        qos_specs.update(self.ctxt, specs_id,
+                         {'key1': 'newvalue1',
+                          'key2': 'value2'})
+        specs = qos_specs.get_qos_specs(self.ctxt, specs_id)
+        self.assertEqual(specs['Name']['key1'], 'newvalue1')
+        self.assertEqual(specs['Name']['key2'], 'value2')
+
+        self.stubs.Set(db, 'qos_specs_update', fake_db_update)
+        self.assertRaises(exception.QoSSpecsUpdateFailed,
+                          qos_specs.update, self.ctxt, 'fake_id', input)
+
+    def test_delete(self):
+        def fake_db_associations_get(context, id):
+            if id == 'InUse':
+                return True
+            else:
+                return False
+
+        def fake_db_delete(context, id):
+            if id == 'NotFound':
+                raise exception.QoSSpecsNotFound(specs_id=id)
+
+        def fake_disassociate_all(context, id):
+            pass
+
+        self.stubs.Set(db, 'qos_specs_associations_get',
+                       fake_db_associations_get)
+        self.stubs.Set(qos_specs, 'disassociate_all',
+                       fake_disassociate_all)
+        self.stubs.Set(db, 'qos_specs_delete', fake_db_delete)
+        self.assertRaises(exception.InvalidQoSSpecs,
+                          qos_specs.delete, self.ctxt, None)
+        self.assertRaises(exception.QoSSpecsNotFound,
+                          qos_specs.delete, self.ctxt, 'NotFound')
+        self.assertRaises(exception.QoSSpecsInUse,
+                          qos_specs.delete, self.ctxt, 'InUse')
+        # able to delete in-use qos specs if force=True
+        qos_specs.delete(self.ctxt, 'InUse', force=True)
+
+    def test_get_associations(self):
+        def fake_db_associate_get(context, id):
+            if id == 'Trouble':
+                raise db_exc.DBError()
+            return [{'name': 'type-1', 'id': 'id-1'},
+                    {'name': 'type-2', 'id': 'id-2'}]
+
+        self.stubs.Set(db, 'qos_specs_associations_get',
+                       fake_db_associate_get)
+        expected = {'specs-id': {'type-1': 'id-1',
+                                 'type-2': 'id-2'}}
+        res = qos_specs.get_associations(self.ctxt, 'specs-id')
+        self.assertDictMatch(res, expected)
+
+        self.assertRaises(exception.CinderException,
+                          qos_specs.get_associations, self.ctxt,
+                          'Trouble')
+
+    def test_associate_qos_with_type(self):
+        def fake_db_associate(context, id, type_id):
+            if id == 'Trouble':
+                raise db_exc.DBError()
+            elif type_id == 'NotFound':
+                raise exception.VolumeTypeNotFound(volume_type_id=type_id)
+            pass
+
+        type_ref = volume_types.create(self.ctxt, 'TypeName')
+        specs_id = self._create_qos_specs('QoSName')
+
+        qos_specs.associate_qos_with_type(self.ctxt, specs_id,
+                                          type_ref['id'])
+        res = qos_specs.get_associations(self.ctxt, specs_id)
+        self.assertEquals(len(res[specs_id].keys()), 1)
+        self.assertTrue('TypeName' in res[specs_id].keys())
+        self.assertTrue(type_ref['id'] in res[specs_id].values())
+
+        self.stubs.Set(db, 'qos_specs_associate',
+                       fake_db_associate)
+        self.assertRaises(exception.VolumeTypeNotFound,
+                          qos_specs.associate_qos_with_type,
+                          self.ctxt, 'specs-id', 'NotFound')
+        self.assertRaises(exception.QoSSpecsAssociateFailed,
+                          qos_specs.associate_qos_with_type,
+                          self.ctxt, 'Trouble', 'id')
+
+    def test_disassociate_qos_specs(self):
+        def fake_db_disassociate(context, id, type_id):
+            if id == 'Trouble':
+                raise db_exc.DBError()
+            elif type_id == 'NotFound':
+                raise exception.VolumeTypeNotFound(volume_type_id=type_id)
+            pass
+
+        type_ref = volume_types.create(self.ctxt, 'TypeName')
+        specs_id = self._create_qos_specs('QoSName')
+
+        qos_specs.associate_qos_with_type(self.ctxt, specs_id,
+                                          type_ref['id'])
+        res = qos_specs.get_associations(self.ctxt, specs_id)
+        self.assertEquals(len(res[specs_id].keys()), 1)
+
+        qos_specs.disassociate_qos_specs(self.ctxt, specs_id, type_ref['id'])
+        res = qos_specs.get_associations(self.ctxt, specs_id)
+        self.assertEquals(len(res[specs_id].keys()), 0)
+
+        self.stubs.Set(db, 'qos_specs_disassociate',
+                       fake_db_disassociate)
+        self.assertRaises(exception.VolumeTypeNotFound,
+                          qos_specs.disassociate_qos_specs,
+                          self.ctxt, 'specs-id', 'NotFound')
+        self.assertRaises(exception.QoSSpecsDisassociateFailed,
+                          qos_specs.disassociate_qos_specs,
+                          self.ctxt, 'Trouble', 'id')
+
+    def test_disassociate_all(self):
+        def fake_db_disassociate_all(context, id):
+            if id == 'Trouble':
+                raise db_exc.DBError()
+            pass
+
+        type1_ref = volume_types.create(self.ctxt, 'TypeName1')
+        type2_ref = volume_types.create(self.ctxt, 'TypeName2')
+        specs_id = self._create_qos_specs('QoSName')
+
+        qos_specs.associate_qos_with_type(self.ctxt, specs_id,
+                                          type1_ref['id'])
+        qos_specs.associate_qos_with_type(self.ctxt, specs_id,
+                                          type2_ref['id'])
+        res = qos_specs.get_associations(self.ctxt, specs_id)
+        self.assertEquals(len(res[specs_id].keys()), 2)
+
+        qos_specs.disassociate_all(self.ctxt, specs_id)
+        res = qos_specs.get_associations(self.ctxt, specs_id)
+        self.assertEquals(len(res[specs_id].keys()), 0)
+
+        self.stubs.Set(db, 'qos_specs_disassociate_all',
+                       fake_db_disassociate_all)
+        self.assertRaises(exception.QoSSpecsDisassociateFailed,
+                          qos_specs.disassociate_all,
+                          self.ctxt, 'Trouble')
+
+    def test_get_all_specs(self):
+        input = {'key1': 'value1',
+                 'key2': 'value2',
+                 'key3': 'value3'}
+        specs_id1 = self._create_qos_specs('Specs1', input)
+        input.update({'key4': 'value4'})
+        specs_id2 = self._create_qos_specs('Specs2', input)
+
+        expected = {'Specs1': {'key1': 'value1',
+                               'id': specs_id1,
+                               'key2': 'value2',
+                               'key3': 'value3'},
+                    'Specs2': {'key1': 'value1',
+                               'id': specs_id2,
+                               'key2': 'value2',
+                               'key3': 'value3',
+                               'key4': 'value4'}}
+        res = qos_specs.get_all_specs(self.ctxt)
+        self.assertDictMatch(expected, res)
+
+    def test_get_qos_specs(self):
+        one_time_value = str(int(time.time()))
+        input = {'key1': one_time_value,
+                 'key2': 'value2',
+                 'key3': 'value3'}
+        id = self._create_qos_specs('Specs1', input)
+        specs = qos_specs.get_qos_specs(self.ctxt, id)
+        self.assertEquals(specs['Specs1']['key1'], one_time_value)
+
+        self.assertRaises(exception.InvalidQoSSpecs,
+                          qos_specs.get_qos_specs, self.ctxt, None)
+
+    def test_get_qos_specs_by_name(self):
+        one_time_value = str(int(time.time()))
+        input = {'key1': one_time_value,
+                 'key2': 'value2',
+                 'key3': 'value3'}
+        id = self._create_qos_specs(one_time_value, input)
+        specs = qos_specs.get_qos_specs_by_name(self.ctxt,
+                                                one_time_value)
+        self.assertEquals(specs[one_time_value]['key1'], one_time_value)
+
+        self.assertRaises(exception.InvalidQoSSpecs,
+                          qos_specs.get_qos_specs_by_name, self.ctxt, None)
index 60102a75badc54ccb9aa186d068112e782d9501e..743746071bb708b840a1ddd8ca973e40693f9171 100644 (file)
@@ -27,6 +27,7 @@ from cinder import exception
 from cinder.openstack.common import log as logging
 from cinder import test
 from cinder.tests import conf_fixture
+from cinder.volume import qos_specs
 from cinder.volume import volume_types
 
 
@@ -201,3 +202,22 @@ class VolumeTypeTestCase(test.TestCase):
                                                        volume_type_id,
                                                        encryption)
         self.assertTrue(volume_types.is_encrypted(self.ctxt, volume_type_id))
+
+    def test_get_volume_type_qos_specs(self):
+        qos_ref = qos_specs.create(self.ctxt, 'qos-specs-1', {'k1': 'v1',
+                                                              'k2': 'v2',
+                                                              'k3': 'v3'})
+        type_ref = volume_types.create(self.ctxt, "type1", {"key2": "val2",
+                                                  "key3": "val3"})
+        res = volume_types.get_volume_type_qos_specs(type_ref['id'])
+        self.assertEquals(res['qos_specs'], {})
+        qos_specs.associate_qos_with_type(self.ctxt,
+                                          qos_ref['id'],
+                                          type_ref['id'])
+
+        expected = {'qos_specs': {'consumer': 'back-end',
+                                  'k1': 'v1',
+                                  'k2': 'v2',
+                                  'k3': 'v3'}}
+        res = volume_types.get_volume_type_qos_specs(type_ref['id'])
+        self.assertDictMatch(expected, res)
index ea05d3dc5f93e969c259a5f776c494d70b55e8ab..3eb2933b3fe87b93ad6685ee9b2c9a143d40b65b 100644 (file)
@@ -519,6 +519,14 @@ class ExtractVolumeRequestTask(CinderTask):
                                                         source_volume,
                                                         backup_source_volume)
 
+        specs = {}
+        if volume_type_id:
+            qos_specs = volume_types.get_volume_type_qos_specs(volume_type_id)
+            specs = qos_specs['qos_specs']
+        if not specs:
+            # to make sure we don't pass empty dict
+            specs = None
+
         self._check_metadata_properties(metadata)
 
         return {
@@ -529,6 +537,7 @@ class ExtractVolumeRequestTask(CinderTask):
             'volume_type': volume_type,
             'volume_type_id': volume_type_id,
             'encryption_key_id': encryption_key_id,
+            'qos_specs': specs,
         }
 
 
index 6513c14c3b0c26544aa05e461adf5884e87ac768..532f688ede2af8db22111b85e6a55fc5e798418d 100644 (file)
@@ -58,6 +58,7 @@ from cinder.volume.configuration import Configuration
 from cinder.volume.flows import create_volume
 from cinder.volume import rpcapi as volume_rpcapi
 from cinder.volume import utils as volume_utils
+from cinder.volume import volume_types
 
 from cinder.taskflow import states
 
@@ -516,7 +517,21 @@ class VolumeManager(manager.SchedulerDependentManager):
         """
         volume_ref = self.db.volume_get(context, volume_id)
         self.driver.validate_connector(connector)
-        return self.driver.initialize_connection(volume_ref, connector)
+        conn_info = self.driver.initialize_connection(volume_ref, connector)
+
+        # Add qos_specs to connection info
+        typeid = volume_ref['volume_type_id']
+        specs = {}
+        if typeid:
+            res = volume_types.get_volume_type_qos_specs(typeid)
+            specs = res['qos_specs']
+
+        # Don't pass qos_spec as empty dict
+        qos_spec = dict(qos_spec=specs if specs else None)
+
+        conn_info['data'].update(qos_spec)
+
+        return conn_info
 
     def terminate_connection(self, context, volume_id, connector, force=False):
         """Cleanup connection from host represented by connector.
diff --git a/cinder/volume/qos_specs.py b/cinder/volume/qos_specs.py
new file mode 100644 (file)
index 0000000..2b933cc
--- /dev/null
@@ -0,0 +1,250 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2013 eBay Inc.
+# Copyright (c) 2013 OpenStack LLC.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""The QoS Specs Implementation"""
+
+
+from oslo.config import cfg
+
+from cinder import context
+from cinder import db
+from cinder import exception
+from cinder.openstack.common.db import exception as db_exc
+from cinder.openstack.common import log as logging
+
+
+CONF = cfg.CONF
+LOG = logging.getLogger(__name__)
+
+CONTROL_LOCATION = ['front-end', 'back-end', 'both']
+
+
+def _verify_prepare_qos_specs(specs, create=True):
+    """Check if 'consumer' value in qos specs is valid.
+
+    Verify 'consumer' value in qos_specs is valid, raise
+    exception if not. Assign default value to 'consumer', which
+    is 'back-end' if input is empty.
+
+    :params create a flag indicate if specs being verified is
+    for create. If it's false, that means specs is for update,
+    so that there's no need to add 'consumer' if that wasn't in
+    specs.
+    """
+
+    # Check control location, if it's missing in input, assign default
+    # control location: 'front-end'
+    if not specs:
+        specs = {}
+    # remove 'name' since we will handle that elsewhere.
+    if specs.get('name', None):
+        del specs['name']
+    try:
+        if specs['consumer'] not in CONTROL_LOCATION:
+            msg = _("Valid consumer of QoS specs are: %s") % CONTROL_LOCATION
+            raise exception.InvalidQoSSpecs(reason=msg)
+    except KeyError:
+        # Default consumer is back-end, i.e Cinder volume service
+        if create:
+            specs['consumer'] = 'back-end'
+
+    return specs
+
+
+def create(context, name, specs=None):
+    """Creates qos_specs.
+
+    :param specs dictionary that contains specifications for QoS
+          e.g. {'consumer': 'front-end',
+                'total_iops_sec': 1000,
+                'total_bytes_sec': 1024000}
+    """
+    _verify_prepare_qos_specs(specs)
+
+    values = dict(name=name, qos_specs=specs)
+
+    LOG.debug("Dict for qos_specs: %s" % values)
+
+    try:
+        qos_specs_ref = db.qos_specs_create(context, values)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        raise exception.QoSSpecsCreateFailed(name=name,
+                                             qos_specs=specs)
+    return qos_specs_ref
+
+
+def update(context, qos_specs_id, specs):
+    """Update qos specs.
+
+    :param specs dictionary that contains key/value pairs for updating
+    existing specs.
+        e.g. {'consumer': 'front-end',
+              'total_iops_sec': 500,
+              'total_bytes_sec': 512000,}
+    """
+    # need to verify specs in case 'consumer' is passed
+    _verify_prepare_qos_specs(specs, create=False)
+    LOG.debug('qos_specs.update(): specs %s' % specs)
+    try:
+        res = db.qos_specs_update(context, qos_specs_id, specs)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        raise exception.QoSSpecsUpdateFailed(specs_id=qos_specs_id,
+                                             qos_specs=specs)
+
+    return res
+
+
+def delete(context, qos_specs_id, force=False):
+    """Marks qos specs as deleted.
+
+    'force' parameter is a flag to determine whether should destroy
+    should continue when there were entities associated with the qos specs.
+    force=True indicates caller would like to mark qos specs as deleted
+    even if there was entities associate with target qos specs.
+    Trying to delete a qos specs still associated with entities will
+    cause QoSSpecsInUse exception if force=False (default).
+    """
+    if qos_specs_id is None:
+        msg = _("id cannot be None")
+        raise exception.InvalidQoSSpecs(reason=msg)
+    else:
+        # check if there is any entity associated with this
+        # qos specs.
+        res = db.qos_specs_associations_get(context, qos_specs_id)
+        if res and not force:
+            raise exception.QoSSpecsInUse(specs_id=qos_specs_id)
+        elif force:
+            # remove all association
+            disassociate_all(context, qos_specs_id)
+        db.qos_specs_delete(context, qos_specs_id)
+
+
+def get_associations(context, specs_id):
+    """Get all associations of given qos specs."""
+    try:
+        # query returns a list of volume types associated with qos specs
+        associates = db.qos_specs_associations_get(context, specs_id)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        msg = _('Failed to get all associations of '
+                'qos specs %s') % specs_id
+        LOG.warn(msg)
+        raise exception.CinderException(message=msg)
+
+    result = {}
+    for vol_type in associates:
+        result[vol_type['name']] = vol_type['id']
+
+    return {specs_id: result}
+
+
+def associate_qos_with_type(context, specs_id, type_id):
+    """Associate qos_specs from volume type."""
+    try:
+        db.qos_specs_associate(context, specs_id, type_id)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        LOG.warn(_('Failed to associate qos specs '
+                   '%(id)s with type: %(vol_type_id)s') %
+                 dict(id=specs_id, vol_type_id=type_id))
+        raise exception.QoSSpecsAssociateFailed(specs_id=specs_id,
+                                                type_id=type_id)
+
+
+def disassociate_qos_specs(context, specs_id, type_id):
+    """Disassociate qos_specs from volume type."""
+    try:
+        db.qos_specs_disassociate(context, specs_id, type_id)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        LOG.warn(_('Failed to disassociate qos specs '
+                   '%(id)s with type: %(vol_type_id)s') %
+                 dict(id=specs_id, vol_type_id=type_id))
+        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
+                                                   type_id=type_id)
+
+
+def disassociate_all(context, specs_id):
+    """Disassociate qos_specs from all entities."""
+    try:
+        db.qos_specs_disassociate_all(context, specs_id)
+    except db_exc.DBError as e:
+        LOG.exception(_('DB error: %s') % e)
+        LOG.warn(_('Failed to disassociate qos specs %s.') % specs_id)
+        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
+                                                   type_id=None)
+
+
+def get_all_specs(context, inactive=False, search_opts={}):
+    """Get all non-deleted qos specs.
+
+    Pass inactive=True as argument and deleted volume types would return
+    as well.
+    """
+    qos_specs = db.qos_specs_get_all(context, inactive)
+
+    if search_opts:
+        LOG.debug(_("Searching by: %s") % str(search_opts))
+
+        def _check_specs_match(qos_specs, searchdict):
+            for k, v in searchdict.iteritems():
+                if ((k not in qos_specs['specs'].keys() or
+                     qos_specs['specs'][k] != v)):
+                    return False
+            return True
+
+        # search_option to filter_name mapping.
+        filter_mapping = {'qos_specs': _check_specs_match}
+
+        result = {}
+        for name, args in qos_specs.iteritems():
+            # go over all filters in the list
+            for opt, values in search_opts.iteritems():
+                try:
+                    filter_func = filter_mapping[opt]
+                except KeyError:
+                    # no such filter - ignore it, go to next filter
+                    continue
+                else:
+                    if filter_func(args, values):
+                        result[name] = args
+                        break
+        qos_specs = result
+    return qos_specs
+
+
+def get_qos_specs(ctxt, id):
+    """Retrieves single qos specs by id."""
+    if id is None:
+        msg = _("id cannot be None")
+        raise exception.InvalidQoSSpecs(reason=msg)
+
+    if ctxt is None:
+        ctxt = context.get_admin_context()
+
+    return db.qos_specs_get(ctxt, id)
+
+
+def get_qos_specs_by_name(context, name):
+    """Retrieves single qos specs by name."""
+    if name is None:
+        msg = _("name cannot be None")
+        raise exception.InvalidQoSSpecs(reason=msg)
+
+    return db.qos_specs_get_by_name(context, name)
index e0161c3a72a0c81c477df60ede0c48d138805134..01d9cdd288d0fbb9393a37938f61d1b0ae54fbb3 100644 (file)
@@ -168,3 +168,10 @@ def is_encrypted(context, volume_type_id):
 
     encryption = db.volume_type_encryption_get(context, volume_type_id)
     return encryption is not None
+
+
+def get_volume_type_qos_specs(volume_type_id):
+    ctxt = context.get_admin_context()
+    res = db.volume_type_qos_specs_get(ctxt,
+                                       volume_type_id)
+    return res