diff --git a/etc/heat/policy.json b/etc/heat/policy.json index f114cab6f6..a3d4cf6127 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -90,5 +90,6 @@ "resource_types:OS::Manila::ShareType": "rule:project_admin", "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::Nova::HostAggregate": "rule:project_admin", + "resource_types:OS::Cinder::QoSSpecs": "rule:project_admin" } diff --git a/heat/engine/resources/openstack/cinder/qos_specs.py b/heat/engine/resources/openstack/cinder/qos_specs.py new file mode 100644 index 0000000000..0a7cbdb155 --- /dev/null +++ b/heat/engine/resources/openstack/cinder/qos_specs.py @@ -0,0 +1,99 @@ +# +# 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 oslo_log import log as logging + +from heat.common.i18n import _ +from heat.engine import properties +from heat.engine import resource +from heat.engine import support + +LOG = logging.getLogger(__name__) + + +class QoSSpecs(resource.Resource): + """A resource for creating cinder QoS specs. + + Users can ask for a specific volume type. Part of that volume type is a + string that defines the QoS of the volume IO (fast, normal, or slow). + Backends that can handle all of the demands of the volume type become + candidates for scheduling. Usage of this resource restricted to admins + only by default policy. + """ + + support_status = support.SupportStatus(version='7.0.0') + + default_client_name = 'cinder' + entity = 'qos_specs' + required_service_extension = 'qos-specs' + + PROPERTIES = ( + NAME, SPECS, + ) = ( + 'name', 'specs', + ) + + properties_schema = { + NAME: properties.Schema( + properties.Schema.STRING, + _('Name of the QoS.'), + ), + SPECS: properties.Schema( + properties.Schema.MAP, + _('The specs key and value pairs of the QoS.'), + required=True, + update_allowed=True + ), + } + + def _find_diff(self, update_prps, stored_prps): + remove_prps = list( + set(stored_prps.keys() or []) - set(update_prps.keys() or []) + ) + add_prps = dict(set(update_prps.items() or []) - set( + stored_prps.items() or [])) + return add_prps, remove_prps + + def handle_create(self): + name = (self.properties[self.NAME] or + self.physical_resource_name()) + specs = self.properties[self.SPECS] + + qos = self.client().qos_specs.create(name, specs) + self.resource_id_set(qos.id) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + """Update the specs for QoS.""" + + new_specs = prop_diff.get(self.SPECS) + old_specs = self.properties[self.SPECS] + add_specs, remove_specs = self._find_diff(new_specs, old_specs) + if self.resource_id is not None: + # Set new specs to QoS Specs + if add_specs: + self.client().qos_specs.set_keys(self.resource_id, add_specs) + # Unset old specs from QoS Specs + if remove_specs: + self.client().qos_specs.unset_keys(self.resource_id, + remove_specs) + + def handle_delete(self): + if self.resource_id is not None: + self.client().qos_specs.disassociate_all(self.resource_id) + super(QoSSpecs, self).handle_delete() + + +def resource_mapping(): + return { + 'OS::Cinder::QoSSpecs': QoSSpecs, + } diff --git a/heat/tests/openstack/cinder/test_qos_specs.py b/heat/tests/openstack/cinder/test_qos_specs.py new file mode 100644 index 0000000000..094cc0a84b --- /dev/null +++ b/heat/tests/openstack/cinder/test_qos_specs.py @@ -0,0 +1,101 @@ +# +# 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 mock + +from heat.engine.clients.os import cinder as c_plugin +from heat.engine.resources.openstack.cinder import qos_specs +from heat.engine import stack +from heat.engine import template +from heat.tests import common +from heat.tests import utils + +QOS_SPECS_TEMPLATE = { + 'heat_template_version': '2015-10-15', + 'description': 'Cinder QoS specs creation example', + 'resources': { + 'my_qos_specs': { + 'type': 'OS::Cinder::QoSSpecs', + 'properties': { + 'name': 'foobar', + 'specs': {"foo": "bar", "foo1": "bar1"} + } + } + } +} + + +class QoSSpecsTest(common.HeatTestCase): + + def setUp(self): + super(QoSSpecsTest, self).setUp() + self.ctx = utils.dummy_context() + self.patchobject(c_plugin.CinderClientPlugin, 'has_extension', + return_value=True) + self.stack = stack.Stack( + self.ctx, 'cinder_qos_spec_test_stack', + template.Template(QOS_SPECS_TEMPLATE) + ) + self.my_qos_specs = self.stack['my_qos_specs'] + cinder_client = mock.MagicMock() + self.cinderclient = mock.MagicMock() + self.my_qos_specs.client = cinder_client + cinder_client.return_value = self.cinderclient + self.qos_specs = self.cinderclient.qos_specs + self.value = mock.MagicMock() + self.value.id = '927202df-1afb-497f-8368-9c2d2f26e5db' + self.value.name = 'foobar' + 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(qos_specs.QoSSpecs, + mapping['OS::Cinder::QoSSpecs']) + self.assertIsInstance(self.my_qos_specs, + qos_specs.QoSSpecs) + + def _set_up_qos_specs_environment(self): + self.qos_specs.create.return_value = self.value + self.my_qos_specs.handle_create() + + def test_qos_specs_handle_create_specs(self): + self._set_up_qos_specs_environment() + self.assertEqual(1, self.qos_specs.create.call_count) + self.assertEqual(self.value.id, self.my_qos_specs.resource_id) + + 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"] + + self.my_qos_specs.handle_update( + json_snippet=None, tmpl_diff=None, prop_diff=prop_diff + ) + self.qos_specs.set_keys.assert_called_once_with( + resource_id, + set_expected + ) + self.qos_specs.unset_keys.assert_called_once_with( + resource_id, + unset_expected + ) + + def test_qos_specs_handle_delete_specs(self): + self._set_up_qos_specs_environment() + 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)