diff --git a/lower-constraints.txt b/lower-constraints.txt index 0e02d68f39d..93423e4f03a 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -50,7 +50,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -neutron-lib==2.15.0 +neutron-lib==2.16.0 openstacksdk==0.31.2 os-client-config==1.28.0 os-ken==2.2.0 diff --git a/neutron/db/quota/driver.py b/neutron/db/quota/driver.py index 1da31ae9f83..3aafc2ec691 100644 --- a/neutron/db/quota/driver.py +++ b/neutron/db/quota/driver.py @@ -224,15 +224,13 @@ class DbQuotaDriver(quota_api.QuotaDriverAPI): requested_resources = (set(requested_resources) - unlimited_resources) # Gather current usage information - # TODO(salv-orlando): calling count() for every resource triggers - # multiple queries on quota usage. This should be improved, however - # this is not an urgent matter as the REST API currently only - # allows allocation of a resource at a time - # NOTE: pass plugin too for compatibility with CountableResource - # instances + # TODO(salv-orlando): calling get_resource_usage() for every + # resource triggers multiple queries on quota usage. This should be + # improved, however this is not an urgent matter as the REST API + # currently only allows allocation of a resource at a time current_usages = dict( - (resource, resources[resource].count( - context, plugin, project_id, resync_usage=False)) for + (resource, self.get_resource_usage(context, project_id, + resources, resource)) for resource in requested_resources) # Adjust for expired reservations. Apparently it is cheaper than # querying every time for active reservations and counting overall diff --git a/neutron/extensions/quota_check_limit.py b/neutron/extensions/quota_check_limit.py new file mode 100644 index 00000000000..c2db74a2b3f --- /dev/null +++ b/neutron/extensions/quota_check_limit.py @@ -0,0 +1,20 @@ +# Copyright (c) 2021 Red Hat, Inc. +# +# 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 neutron_lib.api.definitions import quota_check_limit as apidef +from neutron_lib.api import extensions + + +class Quota_check_limit(extensions.APIExtensionDescriptor): + api_definition = apidef diff --git a/neutron/extensions/quotasv2.py b/neutron/extensions/quotasv2.py index e0b9b11949e..242f07ac63d 100644 --- a/neutron/extensions/quotasv2.py +++ b/neutron/extensions/quotasv2.py @@ -131,6 +131,7 @@ class QuotaSetsController(wsgi.Controller): def update(self, request, id, body=None): validate_policy(request.context, "update_quota") + check_limit = body[self._resource_name].pop('check_limit', False) if self._update_extended_attributes: self._update_attributes() try: @@ -142,6 +143,20 @@ class QuotaSetsController(wsgi.Controller): "An exception happened while processing the request " "body. The exception message is [%s].", e) raise e + + if check_limit: + resources = resource_registry.get_all_resources() + for resource_name, limit in body[self._resource_name].items(): + resource_usage = self._driver.get_resource_usage( + request.context, id, resources, resource_name) + if resource_usage > limit: + msg = ('Quota limit %(limit)s for %(resource)s must be ' + 'greater than or equal to already used ' + '%(resource_usage)s' % + {'limit': limit, 'resource': resource_name, + 'resource_usage': resource_usage}) + raise webob.exc.HTTPBadRequest(msg) + for key, value in body[self._resource_name].items(): self._driver.update_quota_limit(request.context, id, key, value) return {self._resource_name: self._get_quotas(request, id)} diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 5d82b36fc47..f4d116dd011 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -46,6 +46,7 @@ from neutron_lib.api.definitions import port_security as psec from neutron_lib.api.definitions import portbindings from neutron_lib.api.definitions import portbindings_extended as pbe_ext from neutron_lib.api.definitions import provider_net +from neutron_lib.api.definitions import quota_check_limit from neutron_lib.api.definitions import rbac_address_groups as rbac_ag_apidef from neutron_lib.api.definitions import rbac_address_scope from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef @@ -227,6 +228,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, addrgrp_def.ALIAS, pnap_def.ALIAS, pdp_def.ALIAS, + quota_check_limit.ALIAS, ] # List of agent types for which all binding_failed ports should try to be diff --git a/neutron/tests/unit/extensions/test_quotasv2.py b/neutron/tests/unit/extensions/test_quotasv2.py index 6ad124a0f6e..c70ba5bb2b2 100644 --- a/neutron/tests/unit/extensions/test_quotasv2.py +++ b/neutron/tests/unit/extensions/test_quotasv2.py @@ -317,6 +317,24 @@ class QuotaExtensionDbTestCase(QuotaExtensionTestCase): quota = self.deserialize(res) self.assertEqual(100, quota['quota']['extra1']) + @mock.patch.object(driver_nolock.DbQuotaNoLockDriver, 'get_resource_usage') + def test_update_quotas_check_limit(self, mock_get_resource_usage): + tenant_id = 'tenant_id1' + env = {'neutron.context': context.Context('', tenant_id, + is_admin=True)} + quotas = {'quota': {'network': 100, 'check_limit': False}} + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), extra_environ=env, + expect_errors=False) + self.assertEqual(200, res.status_int) + + quotas = {'quota': {'network': 50, 'check_limit': True}} + mock_get_resource_usage.return_value = 51 + res = self.api.put(_get_path('quotas', id=tenant_id, fmt=self.fmt), + self.serialize(quotas), extra_environ=env, + expect_errors=True) + self.assertEqual(400, res.status_int) + def test_delete_quotas_with_admin(self): project_id = 'project_id1' env = {'neutron.context': context.Context('', project_id + '2', diff --git a/requirements.txt b/requirements.txt index 008dc5d690d..4e5e7fec7d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ Jinja2>=2.10 # BSD License (3 clause) keystonemiddleware>=5.1.0 # Apache-2.0 netaddr>=0.7.18 # BSD netifaces>=0.10.4 # MIT -neutron-lib>=2.15.0 # Apache-2.0 +neutron-lib>=2.16.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 tenacity>=6.0.0 # Apache-2.0 SQLAlchemy>=1.4.23 # MIT