diff --git a/doc/source/user/proxies/dns.rst b/doc/source/user/proxies/dns.rst index a629375ed..0229eb4ef 100644 --- a/doc/source/user/proxies/dns.rst +++ b/doc/source/user/proxies/dns.rst @@ -83,6 +83,13 @@ Limit Operations :noindex: :members: limits +Quota Operations +^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.dns.v2._proxy.Proxy + :noindex: + :members: quotas, get_quota, update_quota, delete_quota + Service Status Operations ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/dns/index.rst b/doc/source/user/resources/dns/index.rst index 90a58a82d..576c16c9f 100644 --- a/doc/source/user/resources/dns/index.rst +++ b/doc/source/user/resources/dns/index.rst @@ -13,5 +13,6 @@ DNS Resources v2/tld v2/recordset v2/limit + v2/quota v2/service_status v2/blacklist diff --git a/doc/source/user/resources/dns/v2/quota.rst b/doc/source/user/resources/dns/v2/quota.rst new file mode 100644 index 000000000..9ad1f6304 --- /dev/null +++ b/doc/source/user/resources/dns/v2/quota.rst @@ -0,0 +1,12 @@ +openstack.dns.v2.quota +====================== + +.. automodule:: openstack.dns.v2.quota + +The Quota Class +--------------- + +The ``Quota`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.dns.v2.quota.Quota + :members: diff --git a/openstack/dns/v2/_proxy.py b/openstack/dns/v2/_proxy.py index a28f235f0..e0a561454 100644 --- a/openstack/dns/v2/_proxy.py +++ b/openstack/dns/v2/_proxy.py @@ -15,6 +15,7 @@ import typing as ty from openstack.dns.v2 import blacklist as _blacklist from openstack.dns.v2 import floating_ip as _fip from openstack.dns.v2 import limit as _limit +from openstack.dns.v2 import quota as _quota from openstack.dns.v2 import recordset as _rs from openstack.dns.v2 import service_status as _svc_status from openstack.dns.v2 import tld as _tld @@ -34,6 +35,7 @@ class Proxy(proxy.Proxy): "blacklist": _blacklist.Blacklist, "floating_ip": _fip.FloatingIP, "limits": _limit.Limit, + "quota": _quota.Quota, "recordset": _rs.Recordset, "service_status": _svc_status.ServiceStatus, "zone": _zone.Zone, @@ -699,6 +701,60 @@ class Proxy(proxy.Proxy): """ return self._list(_limit.Limit, **query) + # ======== Quotas ======== + def quotas(self, **query): + """Return a generator of quotas + + :param dict query: Optional query parameters to be sent to limit the + resources being returned. + + :returns: A generator of quota objects + :rtype: :class:`~openstack.dns.v2.quota.Quota` + """ + return self._list(_quota.Quota, **query) + + def get_quota(self, quota): + """Get a quota + + :param quota: The value can be the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID for the quota. + + :returns: One :class:`~openstack.dns.v2.quota.Quota` + :raises: :class:`~openstack.exceptions.ResourceNotFound` + """ + return self._get(_quota.Quota, quota) + + def update_quota(self, quota, **attrs): + """Update a quota + + :param quota: Either the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. The ID of a quota + is the same as the project ID for the quota. + :param dict attrs: The attributes to update on the quota represented + by ``quota``. + + :returns: The updated quota + :rtype: :class:`~openstack.dns.v2.quota.Quota` + """ + return self._update(_quota.Quota, quota, **attrs) + + def delete_quota(self, quota, ignore_missing=True): + """Delete a quota (i.e. reset to the default quota) + + :param quota: The value can be the ID of a quota or a + :class:`~openstack.dns.v2.quota.Quota` instance. + The ID of a quota is the same as the project ID for the quota. + :param bool ignore_missing: When set to ``False``, + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the quota does not exist. + When set to ``True``, no exception will be set when attempting to + delete a nonexistent quota. + + :returns: ``None`` + """ + return self._delete(_quota.Quota, quota, ignore_missing=ignore_missing) + # ======== Service Statuses ======== def service_statuses(self): """Retrieve a generator of service statuses diff --git a/openstack/dns/v2/quota.py b/openstack/dns/v2/quota.py new file mode 100644 index 000000000..3e8e70e80 --- /dev/null +++ b/openstack/dns/v2/quota.py @@ -0,0 +1,106 @@ +# 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 typing as ty + +from keystoneauth1 import adapter +import typing_extensions as ty_ext + +from openstack.dns.v2 import _base +from openstack import resource + + +class Quota(_base.Resource): + """DNS Quota Resource""" + + base_path = "/quotas" + + # capabilities + allow_fetch = True + allow_commit = True + allow_delete = True + allow_list = True + commit_method = "PATCH" + + # Properties + #: The ID of the project. + project = resource.URI("project", alternate_id=True) + #: The maximum amount of recordsets allowed in a zone export. *Type: int* + api_export_size = resource.Body("api_export_size", type=int) + #: The maximum amount of records allowed per recordset. *Type: int* + recordset_records = resource.Body("recordset_records", type=int) + #: The maximum amount of records allowed per zone. *Type: int* + zone_records = resource.Body("zone_records", type=int) + #: The maximum amount of recordsets allowed per zone. *Type: int* + zone_recordsets = resource.Body("zone_recordsets", type=int) + #: The maximum amount of zones allowed per project. *Type: int* + zones = resource.Body("zones", type=int) + + def _prepare_request( + self, + requires_id=True, + prepend_key=False, + patch=False, + base_path=None, + params=None, + *, + resource_request_key=None, + **kwargs, + ): + _request = super()._prepare_request( + requires_id, prepend_key, base_path=base_path + ) + if self.resource_key in _request.body: + _body = _request.body[self.resource_key] + else: + _body = _request.body + if "id" in _body: + del _body["id"] + _request.headers = {'x-auth-sudo-project-id': self.id} + return _request + + def fetch( + self, + session: adapter.Adapter, + requires_id: bool = True, + base_path: str | None = None, + error_message: str | None = None, + skip_cache: bool = False, + *, + resource_response_key: str | None = None, + microversion: str | None = None, + **params: ty.Any, + ) -> ty_ext.Self: + request = self._prepare_request( + requires_id=requires_id, + base_path=base_path, + ) + session = self._get_session(session) + if microversion is None: + microversion = self._get_microversion(session) + self.microversion = microversion + + response = session.get( + request.url, + microversion=microversion, + params=params, + skip_cache=skip_cache, + headers=request.headers, + ) + + self._translate_response( + response, + error_message=error_message, + resource_response_key=resource_response_key, + ) + + return self diff --git a/openstack/tests/functional/dns/v2/test_quota.py b/openstack/tests/functional/dns/v2/test_quota.py new file mode 100644 index 000000000..3a642a60c --- /dev/null +++ b/openstack/tests/functional/dns/v2/test_quota.py @@ -0,0 +1,71 @@ +# 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 openstack.tests.functional import base + + +class TestQuota(base.BaseFunctionalTest): + def setUp(self): + super().setUp() + + self.require_service("dns") + if not self._operator_cloud_name: + self.skip("Operator cloud must be set for this test") + + self.project = self.create_temporary_project() + + def test_quota(self): + # set quota + + attrs = { + "api_export_size": 1, + "recordset_records": 2, + "zone_records": 3, + "zone_recordsets": 4, + "zones": 5, + } + new_quota = self.operator_cloud.dns.update_quota( + self.project.id, **attrs + ) + self.assertEqual(attrs["api_export_size"], new_quota.api_export_size) + self.assertEqual( + attrs["recordset_records"], new_quota.recordset_records + ) + self.assertEqual(attrs["zone_records"], new_quota.zone_records) + self.assertEqual(attrs["zone_recordsets"], new_quota.zone_recordsets) + self.assertEqual(attrs["zones"], new_quota.zones) + + # get quota + + expected_keys = [ + "id", + "api_export_size", + "recordset_records", + "zone_records", + "zone_recordsets", + "zones", + ] + test_quota = self.operator_cloud.dns.get_quota(self.project.id) + for actual_key in test_quota._body.attributes.keys(): + self.assertIn(actual_key, expected_keys) + self.assertEqual(self.project.id, test_quota.id) + self.assertEqual(attrs["api_export_size"], test_quota.api_export_size) + self.assertEqual( + attrs["recordset_records"], test_quota.recordset_records + ) + self.assertEqual(attrs["zone_records"], test_quota.zone_records) + self.assertEqual(attrs["zone_recordsets"], test_quota.zone_recordsets) + self.assertEqual(attrs["zones"], test_quota.zones) + + # reset quota + + self.operator_cloud.dns.delete_quota(self.project.id) diff --git a/openstack/tests/unit/dns/v2/test_proxy.py b/openstack/tests/unit/dns/v2/test_proxy.py index 14773738d..5874ce1d0 100644 --- a/openstack/tests/unit/dns/v2/test_proxy.py +++ b/openstack/tests/unit/dns/v2/test_proxy.py @@ -13,6 +13,7 @@ from openstack.dns.v2 import _proxy from openstack.dns.v2 import blacklist from openstack.dns.v2 import floating_ip +from openstack.dns.v2 import quota from openstack.dns.v2 import recordset from openstack.dns.v2 import service_status from openstack.dns.v2 import tld @@ -424,3 +425,30 @@ class TestDnsTLD(TestDnsProxy): def test_tld_update(self): self.verify_update(self.proxy.update_tld, tld.TLD) + + +class TestDnsQuota(TestDnsProxy): + def test_quotas(self): + self.verify_list(self.proxy.quotas, quota.Quota) + + def test_quota_get(self): + self.verify_get(self.proxy.get_quota, quota.Quota) + + def test_quota_update(self): + self.verify_update(self.proxy.update_quota, quota.Quota) + + def test_quota_delete(self): + self.verify_delete( + self.proxy.delete_quota, + quota.Quota, + False, + expected_kwargs={'ignore_missing': False}, + ) + + def test_quota_delete_ignore(self): + self.verify_delete( + self.proxy.delete_quota, + quota.Quota, + True, + expected_kwargs={'ignore_missing': True}, + ) diff --git a/openstack/tests/unit/dns/v2/test_quota.py b/openstack/tests/unit/dns/v2/test_quota.py new file mode 100644 index 000000000..e788b02cf --- /dev/null +++ b/openstack/tests/unit/dns/v2/test_quota.py @@ -0,0 +1,51 @@ +# 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 openstack.dns.v2 import quota +from openstack.tests.unit import base + +IDENTIFIER = "IDENTIFIER" +EXAMPLE = { + "zones": 10, + "zone_recordsets": 500, + "zone_records": 500, + "recordset_records": 20, + "api_export_size": 1000, +} + + +class TestQuota(base.TestCase): + def test_basic(self): + sot = quota.Quota() + self.assertIsNone(sot.resources_key) + self.assertIsNone(sot.resource_key) + self.assertEqual("/quotas", sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_commit) + self.assertTrue(sot.allow_delete) + self.assertTrue(sot.allow_list) + self.assertTrue(sot.commit_method, "PATCH") + + def test_make_it(self): + sot = quota.Quota(project='FAKE_PROJECT', **EXAMPLE) + self.assertEqual(EXAMPLE['zones'], sot.zones) + self.assertEqual(EXAMPLE['zone_recordsets'], sot.zone_recordsets) + self.assertEqual(EXAMPLE['zone_records'], sot.zone_records) + self.assertEqual(EXAMPLE['recordset_records'], sot.recordset_records) + self.assertEqual(EXAMPLE['api_export_size'], sot.api_export_size) + self.assertEqual('FAKE_PROJECT', sot.project) + + def test_prepare_request(self): + body = {'id': 'ABCDEFGH', 'zones': 20} + quota_obj = quota.Quota(**body) + response = quota_obj._prepare_request() + self.assertNotIn('id', response) diff --git a/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml b/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml new file mode 100644 index 000000000..c2be9828a --- /dev/null +++ b/releasenotes/notes/add-dns-quota-49ae659a88eeeab9.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add quota support for designate(DNS) API.