diff --git a/etc/heat/policy.json b/etc/heat/policy.json index 787b7c6bb3..e8aad04fa4 100644 --- a/etc/heat/policy.json +++ b/etc/heat/policy.json @@ -88,6 +88,7 @@ "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::Neutron::Quota": "rule:project_admin", "resource_types:OS::Nova::Quota": "rule:project_admin", "resource_types:OS::Manila::ShareType": "rule:project_admin", "resource_types:OS::Neutron::QoSPolicy": "rule:project_admin", diff --git a/heat/engine/resources/openstack/neutron/quota.py b/heat/engine/resources/openstack/neutron/quota.py new file mode 100644 index 0000000000..b2c401d04f --- /dev/null +++ b/heat/engine/resources/openstack/neutron/quota.py @@ -0,0 +1,159 @@ +# +# 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.resources.openstack.neutron import neutron +from heat.engine import support +from heat.engine import translation + + +class NeutronQuota(neutron.NeutronResource): + """A resource for managing neutron quotas. + + Neutron Quota is used to manage operational limits for projects. Currently, + this resource can manage Neutron's quotas for: + - subnet + - network + - floatingip + - security_group_rule + - security_group + - router + - port + + Note that default neutron security policy usage of this resource + is limited to being used by administrators only. Administrators should be + careful to create only one Neutron Quota resource per project, otherwise + it will be hard for them to manage the quota properly. + """ + + support_status = support.SupportStatus(version='8.0.0') + + required_service_extension = 'quotas' + + PROPERTIES = ( + PROJECT, SUBNET, NETWORK, FLOATINGIP, SECURITY_GROUP_RULE, + SECURITY_GROUP, ROUTER, PORT + ) = ( + 'project', 'subnet', 'network', 'floatingip', 'security_group_rule', + 'security_group', 'router', 'port' + ) + + 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') + ] + ), + SUBNET: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of subnets. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + NETWORK: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of networks. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + FLOATINGIP: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of floating IPs. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + SECURITY_GROUP_RULE: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of security group rules. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + SECURITY_GROUP: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of security groups. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + ROUTER: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of routers. ' + 'Setting -1 means unlimited.'), + constraints=[constraints.Range(min=-1)], + update_allowed=True + ), + PORT: properties.Schema( + properties.Schema.INTEGER, + _('Quota for the number of ports. ' + 'Setting -1 means unlimited.'), + 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) + body = {"quota": args} + + self.client().update_quota(project, body) + + def handle_delete(self): + if self.resource_id is not None: + with self.client_plugin().ignore_not_found: + self.client().delete_quota(self.resource_id) + + def validate(self): + super(NeutronQuota, self).validate() + if len(self.properties.data) == 1: + raise exception.PropertyUnspecifiedError( + *sorted(set(self.PROPERTIES) - {self.PROJECT})) + + +def resource_mapping(): + return { + 'OS::Neutron::Quota': NeutronQuota + } diff --git a/heat/tests/openstack/neutron/test_quota.py b/heat/tests/openstack/neutron/test_quota.py new file mode 100644 index 0000000000..c3b01b48c3 --- /dev/null +++ b/heat/tests/openstack/neutron/test_quota.py @@ -0,0 +1,146 @@ +# +# 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.clients.os import neutron as n_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 neutron quota heat template + +resources: + my_quota: + type: OS::Neutron::Quota + properties: + project: demo + subnet: 5 + network: 5 + floatingip: 5 + security_group_rule: 5 + security_group: 5 + router: 5 + port: 5 +''' + +valid_properties = [ + 'subnet', 'network', 'floatingip', 'security_group_rule', + 'security_group', 'router', 'port' +] + + +class NeutronQuotaTest(common.HeatTestCase): + def setUp(self): + super(NeutronQuotaTest, self).setUp() + + self.ctx = utils.dummy_context() + self.patchobject(n_plugin.NeutronClientPlugin, 'has_extension', + return_value=True) + self.patchobject(n_plugin.NeutronClientPlugin, 'ignore_not_found', + return_value=None) + 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, 'neutron_quota_test_stack', + template.Template(tpl) + ) + + self.my_quota = self.stack['my_quota'] + neutron = mock.MagicMock() + self.neutronclient = mock.MagicMock() + self.my_quota.client = neutron + neutron.return_value = self.neutronclient + self.update_quota = self.neutronclient.update_quota + self.delete_quota = self.neutronclient.delete_quota + self.update_quota.return_value = mock.MagicMock() + self.delete_quota.return_value = mock.MagicMock() + + def _test_validate(self, resource, error_msg): + exc = self.assertRaises(exception.StackValidationFailed, + resource.validate) + self.assertIn(error_msg, six.text_type(exc)) + + 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: ' + 'floatingip, network, port, router, ' + 'security_group, security_group_rule, subnet.') + 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() + body = { + "quota": { + 'subnet': 5, + 'network': 5, + 'floatingip': 5, + 'security_group_rule': 5, + 'security_group': 5, + 'router': 5, + 'port': 5 + } + } + self.update_quota.assert_called_once_with( + 'some_project_id', + body + ) + 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', 'floatingip': 1, + 'security_group': 4} + json_snippet = rsrc_defn.ResourceDefinition( + self.my_quota.name, + 'OS::Neutron::Quota', + properties=props) + self.my_quota.reparse() + self.my_quota.handle_update(json_snippet, tmpl_diff, prop_diff) + body = { + "quota": { + 'floatingip': 1, + 'security_group': 4 + } + } + self.update_quota.assert_called_once_with( + 'some_project_id', + body + ) + + def test_quota_handle_delete(self): + self.my_quota.reparse() + self.my_quota.resource_id_set('some_project_id') + self.my_quota.handle_delete() + self.delete_quota.assert_called_once_with('some_project_id') diff --git a/releasenotes/notes/neutron-quota-resource-7fa5e4df8287bf77.yaml b/releasenotes/notes/neutron-quota-resource-7fa5e4df8287bf77.yaml new file mode 100644 index 0000000000..9d741f4f4a --- /dev/null +++ b/releasenotes/notes/neutron-quota-resource-7fa5e4df8287bf77.yaml @@ -0,0 +1,3 @@ +--- +features: + - New resource ``OS::Neutron::Quota`` is added to manage neutron quotas.