1# Copyright (c) 2013 eBay Inc.
2# Copyright (c) 2013 OpenStack Foundation
3#
4#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5#    not use this file except in compliance with the License. You may obtain
6#    a copy of the License at
7#
8#         http://www.apache.org/licenses/LICENSE-2.0
9#
10#    Unless required by applicable law or agreed to in writing, software
11#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13#    License for the specific language governing permissions and limitations
14#    under the License.
15
16"""The QoS Specs Implementation"""
17
18
19from oslo_db import exception as db_exc
20from oslo_log import log as logging
21
22from cinder import context
23from cinder import db
24from cinder import exception
25from cinder.i18n import _
26from cinder import objects
27from cinder import utils
28from cinder.volume import volume_types
29
30
31LOG = logging.getLogger(__name__)
32
33CONTROL_LOCATION = ['front-end', 'back-end', 'both']
34
35
36def create(context, name, specs=None):
37    """Creates qos_specs.
38
39    :param specs: Dictionary that contains specifications for QoS
40
41    Expected format of the input parameter:
42
43       .. code-block:: python
44
45          {
46             'consumer': 'front-end',
47             'total_iops_sec': 1000,
48             'total_bytes_sec': 1024000
49          }
50
51    """
52    # Validate the key-value pairs in the qos spec.
53    utils.validate_dictionary_string_length(specs)
54
55    consumer = specs.get('consumer')
56    if consumer:
57        # If we need to modify specs, copy so we don't cause unintended
58        # consequences for the caller
59        specs = specs.copy()
60        del specs['consumer']
61
62    values = dict(name=name, consumer=consumer, specs=specs)
63
64    LOG.debug("Dict for qos_specs: %s", values)
65    qos_spec = objects.QualityOfServiceSpecs(context, **values)
66    qos_spec.create()
67    return qos_spec
68
69
70def update(context, qos_specs_id, specs):
71    """Update qos specs.
72
73    :param specs: dictionary that contains key/value pairs for updating
74                  existing specs.
75          e.g. {'consumer': 'front-end',
76                'total_iops_sec': 500,
77                'total_bytes_sec': 512000,}
78    """
79    LOG.debug('qos_specs.update(): specs %s', specs)
80
81    try:
82        utils.validate_dictionary_string_length(specs)
83        qos_spec = objects.QualityOfServiceSpecs.get_by_id(context,
84                                                           qos_specs_id)
85
86        if 'consumer' in specs:
87            qos_spec.consumer = specs['consumer']
88            # If we need to modify specs, copy so we don't cause unintended
89            # consequences for the caller
90            specs = specs.copy()
91            del specs['consumer']
92
93        # Update any values in specs dict
94        qos_spec.specs.update(specs)
95
96        qos_spec.save()
97    except exception.InvalidInput as e:
98        raise exception.InvalidQoSSpecs(reason=e)
99    except db_exc.DBError:
100        LOG.exception('DB error:')
101        raise exception.QoSSpecsUpdateFailed(specs_id=qos_specs_id,
102                                             qos_specs=specs)
103
104    return qos_spec
105
106
107def delete(context, qos_specs_id, force=False):
108    """Marks qos specs as deleted.
109
110    'force' parameter is a flag to determine whether should destroy
111    should continue when there were entities associated with the qos specs.
112    force=True indicates caller would like to mark qos specs as deleted
113    even if there was entities associate with target qos specs.
114    Trying to delete a qos specs still associated with entities will
115    cause QoSSpecsInUse exception if force=False (default).
116    """
117    if qos_specs_id is None:
118        msg = _("id cannot be None")
119        raise exception.InvalidQoSSpecs(reason=msg)
120
121    qos_spec = objects.QualityOfServiceSpecs.get_by_id(
122        context, qos_specs_id)
123
124    qos_spec.destroy(force)
125
126
127def delete_keys(context, qos_specs_id, keys):
128    """Marks specified key of target qos specs as deleted."""
129    if qos_specs_id is None:
130        msg = _("id cannot be None")
131        raise exception.InvalidQoSSpecs(reason=msg)
132
133    qos_spec = objects.QualityOfServiceSpecs.get_by_id(context, qos_specs_id)
134
135    # Previous behavior continued to delete keys until it hit first unset one,
136    # so for now will mimic that. In the future it would be useful to have all
137    # or nothing deletion of keys (or at least delete all set keys),
138    # especially since order of keys from CLI to API is not preserved currently
139    try:
140        for key in keys:
141            try:
142                del qos_spec.specs[key]
143            except KeyError:
144                raise exception.QoSSpecsKeyNotFound(
145                    specs_key=key, specs_id=qos_specs_id)
146    finally:
147        qos_spec.save()
148
149
150def get_associations(context, qos_specs_id):
151    """Get all associations of given qos specs."""
152    try:
153        types = objects.VolumeTypeList.get_all_types_for_qos(context,
154                                                             qos_specs_id)
155    except db_exc.DBError:
156        LOG.exception('DB error:')
157        msg = _('Failed to get all associations of '
158                'qos specs %s') % qos_specs_id
159        LOG.warning(msg)
160        raise exception.CinderException(message=msg)
161
162    result = []
163    for vol_type in types:
164        result.append({
165            'association_type': 'volume_type',
166            'name': vol_type.name,
167            'id': vol_type.id
168        })
169
170    return result
171
172
173def associate_qos_with_type(context, specs_id, type_id):
174    """Associate qos_specs with volume type.
175
176    Associate target qos specs with specific volume type.
177
178    :param specs_id: qos specs ID to associate with
179    :param type_id: volume type ID to associate with
180    :raises VolumeTypeNotFound: if volume type doesn't exist
181    :raises QoSSpecsNotFound: if qos specs doesn't exist
182    :raises InvalidVolumeType: if volume type is already associated
183                               with qos specs other than given one.
184    :raises QoSSpecsAssociateFailed: if there was general DB error
185    """
186    try:
187        get_qos_specs(context, specs_id)
188        res = volume_types.get_volume_type_qos_specs(type_id)
189        if res.get('qos_specs', None):
190            if res['qos_specs'].get('id') != specs_id:
191                msg = (_("Type %(type_id)s is already associated with another "
192                         "qos specs: %(qos_specs_id)s") %
193                       {'type_id': type_id,
194                        'qos_specs_id': res['qos_specs']['id']})
195                raise exception.InvalidVolumeType(reason=msg)
196        else:
197            db.qos_specs_associate(context, specs_id, type_id)
198    except db_exc.DBError:
199        LOG.exception('DB error:')
200        LOG.warning('Failed to associate qos specs '
201                    '%(id)s with type: %(vol_type_id)s',
202                    dict(id=specs_id, vol_type_id=type_id))
203        raise exception.QoSSpecsAssociateFailed(specs_id=specs_id,
204                                                type_id=type_id)
205
206
207def disassociate_qos_specs(context, specs_id, type_id):
208    """Disassociate qos_specs from volume type."""
209    try:
210        get_qos_specs(context, specs_id)
211        db.qos_specs_disassociate(context, specs_id, type_id)
212    except db_exc.DBError:
213        LOG.exception('DB error:')
214        LOG.warning('Failed to disassociate qos specs '
215                    '%(id)s with type: %(vol_type_id)s',
216                    dict(id=specs_id, vol_type_id=type_id))
217        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
218                                                   type_id=type_id)
219
220
221def disassociate_all(context, specs_id):
222    """Disassociate qos_specs from all entities."""
223    try:
224        get_qos_specs(context, specs_id)
225        db.qos_specs_disassociate_all(context, specs_id)
226    except db_exc.DBError:
227        LOG.exception('DB error:')
228        LOG.warning('Failed to disassociate qos specs %s.', specs_id)
229        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
230                                                   type_id=None)
231
232
233def get_all_specs(context, filters=None, marker=None, limit=None, offset=None,
234                  sort_keys=None, sort_dirs=None):
235    """Get all non-deleted qos specs."""
236    return objects.QualityOfServiceSpecsList.get_all(
237        context, filters=filters, marker=marker, limit=limit, offset=offset,
238        sort_keys=sort_keys, sort_dirs=sort_dirs)
239
240
241def get_qos_specs(ctxt, spec_id):
242    """Retrieves single qos specs by id."""
243    if spec_id is None:
244        msg = _("id cannot be None")
245        raise exception.InvalidQoSSpecs(reason=msg)
246
247    if ctxt is None:
248        ctxt = context.get_admin_context()
249
250    return objects.QualityOfServiceSpecs.get_by_id(ctxt, spec_id)
251