summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.zuul.yaml16
-rw-r--r--designate/api/v2/controllers/quotas.py10
-rw-r--r--designate/central/service.py8
-rw-r--r--designate/common/constants.py24
-rw-r--r--designate/objects/quota.py8
-rw-r--r--designate/quota/base.py2
-rw-r--r--designate/tests/__init__.py5
-rw-r--r--designate/tests/test_central/test_service.py4
-rw-r--r--designate/tests/test_quota/test_quota.py14
-rw-r--r--designate/tests/test_quota/test_storage.py81
-rw-r--r--designate/tests/unit/objects/test_quota.py93
-rw-r--r--designate/tests/unit/test_central/test_basic.py105
-rw-r--r--playbooks/enable-fips.yaml3
-rw-r--r--releasenotes/notes/Fix-recordset-records-quota-76ed3095dd2afbbe.yaml4
-rw-r--r--releasenotes/notes/Require-all-projects-for-set-quotas-with-non-project-scoped-tokens-ffe3082db3dbb55b.yaml6
-rw-r--r--tox.ini1
16 files changed, 334 insertions, 50 deletions
diff --git a/.zuul.yaml b/.zuul.yaml
index d1a70fc6..5fb90d98 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -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.
diff --git a/tox.ini b/tox.ini
index f44ba538..ce2f462f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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