From af5e9d16e8a00c0d382b7090c66df211fefc0b3c Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Thu, 5 Nov 2020 11:22:05 +0000 Subject: [PATCH] compute: Fix 'usage * -f yaml' output Make use of 'FormattableColumn'-derived formatters, which provide better output than what we were using before, particularly for the YAML output format. Change-Id: Ic770f27cb1f74222636f05350f97400808adffbf Signed-off-by: Stephen Finucane --- openstackclient/compute/v2/usage.py | 103 ++++++++++++++---- .../tests/unit/compute/v2/test_usage.py | 42 +++---- ...proved-server-output-6965b664f6abda8d.yaml | 6 + 3 files changed, 107 insertions(+), 44 deletions(-) diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index 307c238af..69fa04e8b 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -17,7 +17,9 @@ import collections import datetime +import functools +from cliff import columns as cliff_columns from novaclient import api_versions from osc_lib.command import command from osc_lib import utils @@ -25,6 +27,57 @@ from osc_lib import utils from openstackclient.i18n import _ +# TODO(stephenfin): This exists in a couple of places and should be moved to a +# common module +class ProjectColumn(cliff_columns.FormattableColumn): + """Formattable column for project column. + + Unlike the parent FormattableColumn class, the initializer of the class + takes project_cache as the second argument. + ``osc_lib.utils.get_item_properties`` instantiates ``FormattableColumn`` + objects with a single parameter, the column value, so you need to pass a + partially initialized class like ``functools.partial(ProjectColumn, + project_cache)`` to use this. + """ + + def __init__(self, value, project_cache=None): + super().__init__(value) + self.project_cache = project_cache or {} + + def human_readable(self): + project = self._value + if not project: + return '' + + if project in self.project_cache.keys(): + return self.project_cache[project].name + + return project + + +class CountColumn(cliff_columns.FormattableColumn): + + def human_readable(self): + return len(self._value) + + +class FloatColumn(cliff_columns.FormattableColumn): + + def human_readable(self): + return float("%.2f" % self._value) + + +def _formatters(project_cache): + return { + 'tenant_id': functools.partial( + ProjectColumn, project_cache=project_cache), + 'server_usages': CountColumn, + 'total_memory_mb_usage': FloatColumn, + 'total_vcpus_usage': FloatColumn, + 'total_local_gb_usage': FloatColumn, + } + + def _get_usage_marker(usage): marker = None if hasattr(usage, 'server_usages') and usage.server_usages: @@ -147,17 +200,15 @@ class ListUsage(command.Lister): "end": end.strftime(dateformat), }) - return (column_headers, - (utils.get_item_properties( + return ( + column_headers, + ( + utils.get_item_properties( s, columns, - formatters={ - 'tenant_id': _format_project, - 'server_usages': lambda x: len(x), - 'total_memory_mb_usage': lambda x: float("%.2f" % x), - 'total_vcpus_usage': lambda x: float("%.2f" % x), - 'total_local_gb_usage': lambda x: float("%.2f" % x), - }, - ) for s in usage_list)) + formatters=_formatters(project_cache), + ) for s in usage_list + ), + ) class ShowUsage(command.ShowOne): @@ -222,17 +273,21 @@ class ShowUsage(command.ShowOne): "project": project, }) - info = {} - info['Servers'] = ( - len(usage.server_usages) - if hasattr(usage, "server_usages") else None) - info['RAM MB-Hours'] = ( - float("%.2f" % usage.total_memory_mb_usage) - if hasattr(usage, "total_memory_mb_usage") else None) - info['CPU Hours'] = ( - float("%.2f" % usage.total_vcpus_usage) - if hasattr(usage, "total_vcpus_usage") else None) - info['Disk GB-Hours'] = ( - float("%.2f" % usage.total_local_gb_usage) - if hasattr(usage, "total_local_gb_usage") else None) - return zip(*sorted(info.items())) + columns = ( + "tenant_id", + "server_usages", + "total_memory_mb_usage", + "total_vcpus_usage", + "total_local_gb_usage" + ) + column_headers = ( + "Project", + "Servers", + "RAM MB-Hours", + "CPU Hours", + "Disk GB-Hours" + ) + + data = utils.get_item_properties( + usage, columns, formatters=_formatters(None)) + return column_headers, data diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index c08710256..bbccb9bdc 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -16,7 +16,7 @@ from unittest import mock from novaclient import api_versions -from openstackclient.compute.v2 import usage +from openstackclient.compute.v2 import usage as usage_cmds from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes @@ -49,11 +49,11 @@ class TestUsageList(TestUsage): ) data = [( - usages[0].tenant_id, - len(usages[0].server_usages), - float("%.2f" % usages[0].total_memory_mb_usage), - float("%.2f" % usages[0].total_vcpus_usage), - float("%.2f" % usages[0].total_local_gb_usage), + usage_cmds.ProjectColumn(usages[0].tenant_id), + usage_cmds.CountColumn(usages[0].server_usages), + usage_cmds.FloatColumn(usages[0].total_memory_mb_usage), + usage_cmds.FloatColumn(usages[0].total_vcpus_usage), + usage_cmds.FloatColumn(usages[0].total_local_gb_usage), )] def setUp(self): @@ -63,7 +63,7 @@ class TestUsageList(TestUsage): self.projects_mock.list.return_value = [self.project] # Get the command object to test - self.cmd = usage.ListUsage(self.app, None) + self.cmd = usage_cmds.ListUsage(self.app, None) def test_usage_list_no_options(self): @@ -79,8 +79,8 @@ class TestUsageList(TestUsage): self.projects_mock.list.assert_called_with() - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) def test_usage_list_with_options(self): arglist = [ @@ -102,8 +102,8 @@ class TestUsageList(TestUsage): datetime.datetime(2016, 12, 20, 0, 0), detailed=True) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) def test_usage_list_with_pagination(self): arglist = [] @@ -127,8 +127,8 @@ class TestUsageList(TestUsage): mock.call(mock.ANY, mock.ANY, detailed=True, marker=self.usages[0]['server_usages'][0]['instance_id']) ]) - self.assertEqual(self.columns, columns) - self.assertEqual(tuple(self.data), tuple(data)) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(tuple(self.data), tuple(data)) class TestUsageShow(TestUsage): @@ -139,17 +139,19 @@ class TestUsageShow(TestUsage): attrs={'tenant_id': project.name}) columns = ( + 'Project', + 'Servers', + 'RAM MB-Hours', 'CPU Hours', 'Disk GB-Hours', - 'RAM MB-Hours', - 'Servers', ) data = ( - float("%.2f" % usage.total_vcpus_usage), - float("%.2f" % usage.total_local_gb_usage), - float("%.2f" % usage.total_memory_mb_usage), - len(usage.server_usages), + usage_cmds.ProjectColumn(usage.tenant_id), + usage_cmds.CountColumn(usage.server_usages), + usage_cmds.FloatColumn(usage.total_memory_mb_usage), + usage_cmds.FloatColumn(usage.total_vcpus_usage), + usage_cmds.FloatColumn(usage.total_local_gb_usage), ) def setUp(self): @@ -159,7 +161,7 @@ class TestUsageShow(TestUsage): self.projects_mock.get.return_value = self.project # Get the command object to test - self.cmd = usage.ShowUsage(self.app, None) + self.cmd = usage_cmds.ShowUsage(self.app, None) def test_usage_show_no_options(self): diff --git a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml index cbe950eac..7a42fa899 100644 --- a/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml +++ b/releasenotes/notes/improved-server-output-6965b664f6abda8d.yaml @@ -8,3 +8,9 @@ fixes: will now be rendered as objects. In addition, the ``power_state`` field will now be humanized and rendered as a string value when using the table formatter. + - | + The ``usage list`` and ``usage show`` commands will now display the name + of the project being queried rather than the ID when using the table + formatter. In addition, the ``server_usages``, ``total_memory_mb_usage``, + ``total_vcpus_usage`` and ``total_local_gb_usage`` values will only be + humanized when using the table formatter.