From 70a44339ac400be8a05baa5f2ff2bda238945566 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 19 May 2022 13:29:30 +0100 Subject: [PATCH] compute: Add support for os-simple-tenant-usages API https://docs.openstack.org/api-ref/compute/#list-tenant-usage-statistics-for-all-tenants Change-Id: Id941a224b2b70617e7639148e9e4e25176726f93 Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 61 ++++++++--- openstack/compute/v2/usage.py | 102 ++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 39 +++++++ openstack/tests/unit/compute/v2/test_usage.py | 99 +++++++++++++++++ 4 files changed, 289 insertions(+), 12 deletions(-) create mode 100644 openstack/compute/v2/usage.py create mode 100644 openstack/tests/unit/compute/v2/test_usage.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index f1e6de584..f9a56cf50 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -31,6 +31,7 @@ from openstack.compute.v2 import server_ip from openstack.compute.v2 import server_migration as _server_migration from openstack.compute.v2 import server_remote_console as _src from openstack.compute.v2 import service as _service +from openstack.compute.v2 import usage as _usage from openstack.compute.v2 import volume_attachment as _volume_attachment from openstack import exceptions from openstack.identity.v3 import project as _project @@ -609,10 +610,8 @@ class Proxy(proxy.Proxy): :class:`~openstack.compute.v2.limits.RateLimits` :rtype: :class:`~openstack.compute.v2.limits.Limits` """ - res = self._get_resource( - limits.Limits, None) - return res.fetch( - self, **query) + res = self._get_resource(limits.Limits, None) + return res.fetch(self, **query) # ========== Servers ========== @@ -1865,6 +1864,46 @@ class Proxy(proxy.Proxy): return self._get(_server_diagnostics.ServerDiagnostics, server_id=server_id, requires_id=False) + # ========== Project usage ============ + + def usages(self, start=None, end=None, **query): + """Get project usages. + + :param datetime.datetime start: Usage range start date. + :param datetime.datetime end: Usage range end date. + :param dict query: Additional query parameters to use. + :returns: A list of compute ``Usage`` objects. + """ + if start is not None: + query['start'] = start + + if end is not None: + query['end'] = end + + return self._list(_usage.Usage, **query) + + def get_usage(self, project, start=None, end=None, **query): + """Get usage for a single project. + + :param project: ID or instance of + :class:`~openstack.identity.project.Project` of the project for + which the usage should be retrieved. + :param datetime.datetime start: Usage range start date. + :param datetime.datetime end: Usage range end date. + :param dict query: Additional query parameters to use. + :returns: A compute ``Usage`` object. + """ + project = self._get_resource(_project.Project, project) + + if start is not None: + query['start'] = start + + if end is not None: + query['end'] = end + + res = self._get_resource(_usage.Usage, project.id) + return res.fetch(self, **query) + # ========== Server consoles ========== def create_server_remote_console(self, server, **attrs): @@ -1953,11 +1992,9 @@ class Proxy(proxy.Proxy): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - if not query: - query = {} - return res.fetch( - self, usage=usage, **query) + _quota_set.QuotaSet, None, project_id=project.id, + ) + return res.fetch(self, usage=usage, **query) def get_quota_set_defaults(self, project): """Show QuotaSet defaults for the project @@ -1972,9 +2009,9 @@ class Proxy(proxy.Proxy): """ project = self._get_resource(_project.Project, project) res = self._get_resource( - _quota_set.QuotaSet, None, project_id=project.id) - return res.fetch( - self, base_path='/os-quota-sets/defaults') + _quota_set.QuotaSet, None, project_id=project.id, + ) + return res.fetch(self, base_path='/os-quota-sets/defaults') def revert_quota_set(self, project, **query): """Reset Quota for the project/user. diff --git a/openstack/compute/v2/usage.py b/openstack/compute/v2/usage.py new file mode 100644 index 000000000..0d20f6de0 --- /dev/null +++ b/openstack/compute/v2/usage.py @@ -0,0 +1,102 @@ +# 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 ServerUsage(resource.Resource): + resource_key = None + resources_key = None + + # Capabilities + allow_create = False + allow_fetch = False + allow_delete = False + allow_list = False + allow_commit = False + + # Properties + #: The duration that the server exists (in hours). + hours = resource.Body('hours') + #: The display name of a flavor. + flavor = resource.Body('flavor') + #: The UUID of the server. + instance_id = resource.Body('instance_id') + #: The server name. + name = resource.Body('name') + #: The UUID of the project in a multi-tenancy cloud. + project_id = resource.Body('tenant_id') + #: The memory size of the server (in MiB). + memory_mb = resource.Body('memory_mb') + #: The sum of the root disk size of the server and the ephemeral disk size + #: of it (in GiB). + local_gb = resource.Body('local_gb') + #: The number of virtual CPUs that the server uses. + vcpus = resource.Body('vcpus') + #: The date and time when the server was launched. + started_at = resource.Body('started_at') + #: The date and time when the server was deleted. + ended_at = resource.Body('ended_at') + #: The VM state. + state = resource.Body('state') + #: The uptime of the server. + uptime = resource.Body('uptime') + + +class Usage(resource.Resource): + resource_key = 'tenant_usage' + resources_key = 'tenant_usages' + base_path = '/os-simple-tenant-usage' + + # Capabilities + allow_create = False + allow_fetch = True + allow_delete = False + allow_list = True + allow_commit = False + + # TODO(stephenfin): Add 'start', 'end'. These conflict with the body + # responses though. + _query_mapping = resource.QueryParameters( + "detailed", + "limit", + "marker", + "start", + "end", + ) + + # Properties + #: The UUID of the project in a multi-tenancy cloud. + project_id = resource.Body('tenant_id') + #: A list of the server usage objects. + server_usages = resource.Body( + 'server_usages', type=list, list_type=ServerUsage, + ) + #: Multiplying the server disk size (in GiB) by hours the server exists, + #: and then adding that all together for each server. + total_local_gb_usage = resource.Body('total_local_gb_usage') + #: Multiplying the number of virtual CPUs of the server by hours the server + #: exists, and then adding that all together for each server. + total_vcpus_usage = resource.Body('total_vcpus_usage') + #: Multiplying the server memory size (in MiB) by hours the server exists, + #: and then adding that all together for each server. + total_memory_mb_usage = resource.Body('total_memory_mb_usage') + #: The total duration that servers exist (in hours). + total_hours = resource.Body('total_hours') + #: The beginning time to calculate usage statistics on compute and storage + #: resources. + start = resource.Body('start') + #: The ending time to calculate usage statistics on compute and storage + #: resources. + stop = resource.Body('stop') + + _max_microversion = '2.75' diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index bf697cff6..4f6a4f60c 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from unittest import mock from openstack.compute.v2 import _proxy @@ -30,6 +31,7 @@ from openstack.compute.v2 import server_ip from openstack.compute.v2 import server_migration from openstack.compute.v2 import server_remote_console from openstack.compute.v2 import service +from openstack.compute.v2 import usage from openstack import resource from openstack.tests.unit import test_proxy_base @@ -1105,6 +1107,43 @@ class TestCompute(TestComputeProxy): method_args=["value", {'id': 'id', 'name': 'sg'}], expected_args=[self.proxy, 'sg']) + def test_usages(self): + self.verify_list(self.proxy.usages, usage.Usage) + + def test_usages__with_kwargs(self): + now = datetime.datetime.utcnow() + start = now - datetime.timedelta(weeks=4) + end = end = now + datetime.timedelta(days=1) + self.verify_list( + self.proxy.usages, + usage.Usage, + method_kwargs={'start': start, 'end': end}, + expected_kwargs={'start': start, 'end': end}, + ) + + def test_get_usage(self): + self._verify( + "openstack.compute.v2.usage.Usage.fetch", + self.proxy.get_usage, + method_args=['value'], + method_kwargs={}, + expected_args=[self.proxy], + expected_kwargs={}, + ) + + def test_get_usage__with_kwargs(self): + now = datetime.datetime.utcnow() + start = now - datetime.timedelta(weeks=4) + end = end = now + datetime.timedelta(days=1) + self._verify( + "openstack.compute.v2.usage.Usage.fetch", + self.proxy.get_usage, + method_args=['value'], + method_kwargs={'start': start, 'end': end}, + expected_args=[self.proxy], + expected_kwargs={'start': start, 'end': end}, + ) + def test_create_server_remote_console(self): self.verify_create( self.proxy.create_server_remote_console, diff --git a/openstack/tests/unit/compute/v2/test_usage.py b/openstack/tests/unit/compute/v2/test_usage.py new file mode 100644 index 000000000..512bd53f0 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_usage.py @@ -0,0 +1,99 @@ +# 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.compute.v2 import usage +from openstack.tests.unit import base + + +EXAMPLE = { + "tenant_id": "781c9299e68d4b7c80ef52712889647f", + "server_usages": [ + { + "hours": 79.51840531333333, + "flavor": "m1.tiny", + "instance_id": "76638c30-d199-4c2e-8154-7dea963bfe2f", + "name": "test-server", + "tenant_id": "781c9299e68d4b7c80ef52712889647f", + "memory_mb": 512, + "local_gb": 1, + "vcpus": 1, + "started_at": "2022-05-16T10:35:31.000000", + "ended_at": None, + "state": "active", + "uptime": 286266, + } + ], + "total_local_gb_usage": 79.51840531333333, + "total_vcpus_usage": 79.51840531333333, + "total_memory_mb_usage": 40713.423520426666, + "total_hours": 79.51840531333333, + "start": "2022-04-21T18:06:47.064959", + "stop": "2022-05-19T18:06:37.259128", +} + + +class TestUsage(base.TestCase): + def test_basic(self): + sot = usage.Usage() + self.assertEqual('tenant_usage', sot.resource_key) + self.assertEqual('tenant_usages', sot.resources_key) + self.assertEqual('/os-simple-tenant-usage', sot.base_path) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + + def test_make_it(self): + sot = usage.Usage(**EXAMPLE) + + self.assertEqual(EXAMPLE['tenant_id'], sot.project_id) + self.assertEqual( + EXAMPLE['total_local_gb_usage'], + sot.total_local_gb_usage, + ) + self.assertEqual(EXAMPLE['total_vcpus_usage'], sot.total_vcpus_usage) + self.assertEqual( + EXAMPLE['total_memory_mb_usage'], + sot.total_memory_mb_usage, + ) + self.assertEqual(EXAMPLE['total_hours'], sot.total_hours) + self.assertEqual(EXAMPLE['start'], sot.start) + self.assertEqual(EXAMPLE['stop'], sot.stop) + + # now do the embedded objects + self.assertIsInstance(sot.server_usages, list) + self.assertEqual(1, len(sot.server_usages)) + + ssot = sot.server_usages[0] + self.assertIsInstance(ssot, usage.ServerUsage) + self.assertEqual(EXAMPLE['server_usages'][0]['hours'], ssot.hours) + self.assertEqual(EXAMPLE['server_usages'][0]['flavor'], ssot.flavor) + self.assertEqual( + EXAMPLE['server_usages'][0]['instance_id'], ssot.instance_id + ) + self.assertEqual(EXAMPLE['server_usages'][0]['name'], ssot.name) + self.assertEqual( + EXAMPLE['server_usages'][0]['tenant_id'], ssot.project_id + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['memory_mb'], ssot.memory_mb + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['local_gb'], ssot.local_gb + ) + self.assertEqual(EXAMPLE['server_usages'][0]['vcpus'], ssot.vcpus) + self.assertEqual( + EXAMPLE['server_usages'][0]['started_at'], ssot.started_at + ) + self.assertEqual( + EXAMPLE['server_usages'][0]['ended_at'], ssot.ended_at + ) + self.assertEqual(EXAMPLE['server_usages'][0]['state'], ssot.state) + self.assertEqual(EXAMPLE['server_usages'][0]['uptime'], ssot.uptime)