dns: Add support for quotas

This patch add quota support for designate(DNS) API

- add dns quota resource & proxy methods
- add unit & functional test
- add docs and release note
- fix tld functional test code

Change-Id: Id194b6871e2bb80d08b0bb0113bf3fa64e419385
Signed-off-by: choieastsea <choieastsea@gmail.com>
This commit is contained in:
choieastsea
2024-09-03 22:51:54 +09:00
committed by Stephen Finucane
parent 633c73018e
commit c826687bbf
9 changed files with 336 additions and 0 deletions

View File

@@ -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
^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -13,5 +13,6 @@ DNS Resources
v2/tld
v2/recordset
v2/limit
v2/quota
v2/service_status
v2/blacklist

View File

@@ -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:

View File

@@ -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

106
openstack/dns/v2/quota.py Normal file
View File

@@ -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

View File

@@ -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)

View File

@@ -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},
)

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
---
features:
- |
Add quota support for designate(DNS) API.