From 7954bcf638317d1d3214d12f4c564ceee09d4972 Mon Sep 17 00:00:00 2001 From: Julian Sy Date: Fri, 19 Aug 2016 15:57:18 +0000 Subject: [PATCH] Add OS::Cinder::Quota resource This creates new resource type whose intended use case is for admin-only use and to manage the cinder quotas (gigabytes, volumes, snapshots). implements bp cinder-quota-resource Co-Authored-By: Andy Hsiang Co-Authored-By: Yosef Hoffman Change-Id: I49d01d229199d9c472dc59ba2bb95d455f6dfb76 --- etc/heat/policy.json | 1 + .../resources/openstack/cinder/quota.py | 128 +++++++++++++++++ heat/tests/openstack/cinder/test_quota.py | 135 ++++++++++++++++++ ...inder-quota-resource-f13211c04020cd0c.yaml | 5 + 4 files changed, 269 insertions(+) create mode 100644 heat/engine/resources/openstack/cinder/quota.py create mode 100644 heat/tests/openstack/cinder/test_quota.py create mode 100644 releasenotes/notes/cinder-quota-resource-f13211c04020cd0c.yaml diff --git a/etc/heat/policy.json b/etc/heat/policy.json index f114cab6f6..05bb5f717f 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -87,6 +87,7 @@ "resource_types:OS::Nova::Flavor": "rule:project_admin", "resource_types:OS::Cinder::EncryptedVolumeType": "rule:project_admin", "resource_types:OS::Cinder::VolumeType": "rule:project_admin", + "resource_types:OS::Cinder::Quota": "rule:project_admin", "resource_types:OS::Manila::ShareType": "rule:project_admin", "resource_types:OS::Neutron::QoSPolicy": "rule:project_admin", "resource_types:OS::Neutron::QoSBandwidthLimitRule": "rule:project_admin", diff --git a/heat/engine/resources/openstack/cinder/quota.py b/heat/engine/resources/openstack/cinder/quota.py new file mode 100644 index 0000000000..2e69f895c1 --- /dev/null +++ b/heat/engine/resources/openstack/cinder/quota.py @@ -0,0 +1,128 @@ +# +# 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 copy + +from heat.common import exception +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 CinderQuota(resource.Resource): + """A resource for creating cinder quotas. + + Cinder Quota is used to manage operational limits for projects. Currently, + this resource can manage Cinder's gigabytes, snapshots, and volumes + quotas. + + Note that default cinder security policy usage of this resource + is limited to being used by administrators only. Administrators should be + careful to create only one Cinder Quota resource per project, otherwise + it will be hard for them to manage the quota properly. + """ + + support_status = support.SupportStatus(version='7.0.0') + + default_client_name = 'cinder' + + entity = 'quotas' + + required_service_extension = 'os-quota-sets' + + PROPERTIES = (PROJECT, GIGABYTES, VOLUMES, SNAPSHOTS) = ( + 'project', 'gigabytes', 'volumes', 'snapshots' + ) + + properties_schema = { + PROJECT: properties.Schema( + properties.Schema.STRING, + _('OpenStack Keystone Project.'), + required=True, + constraints=[ + constraints.CustomConstraint('keystone.project') + ] + ), + GIGABYTES: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the amount of disk space (in Gigabytes). ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + VOLUMES: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of volumes. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + SNAPSHOTS: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of snapshots. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ) + } + + def translation_rules(self, props): + return [ + translation.TranslationRule( + props, + translation.TranslationRule.RESOLVE, + [self.PROJECT], + client_plugin=self.client_plugin('keystone'), + finder='get_project_id') + ] + + def handle_create(self): + self._set_quota() + self.resource_id_set(self.physical_resource_name()) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + self._set_quota(json_snippet.properties(self.properties_schema, + self.context)) + + def _set_quota(self, props=None): + if props is None: + props = self.properties + + args = copy.copy(props.data) + project = args.pop(self.PROJECT) + self.client().quotas.update(project, **args) + + def handle_delete(self): + self.client().quotas.delete(self.properties[self.PROJECT]) + + def validate(self): + super(CinderQuota, self).validate() + if len(self.properties.data) == 1: + raise exception.PropertyUnspecifiedError(self.GIGABYTES, + self.SNAPSHOTS, + self.VOLUMES) + + +def resource_mapping(): + return { + 'OS::Cinder::Quota': CinderQuota + } diff --git a/heat/tests/openstack/cinder/test_quota.py b/heat/tests/openstack/cinder/test_quota.py new file mode 100644 index 0000000000..24eb90a369 --- /dev/null +++ b/heat/tests/openstack/cinder/test_quota.py @@ -0,0 +1,135 @@ +# +# 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 +import six + +from heat.common import exception +from heat.common import template_format +from heat.engine.clients.os import cinder as c_plugin +from heat.engine.clients.os import keystone as k_plugin +from heat.engine import rsrc_defn +from heat.engine import stack as parser +from heat.engine import template +from heat.tests import common +from heat.tests import utils + +quota_template = ''' +heat_template_version: newton + +description: Sample cinder quota heat template + +resources: + my_quota: + type: OS::Cinder::Quota + properties: + project: demo + gigabytes: 1 + snapshots: 2 + volumes: 3 +''' + + +class CinderQuotaTest(common.HeatTestCase): + def setUp(self): + super(CinderQuotaTest, self).setUp() + + self.ctx = utils.dummy_context() + self.patchobject(c_plugin.CinderClientPlugin, 'has_extension', + return_value=True) + self.patchobject(k_plugin.KeystoneClientPlugin, 'get_project_id', + return_value='some_project_id') + tpl = template_format.parse(quota_template) + self.stack = parser.Stack( + self.ctx, 'cinder_quota_test_stack', + template.Template(tpl) + ) + + self.my_quota = self.stack['my_quota'] + cinder = mock.MagicMock() + self.cinderclient = mock.MagicMock() + self.my_quota.client = cinder + cinder.return_value = self.cinderclient + self.quotas = self.cinderclient.quotas + self.quota_set = mock.MagicMock() + self.quotas.update.return_value = self.quota_set + self.quotas.delete.return_value = self.quota_set + + def _test_validate(self, resource, error_msg): + exc = self.assertRaises(exception.StackValidationFailed, + resource.validate) + self.assertIn(error_msg, six.text_type(exc)) + + def _test_invalid_property(self, prop_name): + my_quota = self.stack['my_quota'] + props = self.stack.t.t['resources']['my_quota']['properties'].copy() + props[prop_name] = -2 + my_quota.t = my_quota.t.freeze(properties=props) + my_quota.reparse() + error_msg = ('Property error: resources.my_quota.properties.%s:' + ' -2 is out of range (min: -1, max: None)' % prop_name) + self._test_validate(my_quota, error_msg) + + def test_invalid_gigabytes(self): + self._test_invalid_property('gigabytes') + + def test_invalid_snapshots(self): + self._test_invalid_property('snapshots') + + def test_invalid_volumes(self): + self._test_invalid_property('volumes') + + def test_miss_all_quotas(self): + my_quota = self.stack['my_quota'] + props = self.stack.t.t['resources']['my_quota']['properties'].copy() + del props['gigabytes'], props['snapshots'], props['volumes'] + my_quota.t = my_quota.t.freeze(properties=props) + my_quota.reparse() + msg = ('At least one of the following properties must be specified: ' + 'gigabytes, snapshots, volumes.') + self.assertRaisesRegexp(exception.PropertyUnspecifiedError, msg, + my_quota.validate) + + def test_quota_handle_create(self): + self.my_quota.physical_resource_name = mock.MagicMock( + return_value='some_resource_id') + self.my_quota.reparse() + self.my_quota.handle_create() + self.quotas.update.assert_called_once_with( + 'some_project_id', + gigabytes=1, + snapshots=2, + volumes=3 + ) + self.assertEqual('some_resource_id', self.my_quota.resource_id) + + def test_quota_handle_update(self): + tmpl_diff = mock.MagicMock() + prop_diff = mock.MagicMock() + props = {'project': 'some_project_id', 'gigabytes': 2, + 'volumes': 4} + json_snippet = rsrc_defn.ResourceDefinition( + self.my_quota.name, + 'OS::Cinder::Quota', + properties=props) + self.my_quota.reparse() + self.my_quota.handle_update(json_snippet, tmpl_diff, prop_diff) + self.quotas.update.assert_called_once_with( + 'some_project_id', + gigabytes=2, + volumes=4 + ) + + def test_quota_handle_delete(self): + self.my_quota.reparse() + self.my_quota.handle_delete() + self.quotas.delete.assert_called_once_with('some_project_id') diff --git a/releasenotes/notes/cinder-quota-resource-f13211c04020cd0c.yaml b/releasenotes/notes/cinder-quota-resource-f13211c04020cd0c.yaml new file mode 100644 index 0000000000..2342a69094 --- /dev/null +++ b/releasenotes/notes/cinder-quota-resource-f13211c04020cd0c.yaml @@ -0,0 +1,5 @@ +--- +features: + - New resource ``OS::Cinder::Quota`` is added to manage cinder quotas. + Cinder quotas are operational limits to projects on cinder block storage + resources. These include gigabytes, snapshots, and volumes.