Add volume type support

This patch adds support for Cinder VolumeType's extra specs and QoS
specs.

These parameters must be set when creating a volume, and must be passed
as dictionaries using parameters "extra_specs" and "qos_specs" on the
"create_volume" call.
This commit is contained in:
Gorka Eguileor 2018-10-30 15:03:06 +01:00
parent 9f8d4b8753
commit e53227af2a
5 changed files with 217 additions and 3 deletions

View File

@ -301,10 +301,32 @@ class Volume(NamedObject):
self._snapshots = []
self._connections = []
qos_specs = kwargs.pop('qos_specs', None)
extra_specs = kwargs.pop('extra_specs', {})
super(Volume, self).__init__(backend_or_vol, **kwargs)
self._populate_data()
self.local_attach = None
if qos_specs or extra_specs:
if qos_specs:
qos_specs = cinder_objs.QualityOfServiceSpecs(
id=self.id, name=self.id,
consumer='back-end', specs=qos_specs)
qos_specs_id = self.id
else:
qos_specs = qos_specs_id = None
self._ovo.volume_type = cinder_objs.VolumeType(
context=self.CONTEXT,
is_public=True,
id=self.id,
name=self.id,
qos_specs_id=qos_specs_id,
extra_specs=extra_specs,
qos_specs=qos_specs)
self._ovo.volume_type_id = self.id
@property
def snapshots(self):
# Lazy loading

View File

@ -21,6 +21,8 @@ from oslo_utils import timeutils
from oslo_versionedobjects import fields
import six
from cinderlib import serialization
class PersistenceDriverBase(object):
"""Provide Metadata Persistency for our resources.
@ -51,6 +53,10 @@ class PersistenceDriverBase(object):
def set_volume(self, volume):
self.reset_change_tracker(volume)
if volume.volume_type:
volume.volume_type.obj_reset_changes()
if volume.volume_type.qos_specs_id:
volume.volume_type.qos_specs.obj_reset_changes()
def set_snapshot(self, snapshot):
self.reset_change_tracker(snapshot)
@ -94,6 +100,13 @@ class PersistenceDriverBase(object):
result = {key: getattr(resource._ovo, key)
for key in resource._changed_fields
if not isinstance(resource.fields[key], fields.ObjectField)}
if getattr(resource._ovo, 'volume_type_id', None):
if ('qos_specs' in resource.volume_type._changed_fields
and resource.volume_type.qos_specs):
result['qos_specs'] = resource._ovo.volume_type.qos_specs.specs
if ('extra_specs' in resource.volume_type._changed_fields
and resource.volume_type.extra_specs):
result['extra_specs'] = resource._ovo.volume_type.extra_specs
return result
def get_fields(self, resource):
@ -104,6 +117,10 @@ class PersistenceDriverBase(object):
key not in getattr(resource, 'obj_extra_fields', []) and not
isinstance(resource.fields[key], fields.ObjectField))
}
if getattr(resource._ovo, 'volume_type_id', None):
result['extra_specs'] = resource._ovo.volume_type.extra_specs
if resource._ovo.volume_type.qos_specs_id:
result['qos_specs'] = resource._ovo.volume_type.qos_specs.specs
return result
@ -142,6 +159,27 @@ class DB(object):
def snapshot_get(self, context, snapshot_id, *args, **kwargs):
return self.persistence.get_snapshots(snapshot_id)[0]._ovo
def get_volume_type(self, context, id, inactive=False,
expected_fields=None):
res = self.persistence.get_volumes(id)[0]._ovo
if not res.volume_type_id:
return None
return self._vol_type_to_dict(res.volume_type)
def qos_specs_get(self, context, qos_specs_id, inactive=False):
res = self.persistence.get_volumes(qos_specs_id)[0]._ovo
if not res.volume_type_id:
return None
return self._vol_type_to_dict(res.volume_type)['qos_specs']
@staticmethod
def _vol_type_to_dict(volume_type):
res = serialization.obj_to_primitive(volume_type)
res = res['versioned_object.data']
if res.get('qos_specs'):
res['qos_specs'] = res['qos_specs']['versioned_object.data']
return res
@classmethod
def image_volume_cache_get_by_volume_id(cls, context, volume_id):
return None

View File

@ -88,6 +88,10 @@ class DBPersistence(persistence_base.PersistenceDriverBase):
ovos = cinder_objs.VolumeList.get_all(objects.CONTEXT, filters=filters)
result = [objects.Volume(ovo.availability_zone, __ovo=ovo)
for ovo in ovos.objects]
for r in result:
if r.volume_type_id:
r.volume_type.extra_specs # Trigger extra specs load
r.volume_type.qos_specs # Trigger qos specs load
return result
def get_snapshots(self, snapshot_id=None, snapshot_name=None,
@ -129,7 +133,41 @@ class DBPersistence(persistence_base.PersistenceDriverBase):
if not changed:
changed = self.get_fields(volume)
# Create
extra_specs = changed.pop('extra_specs', None)
qos_specs = changed.pop('qos_specs', None)
# Since OVOs are not tracking QoS or Extra specs dictionary changes,
# we only support setting QoS or Extra specs on creation or add them
# later.
if changed.get('volume_type_id'):
vol_type_fields = {'id': volume.volume_type_id,
'name': volume.volume_type_id,
'extra_specs': extra_specs,
'is_public': True}
if qos_specs:
res = self.db.qos_specs_create(objects.CONTEXT,
{'name': volume.volume_type_id,
'consumer': 'back-end',
'specs': qos_specs})
# Cinder is automatically generating an ID, replace it
query = sqla_api.model_query(objects.CONTEXT,
models.QualityOfServiceSpecs)
query.filter_by(id=res['id']).update(
{'id': volume.volume_type.qos_specs_id})
self.db.volume_type_create(objects.CONTEXT, vol_type_fields)
else:
if extra_specs is not None:
self.db.volume_type_extra_specs_update_or_create(
objects.CONTEXT, volume.volume_type_id, extra_specs)
self.db.qos_specs_update(objects.CONTEXT,
volume.volume_type.qos_specs_id,
{'name': volume.volume_type_id,
'consumer': 'back-end',
'specs': qos_specs})
# Create the volume
if 'id' in changed:
LOG.debug('set_volume creating %s', changed)
try:
@ -200,10 +238,36 @@ class DBPersistence(persistence_base.PersistenceDriverBase):
if self.soft_deletes:
LOG.debug('soft deleting volume %s', volume.id)
self.db.volume_destroy(objects.CONTEXT, volume.id)
if volume.volume_type_id:
LOG.debug('soft deleting volume type %s',
volume.volume_type_id)
self.db.volume_destroy(objects.CONTEXT, volume.volume_type_id)
if volume.volume_type.qos_specs_id:
self.db.qos_specs_delete(objects.CONTEXT,
volume.volume_type.qos_specs_id)
else:
LOG.debug('hard deleting volume %s', volume.id)
query = sqla_api.model_query(objects.CONTEXT, models.Volume)
query.filter_by(id=volume.id).delete()
if volume.volume_type_id:
LOG.debug('hard deleting volume type %s',
volume.volume_type_id)
query = sqla_api.model_query(objects.CONTEXT,
models.VolumeTypeExtraSpecs)
query.filter_by(volume_type_id=volume.volume_type_id).delete()
query = sqla_api.model_query(objects.CONTEXT,
models.VolumeType)
query.filter_by(id=volume.volume_type_id).delete()
query = sqla_api.model_query(objects.CONTEXT,
models.QualityOfServiceSpecs)
qos_id = volume.volume_type.qos_specs_id
if qos_id:
query.filter(sqla_api.or_(
models.QualityOfServiceSpecs.id == qos_id,
models.QualityOfServiceSpecs.specs_id == qos_id
)).delete()
super(DBPersistence, self).delete_volume(volume)
def delete_snapshot(self, snapshot):

View File

@ -144,6 +144,13 @@ Some of the fields we could be interested in are:
- `bootable`: Not relevant for *cinderlib*, but maybe useful for the
*cinderlib* user.
- `extra_specs`: Extra volume configuration used by some drivers to specify
additional information, such as compression, deduplication, etc. Key-Value
pairs are driver specific.
- `qos_specs`: Backend QoS configuration. Dictionary with driver specific
key-value pares that enforced by the backend.
.. note::
*Cinderlib* automatically generates a UUID for the `id` if one is not

View File

@ -53,14 +53,16 @@ class BasePersistenceTest(unittest2.TestCase):
return self.create_volumes([{'size': i, 'name': 'disk%s' % i}
for i in range(1, n+1)])
def create_volumes(self, data):
def create_volumes(self, data, sort=True):
vols = []
for d in data:
d.setdefault('backend_or_vol', self.backend)
vol = cinderlib.Volume(**d)
vols.append(vol)
self.persistence.set_volume(vol)
return self.sorted(vols)
if sort:
return self.sorted(vols)
return vols
def create_snapshots(self):
vols = self.create_n_volumes(2)
@ -185,6 +187,87 @@ class BasePersistenceTest(unittest2.TestCase):
volume_id=vols[0].id)
self.assertListEqualObj([], res)
def _check_volume_type(self, extra_specs, qos_specs, vol):
self.assertEqual(vol.id, vol.volume_type.id)
self.assertEqual(vol.id, vol.volume_type.name)
self.assertTrue(vol.volume_type.is_public)
self.assertEqual(extra_specs, vol.volume_type.extra_specs)
if qos_specs:
self.assertEqual(vol.id, vol.volume_type.qos_specs_id)
self.assertEqual(vol.id, vol.volume_type.qos_specs.id)
self.assertEqual(vol.id, vol.volume_type.qos_specs.name)
self.assertEqual('back-end', vol.volume_type.qos_specs.consumer)
self.assertEqual(qos_specs, vol.volume_type.qos_specs.specs)
else:
self.assertIsNone(vol.volume_type.qos_specs_id)
def test_get_volumes_extra_specs(self):
extra_specs = [{'k1': 'v1', 'k2': 'v2'},
{'kk1': 'vv1', 'kk2': 'vv2', 'kk3': 'vv3'}]
vols = self.create_volumes(
[{'size': 1, 'extra_specs': extra_specs[0]},
{'size': 2, 'extra_specs': extra_specs[1]}],
sort=False)
# Check the volume type and the extra specs on created volumes
for i in range(len(vols)):
self._check_volume_type(extra_specs[i], None, vols[i])
# Check that we get what we stored
res = self.persistence.get_volumes(backend_name=self.backend.id)
vols = self.sorted(vols)
self.assertListEqualObj(vols, self.sorted(res))
for i in range(len(vols)):
self._check_volume_type(vols[i].volume_type.extra_specs, {},
vols[i])
def test_get_volumes_qos_specs(self):
qos_specs = [{'q1': 'r1', 'q2': 'r2'},
{'qq1': 'rr1', 'qq2': 'rr2', 'qq3': 'rr3'}]
vols = self.create_volumes(
[{'size': 1, 'qos_specs': qos_specs[0]},
{'size': 2, 'qos_specs': qos_specs[1]}],
sort=False)
# Check the volume type and the extra specs on created volumes
for i in range(len(vols)):
self._check_volume_type({}, qos_specs[i], vols[i])
# Check that we get what we stored
res = self.persistence.get_volumes(backend_name=self.backend.id)
vols = self.sorted(vols)
res = self.sorted(res)
self.assertListEqualObj(vols, res)
for i in range(len(vols)):
self._check_volume_type({}, vols[i].volume_type.qos_specs.specs,
vols[i])
def test_get_volumes_extra_and_qos_specs(self):
qos_specs = [{'q1': 'r1', 'q2': 'r2'},
{'qq1': 'rr1', 'qq2': 'rr2', 'qq3': 'rr3'}]
extra_specs = [{'k1': 'v1', 'k2': 'v2'},
{'kk1': 'vv1', 'kk2': 'vv2', 'kk3': 'vv3'}]
vols = self.create_volumes(
[{'size': 1, 'qos_specs': qos_specs[0],
'extra_specs': extra_specs[0]},
{'size': 2, 'qos_specs': qos_specs[1],
'extra_specs': extra_specs[1]}],
sort=False)
# Check the volume type and the extra specs on created volumes
for i in range(len(vols)):
self._check_volume_type(extra_specs[i], qos_specs[i], vols[i])
# Check that we get what we stored
res = self.persistence.get_volumes(backend_name=self.backend.id)
vols = self.sorted(vols)
self.assertListEqualObj(vols, self.sorted(res))
for i in range(len(vols)):
self._check_volume_type(vols[i].volume_type.extra_specs,
vols[i].volume_type.qos_specs.specs,
vols[i])
def test_delete_volume(self):
vols = self.create_n_volumes(2)
self.persistence.delete_volume(vols[0])