Implement QoS support for volumes

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
This commit is contained in:
Zhiteng Huang 2013-05-18 22:21:28 +08:00 committed by Zhiteng Huang
parent 61db6db85e
commit d028556263
17 changed files with 2327 additions and 20 deletions

View File

@ -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

View 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)
###################

View 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.
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,

View File

@ -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

View 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'

View 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.")

View 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,

View File

@ -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')

View File

@ -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')

View 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": [],

View 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"))

View File

@ -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)

View 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)

View 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,
}

View 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.

250
cinder/volume/qos_specs.py Normal file
View File

@ -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)

View 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