Add resource OS::Cinder::QoSAssociation

Add association resource between volume types and QoS specs.

blueprint update-cinder-resources

Change-Id: I448bfeed7914308779ab36fe33966e57acaec02b
This commit is contained in:
ricolin 2015-11-24 16:09:06 +08:00
parent 49226daee0
commit cd090780eb
5 changed files with 210 additions and 6 deletions

View File

@ -92,5 +92,6 @@
"resource_types:OS::Neutron::QoSPolicy": "rule:project_admin", "resource_types:OS::Neutron::QoSPolicy": "rule:project_admin",
"resource_types:OS::Neutron::QoSBandwidthLimitRule": "rule:project_admin", "resource_types:OS::Neutron::QoSBandwidthLimitRule": "rule:project_admin",
"resource_types:OS::Nova::HostAggregate": "rule:project_admin", "resource_types:OS::Nova::HostAggregate": "rule:project_admin",
"resource_types:OS::Cinder::QoSSpecs": "rule:project_admin" "resource_types:OS::Cinder::QoSSpecs": "rule:project_admin",
"resource_types:OS::Cinder::QoSAssociation": "rule:project_admin"
} }

View File

@ -12,9 +12,11 @@
# under the License. # under the License.
from heat.common.i18n import _ from heat.common.i18n import _
from heat.engine import constraints
from heat.engine import properties from heat.engine import properties
from heat.engine import resource from heat.engine import resource
from heat.engine import support from heat.engine import support
from heat.engine import translation
class QoSSpecs(resource.Resource): class QoSSpecs(resource.Resource):
@ -89,7 +91,99 @@ class QoSSpecs(resource.Resource):
super(QoSSpecs, self).handle_delete() super(QoSSpecs, self).handle_delete()
class QoSAssociation(resource.Resource):
"""A resource to associate cinder QoS specs with volume types.
Usage of this resource restricted to admins only by default policy.
"""
support_status = support.SupportStatus(version='8.0.0')
default_client_name = 'cinder'
required_service_extension = 'qos-specs'
PROPERTIES = (
QOS_SPECS, VOLUME_TYPES,
) = (
'qos_specs', 'volume_types',
)
properties_schema = {
QOS_SPECS: properties.Schema(
properties.Schema.STRING,
_('ID or Name of the QoS specs.'),
required=True,
constraints=[
constraints.CustomConstraint('cinder.qos_specs')
],
),
VOLUME_TYPES: properties.Schema(
properties.Schema.LIST,
_('List of volume type IDs or Names to be attached to QoS specs.'),
schema=properties.Schema(
properties.Schema.STRING,
_('A volume type to attach specs.'),
constraints=[
constraints.CustomConstraint('cinder.vtype')
],
),
update_allowed=True,
required=True,
),
}
def translation_rules(self, props):
return [
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.VOLUME_TYPES],
client_plugin=self.client_plugin(),
finder='get_volume_type'
),
translation.TranslationRule(
props,
translation.TranslationRule.RESOLVE,
[self.QOS_SPECS],
client_plugin=self.client_plugin(),
finder='get_qos_specs'
)
]
def _find_diff(self, update_prps, stored_prps):
add_prps = list(set(update_prps or []) - set(stored_prps or []))
remove_prps = list(set(stored_prps or []) - set(update_prps or []))
return add_prps, remove_prps
def handle_create(self):
for vt in self.properties[self.VOLUME_TYPES]:
self.client().qos_specs.associate(self.properties[self.QOS_SPECS],
vt)
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""Associate volume types to QoS."""
qos_specs = self.properties[self.QOS_SPECS]
new_associate_vts = prop_diff.get(self.VOLUME_TYPES)
old_associate_vts = self.properties[self.VOLUME_TYPES]
add_associate_vts, remove_associate_vts = self._find_diff(
new_associate_vts, old_associate_vts)
for vt in add_associate_vts:
self.client().qos_specs.associate(qos_specs, vt)
for vt in remove_associate_vts:
self.client().qos_specs.disassociate(qos_specs, vt)
def handle_delete(self):
volume_types = self.properties[self.VOLUME_TYPES]
for vt in volume_types:
self.client().qos_specs.disassociate(
self.properties[self.QOS_SPECS], vt)
def resource_mapping(): def resource_mapping():
return { return {
'OS::Cinder::QoSSpecs': QoSSpecs, 'OS::Cinder::QoSSpecs': QoSSpecs,
'OS::Cinder::QoSAssociation': QoSAssociation,
} }

View File

@ -245,6 +245,10 @@ class HeatTestCase(testscenarios.WithScenarios,
validate = self.patchobject(cinder.VolumeConstraint, 'validate') validate = self.patchobject(cinder.VolumeConstraint, 'validate')
validate.return_value = True validate.return_value = True
def stub_QoSSpecsConstraint_validate(self):
validate = self.patchobject(cinder.QoSSpecsConstraint, 'validate')
validate.return_value = True
def stub_SnapshotConstraint_validate(self): def stub_SnapshotConstraint_validate(self):
validate = self.patchobject( validate = self.patchobject(
cinder.VolumeSnapshotConstraint, 'validate') cinder.VolumeSnapshotConstraint, 'validate')

View File

@ -34,6 +34,20 @@ QOS_SPECS_TEMPLATE = {
} }
} }
QOS_ASSOCIATE_TEMPLATE = {
'heat_template_version': '2015-10-15',
'description': 'Cinder QoS specs association example',
'resources': {
'my_qos_associate': {
'type': 'OS::Cinder::QoSAssociation',
'properties': {
'volume_types': ['ceph', 'lvm'],
'qos_specs': 'foobar'
}
}
}
}
class QoSSpecsTest(common.HeatTestCase): class QoSSpecsTest(common.HeatTestCase):
@ -55,12 +69,12 @@ class QoSSpecsTest(common.HeatTestCase):
self.value = mock.MagicMock() self.value = mock.MagicMock()
self.value.id = '927202df-1afb-497f-8368-9c2d2f26e5db' self.value.id = '927202df-1afb-497f-8368-9c2d2f26e5db'
self.value.name = 'foobar' self.value.name = 'foobar'
self.value.specs = {"foo": "bar", "foo1": "bar1"} self.value.specs = {'foo': 'bar', 'foo1': 'bar1'}
self.qos_specs.create.return_value = self.value self.qos_specs.create.return_value = self.value
def test_resource_mapping(self): def test_resource_mapping(self):
mapping = qos_specs.resource_mapping() mapping = qos_specs.resource_mapping()
self.assertEqual(1, len(mapping)) self.assertEqual(2, len(mapping))
self.assertEqual(qos_specs.QoSSpecs, self.assertEqual(qos_specs.QoSSpecs,
mapping['OS::Cinder::QoSSpecs']) mapping['OS::Cinder::QoSSpecs'])
self.assertIsInstance(self.my_qos_specs, self.assertIsInstance(self.my_qos_specs,
@ -78,9 +92,9 @@ class QoSSpecsTest(common.HeatTestCase):
def test_qos_specs_handle_update_specs(self): def test_qos_specs_handle_update_specs(self):
self._set_up_qos_specs_environment() self._set_up_qos_specs_environment()
resource_id = self.my_qos_specs.resource_id resource_id = self.my_qos_specs.resource_id
prop_diff = {'specs': {"foo": "bar", "bar": "bar"}} prop_diff = {'specs': {'foo': 'bar', 'bar': 'bar'}}
set_expected = {"bar": "bar"} set_expected = {'bar': 'bar'}
unset_expected = ["foo1"] unset_expected = ['foo1']
self.my_qos_specs.handle_update( self.my_qos_specs.handle_update(
json_snippet=None, tmpl_diff=None, prop_diff=prop_diff json_snippet=None, tmpl_diff=None, prop_diff=prop_diff
@ -99,3 +113,89 @@ class QoSSpecsTest(common.HeatTestCase):
resource_id = self.my_qos_specs.resource_id resource_id = self.my_qos_specs.resource_id
self.my_qos_specs.handle_delete() self.my_qos_specs.handle_delete()
self.qos_specs.disassociate_all.assert_called_once_with(resource_id) self.qos_specs.disassociate_all.assert_called_once_with(resource_id)
class QoSAssociationTest(common.HeatTestCase):
def setUp(self):
super(QoSAssociationTest, self).setUp()
self.ctx = utils.dummy_context()
self.qos_specs_id = 'foobar'
self.patchobject(c_plugin.CinderClientPlugin, 'has_extension',
return_value=True)
self.patchobject(c_plugin.CinderClientPlugin, 'get_qos_specs',
return_value=self.qos_specs_id)
self.stack = stack.Stack(
self.ctx, 'cinder_qos_associate_test_stack',
template.Template(QOS_ASSOCIATE_TEMPLATE)
)
self.my_qos_associate = self.stack['my_qos_associate']
cinder_client = mock.MagicMock()
self.cinderclient = mock.MagicMock()
self.my_qos_associate.client = cinder_client
cinder_client.return_value = self.cinderclient
self.qos_specs = self.cinderclient.qos_specs
self.stub_QoSSpecsConstraint_validate()
self.stub_VolumeTypeConstraint_validate()
self.vt_ceph = 'ceph'
self.vt_lvm = 'lvm'
self.vt_new_ceph = 'new_ceph'
def test_resource_mapping(self):
mapping = qos_specs.resource_mapping()
self.assertEqual(2, len(mapping))
self.assertEqual(qos_specs.QoSAssociation,
mapping['OS::Cinder::QoSAssociation'])
self.assertIsInstance(self.my_qos_associate,
qos_specs.QoSAssociation)
def _set_up_qos_associate_environment(self):
self.my_qos_associate.handle_create()
def test_qos_associate_handle_create(self):
self.patchobject(c_plugin.CinderClientPlugin, 'get_volume_type',
side_effect=[self.vt_ceph, self.vt_lvm])
self._set_up_qos_associate_environment()
self.cinderclient.qos_specs.associate.assert_any_call(
self.qos_specs_id,
self.vt_ceph
)
self.qos_specs.associate.assert_any_call(
self.qos_specs_id,
self.vt_lvm
)
def test_qos_associate_handle_update(self):
self.patchobject(c_plugin.CinderClientPlugin, 'get_volume_type',
side_effect=[self.vt_lvm, self.vt_ceph,
self.vt_new_ceph,
self.vt_ceph])
self._set_up_qos_associate_environment()
prop_diff = {'volume_types': [self.vt_lvm, self.vt_new_ceph]}
self.my_qos_associate.handle_update(
json_snippet=None, tmpl_diff=None, prop_diff=prop_diff
)
self.qos_specs.associate.assert_any_call(
self.qos_specs_id,
self.vt_new_ceph
)
self.qos_specs.disassociate.assert_any_call(
self.qos_specs_id,
self.vt_ceph
)
def test_qos_associate_handle_delete_specs(self):
self.patchobject(c_plugin.CinderClientPlugin, 'get_volume_type',
side_effect=[self.vt_ceph, self.vt_lvm,
self.vt_ceph, self.vt_lvm])
self._set_up_qos_associate_environment()
self.my_qos_associate.handle_delete()
self.qos_specs.disassociate.assert_any_call(
self.qos_specs_id,
self.vt_ceph
)
self.qos_specs.disassociate.assert_any_call(
self.qos_specs_id,
self.vt_lvm
)

View File

@ -0,0 +1,5 @@
---
features:
- OS::Cinder::QoSAssociation resource plugin is added to support cinder QoS
Specs Association with Volume Types, which is provided by cinder
``qos-specs`` API extension.