diff --git a/doc/source/user/proxies/block_storage_v2.rst b/doc/source/user/proxies/block_storage_v2.rst index b6e0faa31..c5bca1866 100644 --- a/doc/source/user/proxies/block_storage_v2.rst +++ b/doc/source/user/proxies/block_storage_v2.rst @@ -33,6 +33,13 @@ Capabilities Operations :noindex: :members: get_capabilities +Limits Operations +^^^^^^^^^^^^^^^^^ + +.. autoclass:: openstack.block_storage.v2._proxy.Proxy + :noindex: + :members: get_limits + Type Operations ^^^^^^^^^^^^^^^ diff --git a/doc/source/user/resources/block_storage/v2/limits.rst b/doc/source/user/resources/block_storage/v2/limits.rst new file mode 100644 index 000000000..ec6e8fd0a --- /dev/null +++ b/doc/source/user/resources/block_storage/v2/limits.rst @@ -0,0 +1,37 @@ +openstack.block_storage.v2.limits +================================= + +.. automodule:: openstack.block_storage.v2.limits + +The AbsoluteLimit Class +----------------------- + +The ``AbsoluteLimit`` class inherits from +:class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.AbsoluteLimit + :members: + +The Limit Class +--------------- + +The ``Limit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.Limit + :members: + +The RateLimit Class +------------------- + +The ``RateLimit`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.RateLimit + :members: + +The RateLimits Class +-------------------- + +The ``RateLimits`` class inherits from :class:`~openstack.resource.Resource`. + +.. autoclass:: openstack.block_storage.v2.limits.RateLimits + :members: diff --git a/openstack/block_storage/v2/_proxy.py b/openstack/block_storage/v2/_proxy.py index 0312fbdfb..fa38a0638 100644 --- a/openstack/block_storage/v2/_proxy.py +++ b/openstack/block_storage/v2/_proxy.py @@ -14,6 +14,7 @@ from openstack.block_storage import _base_proxy from openstack.block_storage.v2 import backup as _backup from openstack.block_storage.v2 import capabilities as _capabilities from openstack.block_storage.v2 import extension as _extension +from openstack.block_storage.v2 import limits as _limits from openstack.block_storage.v2 import quota_set as _quota_set from openstack.block_storage.v2 import snapshot as _snapshot from openstack.block_storage.v2 import stats as _stats @@ -610,6 +611,23 @@ class Proxy(_base_proxy.BaseBlockStorageProxy): backup = self._get_resource(_backup.Backup, backup) backup.reset(self, status) + # ====== LIMITS ====== + def get_limits(self, project=None): + """Retrieves limits + + :param project: A project to get limits for. The value can be either + the ID of a project or an + :class:`~openstack.identity.v2.project.Project` instance. + :returns: A Limit object, including both + :class:`~openstack.block_storage.v2.limits.AbsoluteLimit` and + :class:`~openstack.block_storage.v2.limits.RateLimit` + :rtype: :class:`~openstack.block_storage.v2.limits.Limit` + """ + params = {} + if project: + params['project_id'] = resource.Resource._get_id(project) + return self._get(_limits.Limit, requires_id=False, **params) + # ====== CAPABILITIES ====== def get_capabilities(self, host): """Get a backend's capabilites diff --git a/openstack/block_storage/v2/limits.py b/openstack/block_storage/v2/limits.py new file mode 100644 index 000000000..490e4e4b8 --- /dev/null +++ b/openstack/block_storage/v2/limits.py @@ -0,0 +1,82 @@ +# 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 import resource + + +class AbsoluteLimit(resource.Resource): + #: Properties + #: The maximum total amount of backups, in gibibytes (GiB). + max_total_backup_gigabytes = resource.Body( + "maxTotalBackupGigabytes", type=int + ) + #: The maximum number of backups. + max_total_backups = resource.Body("maxTotalBackups", type=int) + #: The maximum number of snapshots. + max_total_snapshots = resource.Body("maxTotalSnapshots", type=int) + #: The maximum total amount of volumes, in gibibytes (GiB). + max_total_volume_gigabytes = resource.Body( + "maxTotalVolumeGigabytes", type=int + ) + #: The maximum number of volumes. + max_total_volumes = resource.Body("maxTotalVolumes", type=int) + #: The total number of backups gibibytes (GiB) used. + total_backup_gigabytes_used = resource.Body( + "totalBackupGigabytesUsed", type=int + ) + #: The total number of backups used. + total_backups_used = resource.Body("totalBackupsUsed", type=int) + #: The total number of gibibytes (GiB) used. + total_gigabytes_used = resource.Body("totalGigabytesUsed", type=int) + #: The total number of snapshots used. + total_snapshots_used = resource.Body("totalSnapshotsUsed", type=int) + #: The total number of volumes used. + total_volumes_used = resource.Body("totalVolumesUsed", type=int) + + +class RateLimit(resource.Resource): + #: Properties + #: Rate limits next availabe time. + next_available = resource.Body("next-available") + #: Integer for rate limits remaining. + remaining = resource.Body("remaining", type=int) + #: Unit of measurement for the value parameter. + unit = resource.Body("unit") + #: Integer number of requests which can be made. + value = resource.Body("value", type=int) + #: An HTTP verb (POST, PUT, etc.). + verb = resource.Body("verb") + + +class RateLimits(resource.Resource): + #: Properties + #: A list of the specific limits that apply to the ``regex`` and ``uri``. + limits = resource.Body("limit", type=list, list_type=RateLimit) + #: A regex representing which routes this rate limit applies to. + regex = resource.Body("regex") + #: A URI representing which routes this rate limit applies to. + uri = resource.Body("uri") + + +class Limit(resource.Resource): + resource_key = "limits" + base_path = "/limits" + + # capabilities + allow_fetch = True + + #: Properties + #: An absolute limits object. + absolute = resource.Body("absolute", type=AbsoluteLimit) + #: Rate-limit volume copy bandwidth, used to mitigate + #: slow down of data access from the instances. + rate = resource.Body("rate", type=list, list_type=RateLimits) diff --git a/openstack/tests/unit/block_storage/v2/test_limits.py b/openstack/tests/unit/block_storage/v2/test_limits.py new file mode 100644 index 000000000..ec74c3fc9 --- /dev/null +++ b/openstack/tests/unit/block_storage/v2/test_limits.py @@ -0,0 +1,206 @@ +# 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.block_storage.v2 import limits +from openstack.tests.unit import base + +ABSOLUTE_LIMIT = { + "totalSnapshotsUsed": 1, + "maxTotalBackups": 10, + "maxTotalVolumeGigabytes": 1000, + "maxTotalSnapshots": 10, + "maxTotalBackupGigabytes": 1000, + "totalBackupGigabytesUsed": 1, + "maxTotalVolumes": 10, + "totalVolumesUsed": 2, + "totalBackupsUsed": 3, + "totalGigabytesUsed": 2, +} + +RATE_LIMIT = { + "verb": "POST", + "value": 80, + "remaining": 80, + "unit": "MINUTE", + "next-available": "2021-02-23T22:08:00Z", +} + +RATE_LIMITS = {"regex": ".*", "uri": "*", "limit": [RATE_LIMIT]} + +LIMIT = {"rate": [RATE_LIMITS], "absolute": ABSOLUTE_LIMIT} + + +class TestAbsoluteLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.AbsoluteLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_absolute_limit(self): + limit_resource = limits.AbsoluteLimit(**ABSOLUTE_LIMIT) + self.assertEqual( + ABSOLUTE_LIMIT['totalSnapshotsUsed'], + limit_resource.total_snapshots_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackups'], limit_resource.max_total_backups + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumeGigabytes'], + limit_resource.max_total_volume_gigabytes, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalSnapshots'], + limit_resource.max_total_snapshots, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalBackupGigabytes'], + limit_resource.max_total_backup_gigabytes, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupGigabytesUsed'], + limit_resource.total_backup_gigabytes_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['maxTotalVolumes'], limit_resource.max_total_volumes + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalVolumesUsed'], + limit_resource.total_volumes_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalBackupsUsed'], + limit_resource.total_backups_used, + ) + self.assertEqual( + ABSOLUTE_LIMIT['totalGigabytesUsed'], + limit_resource.total_gigabytes_used, + ) + + +class TestRateLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.RateLimit() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def test_make_rate_limit(self): + limit_resource = limits.RateLimit(**RATE_LIMIT) + self.assertEqual(RATE_LIMIT['verb'], limit_resource.verb) + self.assertEqual(RATE_LIMIT['value'], limit_resource.value) + self.assertEqual(RATE_LIMIT['remaining'], limit_resource.remaining) + self.assertEqual(RATE_LIMIT['unit'], limit_resource.unit) + self.assertEqual( + RATE_LIMIT['next-available'], limit_resource.next_available + ) + + +class TestRateLimits(base.TestCase): + def test_basic(self): + limit_resource = limits.RateLimits() + self.assertIsNone(limit_resource.resource_key) + self.assertIsNone(limit_resource.resources_key) + self.assertEqual('', limit_resource.base_path) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_list) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available + ) + + def test_make_rate_limits(self): + limit_resource = limits.RateLimits(**RATE_LIMITS) + self.assertEqual(RATE_LIMITS['regex'], limit_resource.regex) + self.assertEqual(RATE_LIMITS['uri'], limit_resource.uri) + self._test_rate_limit(RATE_LIMITS['limit'], limit_resource.limits) + + +class TestLimit(base.TestCase): + def test_basic(self): + limit_resource = limits.Limit() + self.assertEqual('limits', limit_resource.resource_key) + self.assertEqual('/limits', limit_resource.base_path) + self.assertTrue(limit_resource.allow_fetch) + self.assertFalse(limit_resource.allow_create) + self.assertFalse(limit_resource.allow_commit) + self.assertFalse(limit_resource.allow_delete) + self.assertFalse(limit_resource.allow_list) + + def _test_absolute_limit(self, expected, actual): + self.assertEqual( + expected['totalSnapshotsUsed'], actual.total_snapshots_used + ) + self.assertEqual(expected['maxTotalBackups'], actual.max_total_backups) + self.assertEqual( + expected['maxTotalVolumeGigabytes'], + actual.max_total_volume_gigabytes, + ) + self.assertEqual( + expected['maxTotalSnapshots'], actual.max_total_snapshots + ) + self.assertEqual( + expected['maxTotalBackupGigabytes'], + actual.max_total_backup_gigabytes, + ) + self.assertEqual( + expected['totalBackupGigabytesUsed'], + actual.total_backup_gigabytes_used, + ) + self.assertEqual(expected['maxTotalVolumes'], actual.max_total_volumes) + self.assertEqual( + expected['totalVolumesUsed'], actual.total_volumes_used + ) + self.assertEqual( + expected['totalBackupsUsed'], actual.total_backups_used + ) + self.assertEqual( + expected['totalGigabytesUsed'], actual.total_gigabytes_used + ) + + def _test_rate_limit(self, expected, actual): + self.assertEqual(expected[0]['verb'], actual[0].verb) + self.assertEqual(expected[0]['value'], actual[0].value) + self.assertEqual(expected[0]['remaining'], actual[0].remaining) + self.assertEqual(expected[0]['unit'], actual[0].unit) + self.assertEqual( + expected[0]['next-available'], actual[0].next_available + ) + + def _test_rate_limits(self, expected, actual): + self.assertEqual(expected[0]['regex'], actual[0].regex) + self.assertEqual(expected[0]['uri'], actual[0].uri) + self._test_rate_limit(expected[0]['limit'], actual[0].limits) + + def test_make_limit(self): + limit_resource = limits.Limit(**LIMIT) + self._test_rate_limits(LIMIT['rate'], limit_resource.rate) + self._test_absolute_limit(LIMIT['absolute'], limit_resource.absolute) diff --git a/openstack/tests/unit/block_storage/v2/test_proxy.py b/openstack/tests/unit/block_storage/v2/test_proxy.py index bf3a3b66f..3455f6abf 100644 --- a/openstack/tests/unit/block_storage/v2/test_proxy.py +++ b/openstack/tests/unit/block_storage/v2/test_proxy.py @@ -14,6 +14,7 @@ from unittest import mock from openstack.block_storage.v2 import _proxy from openstack.block_storage.v2 import backup from openstack.block_storage.v2 import capabilities +from openstack.block_storage.v2 import limits from openstack.block_storage.v2 import quota_set from openstack.block_storage.v2 import snapshot from openstack.block_storage.v2 import stats @@ -290,6 +291,16 @@ class TestBackup(TestVolumeProxy): ) +class TestLimit(TestVolumeProxy): + def test_limits_get(self): + self.verify_get( + self.proxy.get_limits, + limits.Limit, + method_args=[], + expected_kwargs={'requires_id': False}, + ) + + class TestCapabilities(TestVolumeProxy): def test_capabilites_get(self): self.verify_get(self.proxy.get_capabilities, capabilities.Capabilities)