From cd090780eb634b76e7ba0b9148479f94d719bd28 Mon Sep 17 00:00:00 2001 From: ricolin Date: Tue, 24 Nov 2015 16:09:06 +0800 Subject: [PATCH] Add resource OS::Cinder::QoSAssociation Add association resource between volume types and QoS specs. blueprint update-cinder-resources Change-Id: I448bfeed7914308779ab36fe33966e57acaec02b --- etc/heat/policy.json | 3 +- .../resources/openstack/cinder/qos_specs.py | 94 +++++++++++++++ heat/tests/common.py | 4 + heat/tests/openstack/cinder/test_qos_specs.py | 110 +++++++++++++++++- ...ate-cinder-resources-e23e62762f167d29.yaml | 5 + 5 files changed, 210 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/bp-update-cinder-resources-e23e62762f167d29.yaml diff --git a/etc/heat/policy.json b/etc/heat/policy.json index c9aae5ff79..7a498d4dbc 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -92,5 +92,6 @@ "resource_types:OS::Neutron::QoSPolicy": "rule:project_admin", "resource_types:OS::Neutron::QoSBandwidthLimitRule": "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" } diff --git a/heat/engine/resources/openstack/cinder/qos_specs.py b/heat/engine/resources/openstack/cinder/qos_specs.py index 70174a35f4..e77abb163c 100644 --- a/heat/engine/resources/openstack/cinder/qos_specs.py +++ b/heat/engine/resources/openstack/cinder/qos_specs.py @@ -12,9 +12,11 @@ # under the License. from heat.common.i18n import _ +from heat.engine import constraints from heat.engine import properties from heat.engine import resource from heat.engine import support +from heat.engine import translation class QoSSpecs(resource.Resource): @@ -89,7 +91,99 @@ class QoSSpecs(resource.Resource): 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(): return { 'OS::Cinder::QoSSpecs': QoSSpecs, + 'OS::Cinder::QoSAssociation': QoSAssociation, } diff --git a/heat/tests/common.py b/heat/tests/common.py index 2e631ff60b..23679e0815 100644 --- a/heat/tests/common.py +++ b/heat/tests/common.py @@ -245,6 +245,10 @@ class HeatTestCase(testscenarios.WithScenarios, validate = self.patchobject(cinder.VolumeConstraint, 'validate') validate.return_value = True + def stub_QoSSpecsConstraint_validate(self): + validate = self.patchobject(cinder.QoSSpecsConstraint, 'validate') + validate.return_value = True + def stub_SnapshotConstraint_validate(self): validate = self.patchobject( cinder.VolumeSnapshotConstraint, 'validate') diff --git a/heat/tests/openstack/cinder/test_qos_specs.py b/heat/tests/openstack/cinder/test_qos_specs.py index 094cc0a84b..e00eda73e1 100644 --- a/heat/tests/openstack/cinder/test_qos_specs.py +++ b/heat/tests/openstack/cinder/test_qos_specs.py @@ -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): @@ -55,12 +69,12 @@ class QoSSpecsTest(common.HeatTestCase): self.value = mock.MagicMock() self.value.id = '927202df-1afb-497f-8368-9c2d2f26e5db' 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 def test_resource_mapping(self): mapping = qos_specs.resource_mapping() - self.assertEqual(1, len(mapping)) + self.assertEqual(2, len(mapping)) self.assertEqual(qos_specs.QoSSpecs, mapping['OS::Cinder::QoSSpecs']) self.assertIsInstance(self.my_qos_specs, @@ -78,9 +92,9 @@ class QoSSpecsTest(common.HeatTestCase): def test_qos_specs_handle_update_specs(self): self._set_up_qos_specs_environment() resource_id = self.my_qos_specs.resource_id - prop_diff = {'specs': {"foo": "bar", "bar": "bar"}} - set_expected = {"bar": "bar"} - unset_expected = ["foo1"] + prop_diff = {'specs': {'foo': 'bar', 'bar': 'bar'}} + set_expected = {'bar': 'bar'} + unset_expected = ['foo1'] self.my_qos_specs.handle_update( 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 self.my_qos_specs.handle_delete() 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 + ) diff --git a/releasenotes/notes/bp-update-cinder-resources-e23e62762f167d29.yaml b/releasenotes/notes/bp-update-cinder-resources-e23e62762f167d29.yaml new file mode 100644 index 0000000000..45e1f7769d --- /dev/null +++ b/releasenotes/notes/bp-update-cinder-resources-e23e62762f167d29.yaml @@ -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.