From bbb899a4f549957e4aa9f92018b94cc2d6f01283 Mon Sep 17 00:00:00 2001 From: "Haider, Nafiz (nh532m)" Date: Fri, 31 Jan 2020 18:19:41 -0600 Subject: [PATCH] Add Octavia Quota Resource for lbaas quota parameters Change-Id: Ib6423b9f14beefd3daf5ac5f516b49cdd9fa0a00 --- .../resources/openstack/octavia/quota.py | 150 ++++++++++++++++++ heat/policies/resource_types.py | 3 + heat/tests/openstack/octavia/test_quota.py | 140 ++++++++++++++++ ...tavia-quota-resource-52c1ea86f16d9513.yaml | 4 + 4 files changed, 297 insertions(+) create mode 100644 heat/engine/resources/openstack/octavia/quota.py create mode 100644 heat/tests/openstack/octavia/test_quota.py create mode 100644 releasenotes/notes/octavia-quota-resource-52c1ea86f16d9513.yaml diff --git a/heat/engine/resources/openstack/octavia/quota.py b/heat/engine/resources/openstack/octavia/quota.py new file mode 100644 index 0000000000..5951e5cd70 --- /dev/null +++ b/heat/engine/resources/openstack/octavia/quota.py @@ -0,0 +1,150 @@ +# +# 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 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 OctaviaQuota(resource.Resource): + """A resource for creating Octavia quotas. + + Ocatavia Quota is used to manage operational limits for Octavia. Currently, + this resource can manage Octavia's quotas for: + + - healthmonitor + - listener + - loadbalancer + - pool + - member + + Note that default octavia security policy usage of this resource + is limited to being used by administrators only. Administrators should be + careful to create only one Octavia Quota resource per project, otherwise + it will be hard for them to manage the quota properly. + """ + + support_status = support.SupportStatus(version='14.0.0') + + default_client_name = 'octavia' + + entity = 'quotas' + + PROPERTIES = ( + PROJECT, HEALTHMONITOR, LISTENER, LOADBALANCER, + POOL, MEMBER + ) = ( + 'project', 'healthmonitor', 'listener', 'loadbalancer', + 'pool', 'member' + ) + + properties_schema = { + PROJECT: properties.Schema( + properties.Schema.STRING, + _('Name or id of the project to set the quota for.'), + required=True, + constraints=[ + constraints.CustomConstraint('keystone.project') + ] + ), + HEALTHMONITOR: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of healthmonitors. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + LISTENER: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of listeners. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + LOADBALANCER: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of load balancers. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + POOL: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of pools. ' + 'Setting the value to -1 removes the limit.'), + constraints=[ + constraints.Range(min=-1), + ], + update_allowed=True + ), + MEMBER: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of m. ' + '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 + + kwargs = dict((k, v) for k, v in props.items() + if k != self.PROJECT and v is not None) + self.client().quotas.update(props.get(self.PROJECT), **kwargs) + + def handle_delete(self): + self.client().quotas.delete(self.properties[self.PROJECT]) + + def validate(self): + super(OctaviaQuota, self).validate() + if sum(1 for p in self.properties.values() if p is not None) <= 1: + raise exception.PropertyUnspecifiedError( + *sorted(set(self.PROPERTIES) - {self.PROJECT})) + + +def resource_mapping(): + return { + 'OS::Octavia::Quota': OctaviaQuota + } diff --git a/heat/policies/resource_types.py b/heat/policies/resource_types.py index 009d2a6a39..f943971108 100644 --- a/heat/policies/resource_types.py +++ b/heat/policies/resource_types.py @@ -35,6 +35,9 @@ resource_types_policies = [ policy.RuleDefault( name=POLICY_ROOT % 'OS::Nova::Quota', check_str=base.RULE_PROJECT_ADMIN), + policy.RuleDefault( + name=POLICY_ROOT % 'OS::Octavia::Quota', + check_str=base.RULE_PROJECT_ADMIN), policy.RuleDefault( name=POLICY_ROOT % 'OS::Manila::ShareType', check_str=base.RULE_PROJECT_ADMIN), diff --git a/heat/tests/openstack/octavia/test_quota.py b/heat/tests/openstack/octavia/test_quota.py new file mode 100644 index 0000000000..5d440c88d0 --- /dev/null +++ b/heat/tests/openstack/octavia/test_quota.py @@ -0,0 +1,140 @@ +# +# 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 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 octavia quota heat template + +resources: + my_quota: + type: OS::Octavia::Quota + properties: + project: demo + healthmonitor: 5 + listener: 5 + loadbalancer: 5 + pool: 5 + member: 5 +''' + +valid_properties = [ + 'healthmonitor', 'listener', 'loadbalancer', 'pool', 'member' +] + + +class OcataQuotaTest(common.HeatTestCase): + def setUp(self): + super(OcataQuotaTest, self).setUp() + + self.ctx = utils.dummy_context() + 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, 'ocata_quota_test_stack', + template.Template(tpl) + ) + + self.my_quota = self.stack['my_quota'] + ocata = mock.MagicMock() + self.ocataclient = mock.MagicMock() + self.my_quota.client = ocata + ocata.return_value = self.ocataclient + self.quotas = self.ocataclient.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_properties(self): + for prop in valid_properties: + self._test_invalid_property(prop) + + def test_miss_all_quotas(self): + my_quota = self.stack['my_quota'] + props = self.stack.t.t['resources']['my_quota']['properties'].copy() + for key in valid_properties: + if key in props: + del props[key] + my_quota.t = my_quota.t.freeze(properties=props) + my_quota.reparse() + msg = ('At least one of the following properties must be specified: ' + 'healthmonitor, listener, loadbalancer, member, pool.') + self.assertRaisesRegex(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', + healthmonitor=5, + listener=5, + loadbalancer=5, + pool=5, + member=5 + ) + 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', 'pool': 1, 'member': 2, + 'listener': 3, 'loadbalancer': 4, 'healthmonitor': 2} + json_snippet = rsrc_defn.ResourceDefinition( + self.my_quota.name, + 'OS::Octavia::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', + pool=1, + member=2, + listener=3, + loadbalancer=4, + healthmonitor=2 + ) + + 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/octavia-quota-resource-52c1ea86f16d9513.yaml b/releasenotes/notes/octavia-quota-resource-52c1ea86f16d9513.yaml new file mode 100644 index 0000000000..57e1dad18c --- /dev/null +++ b/releasenotes/notes/octavia-quota-resource-52c1ea86f16d9513.yaml @@ -0,0 +1,4 @@ +--- +features: + - New resource ``OS::Octavia::Quota`` is added to enable an admin to manage + Octavia service quotas for a specific project.