From 9d394372823053e6a4038e14f81e14d1a32a6e3b Mon Sep 17 00:00:00 2001
From: whoami-rajat <rajatdhasmana@gmail.com>
Date: Mon, 17 Jul 2023 15:34:44 +0530
Subject: [PATCH] common: Migrate 'limits show' to SDK

This is done for both the compute and block storage services even though
the compute service hasn't supported rate limits since API v2.1 was
introduced [1].

[1] https://github.com/openstack/nova/commit/ca4ec762804

Change-Id: Idd9f4a1c23952a6087f08c03ac8b5bebd5a0c86d
Co-authored-by: Stephen Finucane <stephenfin@redhat.com>
Depends-on: https://review.opendev.org/c/openstack/openstacksdk/+/918519
---
 openstackclient/common/clientmanager.py       |  12 +-
 openstackclient/common/limits.py              | 102 ++++++++++------
 .../tests/unit/common/test_limits.py          | 108 +++++++++++------
 .../tests/unit/compute/v2/fakes.py            |  87 ++------------
 openstackclient/tests/unit/volume/v2/fakes.py | 111 ------------------
 openstackclient/tests/unit/volume/v3/fakes.py |  53 +++++++++
 .../migrate-limits-show-f586c9762dfd7d0c.yaml |   4 +
 7 files changed, 222 insertions(+), 255 deletions(-)
 create mode 100644 releasenotes/notes/migrate-limits-show-f586c9762dfd7d0c.yaml

diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py
index 8a67dc0c0d..8caf94a5d9 100644
--- a/openstackclient/common/clientmanager.py
+++ b/openstackclient/common/clientmanager.py
@@ -133,7 +133,17 @@ class ClientManager(clientmanager.ClientManager):
         # NOTE(jcross): Cinder did some interesting things with their service
         #               name so we need to figure out which version to look
         #               for when calling is_service_available()
-        volume_version = volume_client.api_version.ver_major
+        endpoint_data = volume_client.get_endpoint_data()
+        # Not sure how endpoint data stores the api version for v2 API,
+        # for v3 it is a tuple (3, 0)
+        if endpoint_data.api_version and isinstance(
+            endpoint_data.api_version, tuple
+        ):
+            volume_version = endpoint_data.api_version[0]
+        else:
+            # Setting volume_version as 2 here if it doesn't satisfy the
+            # conditions for version 3
+            volume_version = 2
         if (
             self.is_service_available("volumev%s" % volume_version)
             is not False
diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py
index 7050d2fd04..043bf5321b 100644
--- a/openstackclient/common/limits.py
+++ b/openstackclient/common/limits.py
@@ -24,6 +24,31 @@ from openstackclient.i18n import _
 from openstackclient.identity import common as identity_common
 
 
+def _format_absolute_limit(absolute_limits):
+    info = {}
+
+    for key in set(absolute_limits):
+        if key in ('id', 'name', 'location'):
+            continue
+
+        info[key] = absolute_limits[key]
+
+    return info
+
+
+def _format_rate_limit(rate_limits):
+    # flatten this:
+    #
+    #   {'uri': '<uri>', 'limit': [{'value': '<value>', ...], ...}
+    #
+    # to this:
+    #
+    #   {'uri': '<uri>', 'value': '<value>', ...}, ...}
+    return itertools.chain(
+        *[[{'uri': x['uri'], **y} for y in x['limit']] for x in rate_limits]
+    )
+
+
 class ShowLimits(command.Lister):
     _description = _("Show compute and block storage limits")
 
@@ -42,36 +67,42 @@ class ShowLimits(command.Lister):
             dest="is_rate",
             action="store_true",
             default=False,
-            help=_("Show rate limits"),
+            help=_(
+                'Show rate limits. This is not supported by the compute '
+                'service since the 12.0.0 (Liberty) release and is only '
+                'supported by the block storage service when the '
+                'rate-limiting middleware is enabled. It is therefore a no-op '
+                'in most deployments.'
+            ),
         )
         parser.add_argument(
             "--reserved",
             dest="is_reserved",
             action="store_true",
             default=False,
-            help=_("Include reservations count [only valid with --absolute]"),
+            help=_("Include reservations count (only valid with --absolute)"),
         )
         parser.add_argument(
             '--project',
             metavar='<project>',
             help=_(
-                'Show limits for a specific project (name or ID)'
-                ' [only valid with --absolute]'
+                'Show limits for a specific project (name or ID) '
+                '(only valid with --absolute)'
             ),
         )
         parser.add_argument(
             '--domain',
             metavar='<domain>',
             help=_(
-                'Domain the project belongs to (name or ID)'
-                ' [only valid with --absolute]'
+                'Domain the project belongs to (name or ID) '
+                '(only valid with --absolute)'
             ),
         )
         return parser
 
     def take_action(self, parsed_args):
-        compute_client = self.app.client_manager.compute
-        volume_client = self.app.client_manager.volume
+        compute_client = self.app.client_manager.sdk_connection.compute
+        volume_client = self.app.client_manager.sdk_connection.volume
 
         project_id = None
         if parsed_args.project is not None:
@@ -94,33 +125,30 @@ class ShowLimits(command.Lister):
         volume_limits = None
 
         if self.app.client_manager.is_compute_endpoint_enabled():
-            compute_limits = compute_client.limits.get(
-                parsed_args.is_reserved, tenant_id=project_id
+            compute_limits = compute_client.get_limits(
+                reserved=parsed_args.is_reserved, tenant_id=project_id
             )
 
         if self.app.client_manager.is_volume_endpoint_enabled(volume_client):
-            volume_limits = volume_client.limits.get()
-
-        data = []
-        if parsed_args.is_absolute:
-            if compute_limits:
-                data.append(compute_limits.absolute)
-            if volume_limits:
-                data.append(volume_limits.absolute)
-            columns = ["Name", "Value"]
-            return (
-                columns,
-                (
-                    utils.get_item_properties(s, columns)
-                    for s in itertools.chain(*data)
-                ),
+            volume_limits = volume_client.get_limits(
+                project_id=project_id,
             )
 
-        elif parsed_args.is_rate:
+        if parsed_args.is_absolute:
+            columns = ["Name", "Value"]
+            info = {}
             if compute_limits:
-                data.append(compute_limits.rate)
+                info.update(_format_absolute_limit(compute_limits.absolute))
             if volume_limits:
-                data.append(volume_limits.rate)
+                info.update(_format_absolute_limit(volume_limits.absolute))
+
+            return (columns, sorted(info.items(), key=lambda x: x[0]))
+        else:  # parsed_args.is_rate
+            data = []
+            if compute_limits:
+                data.extend(_format_rate_limit(compute_limits.rate))
+            if volume_limits:
+                data.extend(_format_rate_limit(volume_limits.rate))
             columns = [
                 "Verb",
                 "URI",
@@ -129,12 +157,18 @@ class ShowLimits(command.Lister):
                 "Unit",
                 "Next Available",
             ]
+
             return (
                 columns,
-                (
-                    utils.get_item_properties(s, columns)
-                    for s in itertools.chain(*data)
-                ),
+                [
+                    (
+                        s['verb'],
+                        s['uri'],
+                        s['value'],
+                        s['remaining'],
+                        s['unit'],
+                        s.get('next-available') or s['next_available'],
+                    )
+                    for s in data
+                ],
             )
-        else:
-            return {}, {}
diff --git a/openstackclient/tests/unit/common/test_limits.py b/openstackclient/tests/unit/common/test_limits.py
index 86b17b1826..b7309ab7c6 100644
--- a/openstackclient/tests/unit/common/test_limits.py
+++ b/openstackclient/tests/unit/common/test_limits.py
@@ -13,23 +13,62 @@
 
 from openstackclient.common import limits
 from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
-from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes
+from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes
 
 
 class TestComputeLimits(compute_fakes.TestComputev2):
-    absolute_columns = [
-        'Name',
-        'Value',
-    ]
-
+    absolute_columns = ['Name', 'Value']
     rate_columns = ["Verb", "URI", "Value", "Remain", "Unit", "Next Available"]
 
     def setUp(self):
         super().setUp()
         self.app.client_manager.volume_endpoint_enabled = False
 
-        self.fake_limits = compute_fakes.FakeLimits()
-        self.compute_client.limits.get.return_value = self.fake_limits
+        self.fake_limits = compute_fakes.create_limits()
+
+        self.absolute_data = [
+            ('floating_ips', 10),
+            ('floating_ips_used', 0),
+            ('image_meta', 128),
+            ('instances', 10),
+            ('instances_used', 0),
+            ('keypairs', 100),
+            ('max_image_meta', 128),
+            ('max_security_group_rules', 20),
+            ('max_security_groups', 10),
+            ('max_server_group_members', 10),
+            ('max_server_groups', 10),
+            ('max_server_meta', 128),
+            ('max_total_cores', 20),
+            ('max_total_floating_ips', 10),
+            ('max_total_instances', 10),
+            ('max_total_keypairs', 100),
+            ('max_total_ram_size', 51200),
+            ('personality', 5),
+            ('personality_size', 10240),
+            ('security_group_rules', 20),
+            ('security_groups', 10),
+            ('security_groups_used', 0),
+            ('server_group_members', 10),
+            ('server_groups', 10),
+            ('server_groups_used', 0),
+            ('server_meta', 128),
+            ('total_cores', 20),
+            ('total_cores_used', 0),
+            ('total_floating_ips_used', 0),
+            ('total_instances_used', 0),
+            ('total_ram', 51200),
+            ('total_ram_used', 0),
+            ('total_security_groups_used', 0),
+            ('total_server_groups_used', 0),
+        ]
+        self.rate_data = [
+            ('POST', '*', 10, 2, 'MINUTE', '2011-12-15T22:42:45Z'),
+            ('PUT', '*', 10, 2, 'MINUTE', '2011-12-15T22:42:45Z'),
+            ('DELETE', '*', 100, 100, 'MINUTE', '2011-12-15T22:42:45Z'),
+        ]
+
+        self.compute_sdk_client.get_limits.return_value = self.fake_limits
 
     def test_compute_show_absolute(self):
         arglist = ['--absolute']
@@ -39,12 +78,8 @@ class TestComputeLimits(compute_fakes.TestComputev2):
 
         columns, data = cmd.take_action(parsed_args)
 
-        ret_limits = list(data)
-        compute_reference_limits = self.fake_limits.absolute_limits()
-
         self.assertEqual(self.absolute_columns, columns)
-        self.assertEqual(compute_reference_limits, ret_limits)
-        self.assertEqual(19, len(ret_limits))
+        self.assertEqual(self.absolute_data, data)
 
     def test_compute_show_rate(self):
         arglist = ['--rate']
@@ -54,28 +89,39 @@ class TestComputeLimits(compute_fakes.TestComputev2):
 
         columns, data = cmd.take_action(parsed_args)
 
-        ret_limits = list(data)
-        compute_reference_limits = self.fake_limits.rate_limits()
-
         self.assertEqual(self.rate_columns, columns)
-        self.assertEqual(compute_reference_limits, ret_limits)
-        self.assertEqual(3, len(ret_limits))
+        self.assertEqual(self.rate_data, data)
 
 
 class TestVolumeLimits(volume_fakes.TestVolume):
-    absolute_columns = [
-        'Name',
-        'Value',
-    ]
-
+    absolute_columns = ['Name', 'Value']
     rate_columns = ["Verb", "URI", "Value", "Remain", "Unit", "Next Available"]
 
     def setUp(self):
         super().setUp()
         self.app.client_manager.compute_endpoint_enabled = False
 
-        self.fake_limits = volume_fakes.FakeLimits()
-        self.volume_client.limits.get.return_value = self.fake_limits
+        self.fake_limits = volume_fakes.create_limits()
+
+        self.absolute_data = [
+            ('max_total_backup_gigabytes', 1000),
+            ('max_total_backups', 10),
+            ('max_total_snapshots', 10),
+            ('max_total_volume_gigabytes', 1000),
+            ('max_total_volumes', 10),
+            ('total_backup_gigabytes_used', 0),
+            ('total_backups_used', 0),
+            ('total_gigabytes_used', 35),
+            ('total_snapshots_used', 1),
+            ('total_volumes_used', 4),
+        ]
+        self.rate_data = [
+            ('POST', '*', 10, 2, 'MINUTE', '2011-12-15T22:42:45Z'),
+            ('PUT', '*', 10, 2, 'MINUTE', '2011-12-15T22:42:45Z'),
+            ('DELETE', '*', 100, 100, 'MINUTE', '2011-12-15T22:42:45Z'),
+        ]
+
+        self.volume_sdk_client.get_limits.return_value = self.fake_limits
 
     def test_volume_show_absolute(self):
         arglist = ['--absolute']
@@ -85,12 +131,8 @@ class TestVolumeLimits(volume_fakes.TestVolume):
 
         columns, data = cmd.take_action(parsed_args)
 
-        ret_limits = list(data)
-        compute_reference_limits = self.fake_limits.absolute_limits()
-
         self.assertEqual(self.absolute_columns, columns)
-        self.assertEqual(compute_reference_limits, ret_limits)
-        self.assertEqual(10, len(ret_limits))
+        self.assertEqual(self.absolute_data, data)
 
     def test_volume_show_rate(self):
         arglist = ['--rate']
@@ -100,9 +142,5 @@ class TestVolumeLimits(volume_fakes.TestVolume):
 
         columns, data = cmd.take_action(parsed_args)
 
-        ret_limits = list(data)
-        compute_reference_limits = self.fake_limits.rate_limits()
-
         self.assertEqual(self.rate_columns, columns)
-        self.assertEqual(compute_reference_limits, ret_limits)
-        self.assertEqual(3, len(ret_limits))
+        self.assertEqual(self.rate_data, data)
diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py
index 614bd90281..3e89d49057 100644
--- a/openstackclient/tests/unit/compute/v2/fakes.py
+++ b/openstackclient/tests/unit/compute/v2/fakes.py
@@ -28,6 +28,7 @@ from openstack.compute.v2 import extension as _extension
 from openstack.compute.v2 import flavor as _flavor
 from openstack.compute.v2 import hypervisor as _hypervisor
 from openstack.compute.v2 import keypair as _keypair
+from openstack.compute.v2 import limits as _limits
 from openstack.compute.v2 import migration as _migration
 from openstack.compute.v2 import server as _server
 from openstack.compute.v2 import server_action as _server_action
@@ -91,9 +92,6 @@ class FakeComputev2Client:
         self.images = mock.Mock()
         self.images.resource_class = fakes.FakeResource(None, {})
 
-        self.limits = mock.Mock()
-        self.limits.resource_class = fakes.FakeResource(None, {})
-
         self.servers = mock.Mock()
         self.servers.resource_class = fakes.FakeResource(None, {})
 
@@ -1177,11 +1175,12 @@ def create_one_comp_detailed_quota(attrs=None):
     return quota
 
 
-class FakeLimits:
-    """Fake limits"""
+def create_limits(attrs=None):
+    """Create a fake limits object."""
+    attrs = attrs or {}
 
-    def __init__(self, absolute_attrs=None, rate_attrs=None):
-        self.absolute_limits_attrs = {
+    limits_attrs = {
+        'absolute': {
             'maxServerMeta': 128,
             'maxTotalInstances': 10,
             'maxPersonality': 5,
@@ -1201,11 +1200,8 @@ class FakeLimits:
             'maxTotalFloatingIps': 10,
             'totalSecurityGroupsUsed': 0,
             'maxTotalCores': 20,
-        }
-        absolute_attrs = absolute_attrs or {}
-        self.absolute_limits_attrs.update(absolute_attrs)
-
-        self.rate_limits_attrs = [
+        },
+        'rate': [
             {
                 "uri": "*",
                 "limit": [
@@ -1232,69 +1228,12 @@ class FakeLimits:
                     },
                 ],
             }
-        ]
+        ],
+    }
+    limits_attrs.update(attrs)
 
-    @property
-    def absolute(self):
-        for name, value in self.absolute_limits_attrs.items():
-            yield FakeAbsoluteLimit(name, value)
-
-    def absolute_limits(self):
-        reference_data = []
-        for name, value in self.absolute_limits_attrs.items():
-            reference_data.append((name, value))
-        return reference_data
-
-    @property
-    def rate(self):
-        for group in self.rate_limits_attrs:
-            uri = group['uri']
-            for rate in group['limit']:
-                yield FakeRateLimit(
-                    rate['verb'],
-                    uri,
-                    rate['value'],
-                    rate['remaining'],
-                    rate['unit'],
-                    rate['next-available'],
-                )
-
-    def rate_limits(self):
-        reference_data = []
-        for group in self.rate_limits_attrs:
-            uri = group['uri']
-            for rate in group['limit']:
-                reference_data.append(
-                    (
-                        rate['verb'],
-                        uri,
-                        rate['value'],
-                        rate['remaining'],
-                        rate['unit'],
-                        rate['next-available'],
-                    )
-                )
-        return reference_data
-
-
-class FakeAbsoluteLimit:
-    """Data model that represents an absolute limit"""
-
-    def __init__(self, name, value):
-        self.name = name
-        self.value = value
-
-
-class FakeRateLimit:
-    """Data model that represents a flattened view of a single rate limit"""
-
-    def __init__(self, verb, uri, value, remain, unit, next_available):
-        self.verb = verb
-        self.uri = uri
-        self.value = value
-        self.remain = remain
-        self.unit = unit
-        self.next_available = next_available
+    limits = _limits.Limits(**limits_attrs)
+    return limits
 
 
 def create_one_migration(attrs=None):
diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py
index 9ba3b2bf5a..389b10b0f9 100644
--- a/openstackclient/tests/unit/volume/v2/fakes.py
+++ b/openstackclient/tests/unit/volume/v2/fakes.py
@@ -1062,114 +1062,3 @@ def create_one_detailed_quota(attrs=None):
     quota = fakes.FakeResource(info=copy.deepcopy(quota_attrs), loaded=True)
 
     return quota
-
-
-class FakeLimits:
-    """Fake limits"""
-
-    def __init__(self, absolute_attrs=None):
-        self.absolute_limits_attrs = {
-            'totalSnapshotsUsed': 1,
-            'maxTotalBackups': 10,
-            'maxTotalVolumeGigabytes': 1000,
-            'maxTotalSnapshots': 10,
-            'maxTotalBackupGigabytes': 1000,
-            'totalBackupGigabytesUsed': 0,
-            'maxTotalVolumes': 10,
-            'totalVolumesUsed': 4,
-            'totalBackupsUsed': 0,
-            'totalGigabytesUsed': 35,
-        }
-        absolute_attrs = absolute_attrs or {}
-        self.absolute_limits_attrs.update(absolute_attrs)
-
-        self.rate_limits_attrs = [
-            {
-                "uri": "*",
-                "limit": [
-                    {
-                        "value": 10,
-                        "verb": "POST",
-                        "remaining": 2,
-                        "unit": "MINUTE",
-                        "next-available": "2011-12-15T22:42:45Z",
-                    },
-                    {
-                        "value": 10,
-                        "verb": "PUT",
-                        "remaining": 2,
-                        "unit": "MINUTE",
-                        "next-available": "2011-12-15T22:42:45Z",
-                    },
-                    {
-                        "value": 100,
-                        "verb": "DELETE",
-                        "remaining": 100,
-                        "unit": "MINUTE",
-                        "next-available": "2011-12-15T22:42:45Z",
-                    },
-                ],
-            }
-        ]
-
-    @property
-    def absolute(self):
-        for name, value in self.absolute_limits_attrs.items():
-            yield FakeAbsoluteLimit(name, value)
-
-    def absolute_limits(self):
-        reference_data = []
-        for name, value in self.absolute_limits_attrs.items():
-            reference_data.append((name, value))
-        return reference_data
-
-    @property
-    def rate(self):
-        for group in self.rate_limits_attrs:
-            uri = group['uri']
-            for rate in group['limit']:
-                yield FakeRateLimit(
-                    rate['verb'],
-                    uri,
-                    rate['value'],
-                    rate['remaining'],
-                    rate['unit'],
-                    rate['next-available'],
-                )
-
-    def rate_limits(self):
-        reference_data = []
-        for group in self.rate_limits_attrs:
-            uri = group['uri']
-            for rate in group['limit']:
-                reference_data.append(
-                    (
-                        rate['verb'],
-                        uri,
-                        rate['value'],
-                        rate['remaining'],
-                        rate['unit'],
-                        rate['next-available'],
-                    )
-                )
-        return reference_data
-
-
-class FakeAbsoluteLimit:
-    """Data model that represents an absolute limit."""
-
-    def __init__(self, name, value):
-        self.name = name
-        self.value = value
-
-
-class FakeRateLimit:
-    """Data model that represents a flattened view of a single rate limit."""
-
-    def __init__(self, verb, uri, value, remain, unit, next_available):
-        self.verb = verb
-        self.uri = uri
-        self.value = value
-        self.remain = remain
-        self.unit = unit
-        self.next_available = next_available
diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py
index 6a9f1bb0e5..e792827047 100644
--- a/openstackclient/tests/unit/volume/v3/fakes.py
+++ b/openstackclient/tests/unit/volume/v3/fakes.py
@@ -22,6 +22,7 @@ from openstack.block_storage.v3 import _proxy
 from openstack.block_storage.v3 import availability_zone as _availability_zone
 from openstack.block_storage.v3 import backup as _backup
 from openstack.block_storage.v3 import extension as _extension
+from openstack.block_storage.v3 import limits as _limits
 from openstack.block_storage.v3 import resource_filter as _filters
 from openstack.block_storage.v3 import volume as _volume
 from openstack.compute.v2 import _proxy as _compute_proxy
@@ -423,6 +424,58 @@ def create_one_encryption_volume_type(attrs=None):
     return encryption_type
 
 
+def create_limits(attrs=None):
+    """Create a fake limits object."""
+    attrs = attrs or {}
+
+    limits_attrs = {
+        'absolute': {
+            'totalSnapshotsUsed': 1,
+            'maxTotalBackups': 10,
+            'maxTotalVolumeGigabytes': 1000,
+            'maxTotalSnapshots': 10,
+            'maxTotalBackupGigabytes': 1000,
+            'totalBackupGigabytesUsed': 0,
+            'maxTotalVolumes': 10,
+            'totalVolumesUsed': 4,
+            'totalBackupsUsed': 0,
+            'totalGigabytesUsed': 35,
+        },
+        'rate': [
+            {
+                "uri": "*",
+                "limit": [
+                    {
+                        "value": 10,
+                        "verb": "POST",
+                        "remaining": 2,
+                        "unit": "MINUTE",
+                        "next-available": "2011-12-15T22:42:45Z",
+                    },
+                    {
+                        "value": 10,
+                        "verb": "PUT",
+                        "remaining": 2,
+                        "unit": "MINUTE",
+                        "next-available": "2011-12-15T22:42:45Z",
+                    },
+                    {
+                        "value": 100,
+                        "verb": "DELETE",
+                        "remaining": 100,
+                        "unit": "MINUTE",
+                        "next-available": "2011-12-15T22:42:45Z",
+                    },
+                ],
+            }
+        ],
+    }
+    limits_attrs.update(attrs)
+
+    limits = _limits.Limit(**limits_attrs)
+    return limits
+
+
 def create_one_resource_filter(attrs=None):
     """Create a fake resource filter.
 
diff --git a/releasenotes/notes/migrate-limits-show-f586c9762dfd7d0c.yaml b/releasenotes/notes/migrate-limits-show-f586c9762dfd7d0c.yaml
new file mode 100644
index 0000000000..81a5119ada
--- /dev/null
+++ b/releasenotes/notes/migrate-limits-show-f586c9762dfd7d0c.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+  - |
+    The ``limits show`` command has been migrated to SDK.