diff options
-rw-r--r-- | .zuul.yaml | 16 | ||||
-rw-r--r-- | designate/api/v2/controllers/quotas.py | 10 | ||||
-rw-r--r-- | designate/central/service.py | 8 | ||||
-rw-r--r-- | designate/common/constants.py | 24 | ||||
-rw-r--r-- | designate/objects/quota.py | 8 | ||||
-rw-r--r-- | designate/quota/base.py | 2 | ||||
-rw-r--r-- | designate/tests/__init__.py | 5 | ||||
-rw-r--r-- | designate/tests/test_central/test_service.py | 4 | ||||
-rw-r--r-- | designate/tests/test_quota/test_quota.py | 14 | ||||
-rw-r--r-- | designate/tests/test_quota/test_storage.py | 81 | ||||
-rw-r--r-- | designate/tests/unit/objects/test_quota.py | 93 | ||||
-rw-r--r-- | designate/tests/unit/test_central/test_basic.py | 105 | ||||
-rw-r--r-- | playbooks/enable-fips.yaml | 3 | ||||
-rw-r--r-- | releasenotes/notes/Fix-recordset-records-quota-76ed3095dd2afbbe.yaml | 4 | ||||
-rw-r--r-- | releasenotes/notes/Require-all-projects-for-set-quotas-with-non-project-scoped-tokens-ffe3082db3dbb55b.yaml | 6 | ||||
-rw-r--r-- | tox.ini | 1 |
16 files changed, 334 insertions, 50 deletions
@@ -46,6 +46,14 @@ parent: designate-base - job: + name: designate-bind9-centos8stream-fips + parent: designate-bind9 + nodeset: devstack-single-node-centos-8-stream + description: | + Functional testing for a FIPS enabled Centos 8 stream system + pre-run: playbooks/enable-fips.yaml + +- job: name: designate-pdns4 post-run: playbooks/designate-pdns4/post.yaml parent: designate-base @@ -125,13 +133,14 @@ check: jobs: - designate-bind9 + - designate-bind9-centos8stream-fips: + voting: false - designate-pdns4 - designate-grenade-pdns4 - designate-ipv6-only-pdns4 - designate-ipv6-only-bind9 - + queue: designate gate: - queue: designate jobs: - designate-bind9 - designate-pdns4 @@ -151,8 +160,7 @@ check: jobs: - neutron-tempest-plugin-designate-scenario - gate: - queue: designate + queue: designate experimental: jobs: - designate-pdns4-postgres diff --git a/designate/api/v2/controllers/quotas.py b/designate/api/v2/controllers/quotas.py index c4a766f4..801a2d8a 100644 --- a/designate/api/v2/controllers/quotas.py +++ b/designate/api/v2/controllers/quotas.py @@ -19,6 +19,7 @@ from oslo_log import log as logging from designate.api.v2.controllers import rest from designate.common import keystone +from designate import exceptions from designate.objects.adapters import DesignateAdapter from designate.objects import QuotaList @@ -63,6 +64,15 @@ class QuotasController(rest.RestController): quotas = DesignateAdapter.parse('API_v2', body, QuotaList()) + # The get_quotas lookup will always return the default quotas + # if the context does not have a project_id (system scoped token) and + # the all_tenants boolean is false. Let's require all_tenants for + # contexts with no project ID. + if context.project_id is None and not context.all_tenants: + raise exceptions.MissingProjectID( + "The all-projects flag must be used when using non-project " + "scoped tokens.") + for quota in quotas: self.central_api.set_quota(context, tenant_id, quota.resource, quota.hard_limit) diff --git a/designate/central/service.py b/designate/central/service.py index 4d504be5..35cd0e3c 100644 --- a/designate/central/service.py +++ b/designate/central/service.py @@ -606,15 +606,17 @@ class Service(service.RPCService): criterion = {'tenant_id': tenant_id} count = self.storage.count_zones(context, criterion) - self.quota.limit_check(context, tenant_id, zones=count) + # Check if adding one more zone would exceed the quota + self.quota.limit_check(context, tenant_id, zones=count + 1) def _enforce_recordset_quota(self, context, zone): # Ensure the recordsets per zone quota is OK criterion = {'zone_id': zone.id} count = self.storage.count_recordsets(context, criterion) + # Check if adding one more recordset would exceed the quota self.quota.limit_check( - context, zone.tenant_id, zone_recordsets=count) + context, zone.tenant_id, zone_recordsets=count + 1) def _enforce_record_quota(self, context, zone, recordset): # Quotas don't apply to managed records. @@ -647,7 +649,7 @@ class Service(service.RPCService): # Ensure the records per recordset quota is OK self.quota.limit_check(context, zone.tenant_id, - recordset_records=recordset_records) + recordset_records=len(recordset.records)) # Misc Methods @rpc.expected_exceptions() diff --git a/designate/common/constants.py b/designate/common/constants.py new file mode 100644 index 00000000..295ee8b7 --- /dev/null +++ b/designate/common/constants.py @@ -0,0 +1,24 @@ +# Copyright 2021 Red Hat +# +# 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. + +# Quotas +MIN_QUOTA = -1 +MAX_QUOTA = 2147483647 +QUOTA_API_EXPORT_SIZE = 'api_export_size' +QUOTA_RECORDSET_RECORDS = 'recordset_records' +QUOTA_ZONE_RECORDS = 'zone_records' +QUOTA_ZONE_RECORDSETS = 'zone_recordsets' +QUOTA_ZONES = 'zones' +VALID_QUOTAS = [QUOTA_API_EXPORT_SIZE, QUOTA_RECORDSET_RECORDS, + QUOTA_ZONE_RECORDS, QUOTA_ZONE_RECORDSETS, QUOTA_ZONES] diff --git a/designate/objects/quota.py b/designate/objects/quota.py index 0ad30094..1d35f9d0 100644 --- a/designate/objects/quota.py +++ b/designate/objects/quota.py @@ -12,6 +12,7 @@ # 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 designate.common import constants from designate.objects import base from designate.objects import fields @@ -21,8 +22,11 @@ class Quota(base.DictObjectMixin, base.PersistentObjectMixin, base.DesignateObject): fields = { 'tenant_id': fields.AnyField(nullable=True), - 'resource': fields.AnyField(nullable=True), - 'hard_limit': fields.AnyField(nullable=True) + 'resource': fields.EnumField(nullable=True, + valid_values=constants.VALID_QUOTAS), + 'hard_limit': fields.IntegerFields(nullable=True, + minimum=constants.MIN_QUOTA, + maximum=constants.MAX_QUOTA) } STRING_KEYS = [ diff --git a/designate/quota/base.py b/designate/quota/base.py index 86dc553b..dc38c3a4 100644 --- a/designate/quota/base.py +++ b/designate/quota/base.py @@ -33,7 +33,7 @@ class Quota(DriverPlugin, metaclass=abc.ABCMeta): if resource in quotas: # Setting the resource quota to a negative value will make # the resource unlimited - if quotas[resource] >= 0 and value >= quotas[resource]: + if quotas[resource] >= 0 and value > quotas[resource]: raise exceptions.OverQuota() else: raise exceptions.QuotaResourceUnknown("%s is not a valid quota" diff --git a/designate/tests/__init__.py b/designate/tests/__init__.py index 64e709df..364fad82 100644 --- a/designate/tests/__init__.py +++ b/designate/tests/__init__.py @@ -28,6 +28,7 @@ from oslo_messaging import conffixture as messaging_fixture from oslotest import base from testtools import testcase +from designate.common import constants import designate.conf from designate import exceptions from designate import objects @@ -81,10 +82,10 @@ class TestCase(base.BaseTestCase): }] quota_fixtures = [{ - 'resource': 'zones', + 'resource': constants.QUOTA_ZONES, 'hard_limit': 5, }, { - 'resource': 'records', + 'resource': constants.QUOTA_ZONE_RECORDS, 'hard_limit': 50, }] diff --git a/designate/tests/test_central/test_service.py b/designate/tests/test_central/test_service.py index 26694b9a..2c6f8cf5 100644 --- a/designate/tests/test_central/test_service.py +++ b/designate/tests/test_central/test_service.py @@ -1986,7 +1986,7 @@ class CentralServiceTest(CentralTestCase): def test_create_record_and_update_over_zone_quota(self): # SOA and NS Records exist - self.config(quota_zone_records=1) + self.config(quota_zone_records=0) # Creating the zone automatically creates SOA & NS records zone = self.create_zone() @@ -2023,7 +2023,7 @@ class CentralServiceTest(CentralTestCase): self.assertEqual(exceptions.OverQuota, exc.exc_info[0]) def test_create_record_over_recordset_quota(self): - self.config(quota_recordset_records=1) + self.config(quota_recordset_records=0) # Creating the zone automatically creates SOA & NS records zone = self.create_zone() diff --git a/designate/tests/test_quota/test_quota.py b/designate/tests/test_quota/test_quota.py index 039dbf7e..56074239 100644 --- a/designate/tests/test_quota/test_quota.py +++ b/designate/tests/test_quota/test_quota.py @@ -82,13 +82,13 @@ class QuotaTestCase(tests.TestCase): with testtools.ExpectedException(exceptions.OverQuota): self.quota.limit_check(context, 'tenant_id', - zones=cfg.CONF.quota_zones) + zones=cfg.CONF.quota_zones + 1) with testtools.ExpectedException(exceptions.OverQuota): self.quota.limit_check( context, 'tenant_id', - zone_records=cfg.CONF.quota_zone_records) + zone_records=cfg.CONF.quota_zone_records + 1) def test_limit_check_unlimited(self): context = self.get_admin_context() @@ -119,16 +119,16 @@ class QuotaTestCase(tests.TestCase): } self.quota.get_quotas.return_value = ret with testtools.ExpectedException(exceptions.OverQuota): - self.quota.limit_check(context, 'tenant_id', zones=0) + self.quota.limit_check(context, 'tenant_id', zones=1) with testtools.ExpectedException(exceptions.OverQuota): - self.quota.limit_check(context, 'tenant_id', zone_recordsets=0) + self.quota.limit_check(context, 'tenant_id', zone_recordsets=1) with testtools.ExpectedException(exceptions.OverQuota): - self.quota.limit_check(context, 'tenant_id', zone_records=0) + self.quota.limit_check(context, 'tenant_id', zone_records=1) with testtools.ExpectedException(exceptions.OverQuota): self.quota.limit_check(context, 'tenant_id', - recordset_records=0) + recordset_records=1) with testtools.ExpectedException(exceptions.OverQuota): - self.quota.limit_check(context, 'tenant_id', api_export_size=0) + self.quota.limit_check(context, 'tenant_id', api_export_size=1) def test_limit_check_over(self): context = self.get_admin_context() diff --git a/designate/tests/test_quota/test_storage.py b/designate/tests/test_quota/test_storage.py index 08dc178a..92c4461f 100644 --- a/designate/tests/test_quota/test_storage.py +++ b/designate/tests/test_quota/test_storage.py @@ -15,6 +15,7 @@ # under the License. from oslo_log import log as logging +from designate.common import constants from designate import quota from designate import tests @@ -27,49 +28,75 @@ class StorageQuotaTest(tests.TestCase): self.config(quota_driver='storage') self.quota = quota.get_quota() - def test_set_quota_create(self): + def test_set_quota_create_min(self): context = self.get_admin_context() context.all_tenants = True - quota = self.quota.set_quota(context, 'tenant_id', 'zones', 1500) + for current_quota in constants.VALID_QUOTAS: + quota = self.quota.set_quota(context, 'tenant_id', + current_quota, constants.MIN_QUOTA) - self.assertEqual({'zones': 1500}, quota) + self.assertEqual({current_quota: constants.MIN_QUOTA}, quota) - # Drop into the storage layer directly to ensure the quota was created - # successfully - criterion = { - 'tenant_id': 'tenant_id', - 'resource': 'zones' - } + # Drop into the storage layer directly to ensure the quota was + # created successfully + criterion = { + 'tenant_id': 'tenant_id', + 'resource': current_quota + } + + quota = self.quota.storage.find_quota(context, criterion) + + self.assertEqual('tenant_id', quota['tenant_id']) + self.assertEqual(current_quota, quota['resource']) + self.assertEqual(constants.MIN_QUOTA, quota['hard_limit']) + + def test_set_quota_create_max(self): + context = self.get_admin_context() + context.all_tenants = True - quota = self.quota.storage.find_quota(context, criterion) + for current_quota in constants.VALID_QUOTAS: + quota = self.quota.set_quota(context, 'tenant_id', + current_quota, constants.MAX_QUOTA) - self.assertEqual('tenant_id', quota['tenant_id']) - self.assertEqual('zones', quota['resource']) - self.assertEqual(1500, quota['hard_limit']) + self.assertEqual({current_quota: constants.MAX_QUOTA}, quota) + + # Drop into the storage layer directly to ensure the quota was + # created successfully + criterion = { + 'tenant_id': 'tenant_id', + 'resource': current_quota + } + + quota = self.quota.storage.find_quota(context, criterion) + + self.assertEqual('tenant_id', quota['tenant_id']) + self.assertEqual(current_quota, quota['resource']) + self.assertEqual(constants.MAX_QUOTA, quota['hard_limit']) def test_set_quota_update(self): context = self.get_admin_context() context.all_tenants = True - # First up, Create the quota - self.quota.set_quota(context, 'tenant_id', 'zones', 1500) + for current_quota in constants.VALID_QUOTAS: + # First up, Create the quota + self.quota.set_quota(context, 'tenant_id', current_quota, 1500) - # Next, update the quota - self.quota.set_quota(context, 'tenant_id', 'zones', 1234) + # Next, update the quota + self.quota.set_quota(context, 'tenant_id', current_quota, 1234) - # Drop into the storage layer directly to ensure the quota was updated - # successfully - criterion = { - 'tenant_id': 'tenant_id', - 'resource': 'zones' - } + # Drop into the storage layer directly to ensure the quota was + # updated successfully + criterion = { + 'tenant_id': 'tenant_id', + 'resource': current_quota + } - quota = self.quota.storage.find_quota(context, criterion) + quota = self.quota.storage.find_quota(context, criterion) - self.assertEqual('tenant_id', quota['tenant_id']) - self.assertEqual('zones', quota['resource']) - self.assertEqual(1234, quota['hard_limit']) + self.assertEqual('tenant_id', quota['tenant_id']) + self.assertEqual(current_quota, quota['resource']) + self.assertEqual(1234, quota['hard_limit']) def test_reset_quotas(self): context = self.get_admin_context() diff --git a/designate/tests/unit/objects/test_quota.py b/designate/tests/unit/objects/test_quota.py new file mode 100644 index 00000000..17a3de98 --- /dev/null +++ b/designate/tests/unit/objects/test_quota.py @@ -0,0 +1,93 @@ +# 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 +import oslotest.base + +from designate.common import constants +from designate import objects + +LOG = logging.getLogger(__name__) + + +class QuotaTest(oslotest.base.BaseTestCase): + def test_quota_min(self): + for current_quota in constants.VALID_QUOTAS: + quota = objects.Quota(tenant_id='123', resource=current_quota, + hard_limit=constants.MIN_QUOTA) + + self.assertEqual('123', quota.tenant_id) + self.assertEqual(current_quota, quota.resource) + self.assertEqual(constants.MIN_QUOTA, quota.hard_limit) + + def test_quota_max(self): + for current_quota in constants.VALID_QUOTAS: + quota = objects.Quota(tenant_id='123', resource=current_quota, + hard_limit=constants.MAX_QUOTA) + + self.assertEqual('123', quota.tenant_id) + self.assertEqual(current_quota, quota.resource) + self.assertEqual(constants.MAX_QUOTA, quota.hard_limit) + + def test_quota_too_small(self): + for current_quota in constants.VALID_QUOTAS: + self.assertRaises(ValueError, objects.Quota, tenant_id='123', + resource=current_quota, + hard_limit=constants.MIN_QUOTA - 1) + + def test_quota_too_large(self): + for current_quota in constants.VALID_QUOTAS: + self.assertRaises(ValueError, objects.Quota, tenant_id='123', + resource=current_quota, + hard_limit=constants.MAX_QUOTA + 1) + + def test_quota_invalid(self): + for current_quota in constants.VALID_QUOTAS: + self.assertRaises(ValueError, objects.Quota, tenant_id='123', + resource=current_quota, + hard_limit='bogus') + + def test_quota_list(self): + quotas = objects.QuotaList() + quotas.append(objects.Quota( + tenant_id='123', resource=constants.QUOTA_RECORDSET_RECORDS)) + quotas.append(objects.Quota(tenant_id='123', + resource=constants.QUOTA_ZONE_RECORDS)) + quotas.append(objects.Quota(tenant_id='123', + resource=constants.QUOTA_ZONE_RECORDSETS)) + + self.assertEqual(constants.QUOTA_RECORDSET_RECORDS, quotas[0].resource) + self.assertEqual(constants.QUOTA_ZONE_RECORDS, quotas[1].resource) + self.assertEqual(constants.QUOTA_ZONE_RECORDSETS, quotas[2].resource) + + def test_quota_list_from_dict(self): + quotas = objects.QuotaList().from_dict({ + constants.QUOTA_ZONES: 100, + constants.QUOTA_ZONE_RECORDSETS: 101, + constants.QUOTA_ZONE_RECORDS: 102, + constants.QUOTA_RECORDSET_RECORDS: 103, + constants.QUOTA_API_EXPORT_SIZE: 104, + }) + + self.assertEqual(constants.QUOTA_ZONES, quotas[0].resource) + self.assertEqual(100, quotas[0].hard_limit) + self.assertEqual(constants.QUOTA_API_EXPORT_SIZE, quotas[4].resource) + self.assertEqual(104, quotas[4].hard_limit) + + def test_quota_list_to_dict(self): + quotas = objects.QuotaList().from_dict({ + constants.QUOTA_ZONES: 100, + constants.QUOTA_ZONE_RECORDSETS: 101, + }) + + self.assertEqual(100, quotas.to_dict()[constants.QUOTA_ZONES]) + self.assertEqual(101, + quotas.to_dict()[constants.QUOTA_ZONE_RECORDSETS]) diff --git a/designate/tests/unit/test_central/test_basic.py b/designate/tests/unit/test_central/test_basic.py index b7711955..688ca340 100644 --- a/designate/tests/unit/test_central/test_basic.py +++ b/designate/tests/unit/test_central/test_basic.py @@ -2190,13 +2190,20 @@ class CentralStatusTests(CentralBasic): class CentralQuotaTest(unittest.TestCase): def setUp(self): + self.CONF = cfg_fixture.Config(cfg.CONF) + cfg.CONF([], project='designate') + self.CONF.config(quota_driver="noop") self.context = mock.Mock() self.zone = mock.Mock() + self.quotas_of_one = {'zones': 1, + 'zone_recordsets': 1, + 'zone_records': 1, + 'recordset_records': 1, + 'api_export_size': 1} @patch('designate.central.service.storage') @patch('designate.central.service.quota') def test_zone_record_quota_allows_lowering_value(self, quota, storage): - cfg.CONF([], project='designate') service = Service() service.storage.count_records.return_value = 10 @@ -2220,6 +2227,100 @@ class CentralQuotaTest(unittest.TestCase): # Check the recordset limit as well check_recordset_records = mock.call( - self.context, self.zone.tenant_id, recordset_records=10 + self.context, self.zone.tenant_id, recordset_records=5 ) assert check_recordset_records in service.quota.limit_check.mock_calls + + @patch('designate.quota.base.Quota.get_quotas') + @patch('designate.central.service.storage') + def test_enforce_zone_quota(self, storage, mock_get_quotas): + service = Service() + mock_get_quotas.return_value = self.quotas_of_one + + # Test creating one zone, 1 quota, no existing zones + service.storage.count_zones.return_value = 0 + self.assertIsNone(service._enforce_zone_quota(self.context, + 'fake_project_id')) + + # Test creating one zone, 1 quota, one existing zone + service.storage.count_zones.return_value = 1 + self.assertRaises(exceptions.OverQuota, service._enforce_zone_quota, + self.context, 'fake_project_id') + + @patch('designate.quota.base.Quota.get_quotas') + @patch('designate.central.service.storage') + def test_enforce_recordset_quota(self, storage, mock_get_quotas): + service = Service() + mock_get_quotas.return_value = self.quotas_of_one + + # Test creating one recordset, 1 quota, no existing recordsets + service.storage.count_recordsets.return_value = 0 + self.assertIsNone(service._enforce_recordset_quota(self.context, + self.zone)) + + # Test creating one recordset, 1 quota, one existing recordset + service.storage.count_recordsets.return_value = 1 + self.assertRaises(exceptions.OverQuota, + service._enforce_recordset_quota, + self.context, self.zone) + + @patch('designate.quota.base.Quota.get_quotas') + @patch('designate.central.service.storage') + def test_enforce_record_quota(self, storage, mock_get_quotas): + service = Service() + mock_get_quotas.return_value = self.quotas_of_one + + service.storage.count_records.side_effect = [ + 0, 0, + 1, 0, + 0, 1, + 1, 1, + 1, 1, + ] + + managed_recordset = mock.Mock() + managed_recordset.managed = True + + recordset_one_record = mock.Mock() + recordset_one_record.managed = False + recordset_one_record.records = ['192.0.2.1'] + + # Test that managed recordsets have no quota limit + self.assertIsNone(service._enforce_record_quota(self.context, + self.zone, + managed_recordset)) + service.storage.count_records.assert_not_called() + + # Test creating recordset with one record, no existing zone records, + # no existing recordsets + self.assertIsNone(service._enforce_record_quota(self.context, + self.zone, + recordset_one_record)) + + # Test creating recordset with one record, one existing zone record, + # no exiting recordsets + self.assertRaises(exceptions.OverQuota, service._enforce_record_quota, + self.context, self.zone, recordset_one_record) + + # Test creating recordset with one record, one existing zone record, + # no exiting recordsets + # Note: Recordsets replace the existing recordset + self.assertIsNone(service._enforce_record_quota(self.context, + self.zone, + recordset_one_record)) + + # Test creating recordset with one record, no existing zone record, + # one exiting recordsets + # Note: Recordsets replace the existing recordset + self.assertIsNone(service._enforce_record_quota(self.context, + self.zone, + recordset_one_record)) + + recordset_two_record = mock.Mock() + recordset_two_record.managed = False + recordset_two_record.records = ['192.0.2.1', '192.0.2.2'] + + # Test creating recordset with two records, one existing zone record, + # one exiting recordsets + self.assertRaises(exceptions.OverQuota, service._enforce_record_quota, + self.context, self.zone, recordset_two_record) diff --git a/playbooks/enable-fips.yaml b/playbooks/enable-fips.yaml new file mode 100644 index 00000000..bc1dc04e --- /dev/null +++ b/playbooks/enable-fips.yaml @@ -0,0 +1,3 @@ +- hosts: all + roles: + - enable-fips diff --git a/releasenotes/notes/Fix-recordset-records-quota-76ed3095dd2afbbe.yaml b/releasenotes/notes/Fix-recordset-records-quota-76ed3095dd2afbbe.yaml new file mode 100644 index 00000000..f1958cb9 --- /dev/null +++ b/releasenotes/notes/Fix-recordset-records-quota-76ed3095dd2afbbe.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixed an issue that caused the recordset_records quota to not be enforced. diff --git a/releasenotes/notes/Require-all-projects-for-set-quotas-with-non-project-scoped-tokens-ffe3082db3dbb55b.yaml b/releasenotes/notes/Require-all-projects-for-set-quotas-with-non-project-scoped-tokens-ffe3082db3dbb55b.yaml new file mode 100644 index 00000000..eaf17979 --- /dev/null +++ b/releasenotes/notes/Require-all-projects-for-set-quotas-with-non-project-scoped-tokens-ffe3082db3dbb55b.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed an issue where set-quotas will always return the default quotas if + it was called with a non-project scoped token and the all-projects flag + was not set. @@ -43,6 +43,7 @@ allowlist_externals = [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/xena} + -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = rm -rf doc/build |