From c97f73ce2a897fe6b83f08de1ce6e1865e9a83c5 Mon Sep 17 00:00:00 2001
From: Daniel Wilson <danielcw@bu.edu>
Date: Tue, 11 Oct 2022 00:39:39 -0400
Subject: [PATCH] Use the compute SDK in server list

Update server list to use the compute component of the OpenStack SDK
instead of directly using the nova interface. This change depends on SDK
version 0.102.0 for automatic client-side query filters.

Change-Id: Ib9985812bfd98320b75f3a82bb594a0daa6e4d93
---
 openstackclient/compute/v2/server.py          | 126 +++++----
 .../functional/compute/v2/test_server.py      |   2 +-
 .../tests/unit/compute/v2/test_server.py      | 261 +++++++++---------
 ...s-server-list-to-sdk-88c8bfc10a9e3032.yaml |   5 +
 4 files changed, 205 insertions(+), 189 deletions(-)
 create mode 100644 releasenotes/notes/nova-gaps-server-list-to-sdk-88c8bfc10a9e3032.yaml

diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index 609faf5aad..f3315b9dc2 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -66,6 +66,28 @@ class PowerStateColumn(cliff_columns.FormattableColumn):
             return 'N/A'
 
 
+class AddressesColumn(cliff_columns.FormattableColumn):
+    """Generate a formatted string of a server's addresses."""
+
+    def human_readable(self):
+        try:
+            return utils.format_dict_of_list({
+                k: [i['addr'] for i in v if 'addr' in i]
+                for k, v in self._value.items()})
+        except Exception:
+            return 'N/A'
+
+
+class HostColumn(cliff_columns.FormattableColumn):
+    """Generate a formatted string of a hostname."""
+
+    def human_readable(self):
+        if self._value is None:
+            return ''
+
+        return self._value
+
+
 def _get_ip_address(addresses, address_type, ip_address_family):
     # Old style addresses
     if address_type in addresses:
@@ -2287,7 +2309,7 @@ class ListServer(command.Lister):
         return parser
 
     def take_action(self, parsed_args):
-        compute_client = self.app.client_manager.compute
+        compute_client = self.app.client_manager.sdk_connection.compute
         identity_client = self.app.client_manager.identity
         image_client = self.app.client_manager.image
 
@@ -2312,10 +2334,11 @@ class ListServer(command.Lister):
         # flavor name is given, map it to ID.
         flavor_id = None
         if parsed_args.flavor:
-            flavor_id = utils.find_resource(
-                compute_client.flavors,
-                parsed_args.flavor,
-            ).id
+            flavor = compute_client.find_flavor(parsed_args.flavor)
+            if flavor is None:
+                msg = _('Unable to find flavor: %s') % parsed_args.flavor
+                raise exceptions.CommandError(msg)
+            flavor_id = flavor.id
 
         # Nova only supports list servers searching by image ID. So if a
         # image name is given, map it to ID.
@@ -2331,19 +2354,21 @@ class ListServer(command.Lister):
             'ip': parsed_args.ip,
             'ip6': parsed_args.ip6,
             'name': parsed_args.name,
-            'instance_name': parsed_args.instance_name,
             'status': parsed_args.status,
             'flavor': flavor_id,
             'image': image_id,
             'host': parsed_args.host,
-            'tenant_id': project_id,
-            'all_tenants': parsed_args.all_projects,
+            'project_id': project_id,
+            'all_projects': parsed_args.all_projects,
             'user_id': user_id,
             'deleted': parsed_args.deleted,
             'changes-before': parsed_args.changes_before,
             'changes-since': parsed_args.changes_since,
         }
 
+        if parsed_args.instance_name is not None:
+            search_opts['instance_name'] = parsed_args.instance_name
+
         if parsed_args.availability_zone:
             search_opts['availability_zone'] = parsed_args.availability_zone
 
@@ -2375,7 +2400,7 @@ class ListServer(command.Lister):
             search_opts['power_state'] = power_state
 
         if parsed_args.tags:
-            if compute_client.api_version < api_versions.APIVersion('2.26'):
+            if not sdk_utils.supports_microversion(compute_client, '2.26'):
                 msg = _(
                     '--os-compute-api-version 2.26 or greater is required to '
                     'support the --tag option'
@@ -2385,7 +2410,7 @@ class ListServer(command.Lister):
             search_opts['tags'] = ','.join(parsed_args.tags)
 
         if parsed_args.not_tags:
-            if compute_client.api_version < api_versions.APIVersion('2.26'):
+            if not sdk_utils.supports_microversion(compute_client, '2.26'):
                 msg = _(
                     '--os-compute-api-version 2.26 or greater is required to '
                     'support the --not-tag option'
@@ -2395,7 +2420,7 @@ class ListServer(command.Lister):
             search_opts['not-tags'] = ','.join(parsed_args.not_tags)
 
         if parsed_args.locked:
-            if compute_client.api_version < api_versions.APIVersion('2.73'):
+            if not sdk_utils.supports_microversion(compute_client, '2.73'):
                 msg = _(
                     '--os-compute-api-version 2.73 or greater is required to '
                     'support the --locked option'
@@ -2404,7 +2429,7 @@ class ListServer(command.Lister):
 
             search_opts['locked'] = True
         elif parsed_args.unlocked:
-            if compute_client.api_version < api_versions.APIVersion('2.73'):
+            if not sdk_utils.supports_microversion(compute_client, '2.73'):
                 msg = _(
                     '--os-compute-api-version 2.73 or greater is required to '
                     'support the --unlocked option'
@@ -2413,10 +2438,14 @@ class ListServer(command.Lister):
 
             search_opts['locked'] = False
 
+        if parsed_args.limit is not None:
+            search_opts['limit'] = parsed_args.limit
+            search_opts['paginated'] = False
+
         LOG.debug('search options: %s', search_opts)
 
         if search_opts['changes-before']:
-            if compute_client.api_version < api_versions.APIVersion('2.66'):
+            if not sdk_utils.supports_microversion(compute_client, '2.66'):
                 msg = _('--os-compute-api-version 2.66 or later is required')
                 raise exceptions.CommandError(msg)
 
@@ -2450,15 +2479,15 @@ class ListServer(command.Lister):
 
         if parsed_args.long:
             columns += (
-                'OS-EXT-STS:task_state',
-                'OS-EXT-STS:power_state',
+                'task_state',
+                'power_state',
             )
             column_headers += (
                 'Task State',
                 'Power State',
             )
 
-        columns += ('networks',)
+        columns += ('addresses',)
         column_headers += ('Networks',)
 
         if parsed_args.long:
@@ -2480,7 +2509,7 @@ class ListServer(command.Lister):
         # microversion 2.47 puts the embedded flavor into the server response
         # body but omits the id, so if not present we just expose the original
         # flavor name in the output
-        if compute_client.api_version >= api_versions.APIVersion('2.47'):
+        if sdk_utils.supports_microversion(compute_client, '2.47'):
             columns += ('flavor_name',)
             column_headers += ('Flavor',)
         else:
@@ -2502,8 +2531,8 @@ class ListServer(command.Lister):
 
         if parsed_args.long:
             columns += (
-                'OS-EXT-AZ:availability_zone',
-                'OS-EXT-SRV-ATTR:host',
+                'availability_zone',
+                'hypervisor_hostname',
                 'metadata',
             )
             column_headers += (
@@ -2512,40 +2541,38 @@ class ListServer(command.Lister):
                 'Properties',
             )
 
-        marker_id = None
-
         # support for additional columns
         if parsed_args.columns:
             for c in parsed_args.columns:
                 if c in ('Project ID', 'project_id'):
-                    columns += ('tenant_id',)
+                    columns += ('project_id',)
                     column_headers += ('Project ID',)
                 if c in ('User ID', 'user_id'):
                     columns += ('user_id',)
                     column_headers += ('User ID',)
                 if c in ('Created At', 'created_at'):
-                    columns += ('created',)
+                    columns += ('created_at',)
                     column_headers += ('Created At',)
                 if c in ('Security Groups', 'security_groups'):
                     columns += ('security_groups_name',)
                     column_headers += ('Security Groups',)
                 if c in ("Task State", "task_state"):
-                    columns += ('OS-EXT-STS:task_state',)
+                    columns += ('task_state',)
                     column_headers += ('Task State',)
                 if c in ("Power State", "power_state"):
-                    columns += ('OS-EXT-STS:power_state',)
+                    columns += ('power_state',)
                     column_headers += ('Power State',)
                 if c in ("Image ID", "image_id"):
                     columns += ('Image ID',)
                     column_headers += ('Image ID',)
                 if c in ("Flavor ID", "flavor_id"):
-                    columns += ('Flavor ID',)
+                    columns += ('flavor_id',)
                     column_headers += ('Flavor ID',)
                 if c in ('Availability Zone', "availability_zone"):
-                    columns += ('OS-EXT-AZ:availability_zone',)
+                    columns += ('availability_zone',)
                     column_headers += ('Availability Zone',)
                 if c in ('Host', "host"):
-                    columns += ('OS-EXT-SRV-ATTR:host',)
+                    columns += ('hypervisor_hostname',)
                     column_headers += ('Host',)
                 if c in ('Properties', "properties"):
                     columns += ('Metadata',)
@@ -2555,7 +2582,7 @@ class ListServer(command.Lister):
             column_headers = tuple(column_headers)
             columns = tuple(columns)
 
-        if parsed_args.marker:
+        if parsed_args.marker is not None:
             # Check if both "--marker" and "--deleted" are used.
             # In that scenario a lookup is not needed as the marker
             # needs to be an ID, because find_resource does not
@@ -2563,16 +2590,10 @@ class ListServer(command.Lister):
             if parsed_args.deleted:
                 marker_id = parsed_args.marker
             else:
-                marker_id = utils.find_resource(
-                    compute_client.servers,
-                    parsed_args.marker,
-                ).id
+                marker_id = compute_client.find_server(parsed_args.marker).id
+            search_opts['marker'] = marker_id
 
-        data = compute_client.servers.list(
-            search_opts=search_opts,
-            marker=marker_id,
-            limit=parsed_args.limit,
-        )
+        data = list(compute_client.servers(**search_opts))
 
         images = {}
         flavors = {}
@@ -2627,12 +2648,12 @@ class ListServer(command.Lister):
                     # "Flavor Name" is not crucial, so we swallow any
                     # exceptions
                     try:
-                        flavors[f_id] = compute_client.flavors.get(f_id)
+                        flavors[f_id] = compute_client.find_flavor(f_id)
                     except Exception:
                         pass
             else:
                 try:
-                    flavors_list = compute_client.flavors.list(is_public=None)
+                    flavors_list = compute_client.flavors(is_public=None)
                     for i in flavors_list:
                         flavors[i.id] = i
                 except Exception:
@@ -2641,7 +2662,7 @@ class ListServer(command.Lister):
         # Populate image_name, image_id, flavor_name and flavor_id attributes
         # of server objects so that we can display those columns.
         for s in data:
-            if compute_client.api_version >= api_versions.APIVersion('2.69'):
+            if sdk_utils.supports_microversion(compute_client, '2.69'):
                 # NOTE(tssurya): From 2.69, we will have the keys 'flavor'
                 # and 'image' missing in the server response during
                 # infrastructure failure situations.
@@ -2650,7 +2671,7 @@ class ListServer(command.Lister):
                 if not hasattr(s, 'image') or not hasattr(s, 'flavor'):
                     continue
 
-            if 'id' in s.image:
+            if 'id' in s.image and s.image.id is not None:
                 image = images.get(s.image['id'])
                 if image:
                     s.image_name = image.name
@@ -2663,7 +2684,7 @@ class ListServer(command.Lister):
                 s.image_name = IMAGE_STRING_FOR_BFV
                 s.image_id = IMAGE_STRING_FOR_BFV
 
-            if compute_client.api_version < api_versions.APIVersion('2.47'):
+            if not sdk_utils.supports_microversion(compute_client, '2.47'):
                 flavor = flavors.get(s.flavor['id'])
                 if flavor:
                     s.flavor_name = flavor.name
@@ -2673,7 +2694,7 @@ class ListServer(command.Lister):
 
         # Add a list with security group name as attribute
         for s in data:
-            if hasattr(s, 'security_groups'):
+            if hasattr(s, 'security_groups') and s.security_groups is not None:
                 s.security_groups_name = [x["name"] for x in s.security_groups]
             else:
                 s.security_groups_name = []
@@ -2686,10 +2707,10 @@ class ListServer(command.Lister):
         # it's on, providing useful information to a user in this
         # situation.
         if (
-            compute_client.api_version >= api_versions.APIVersion('2.16') and
+            sdk_utils.supports_microversion(compute_client, '2.16') and
             parsed_args.long
         ):
-            if any([hasattr(s, 'host_status') for s in data]):
+            if any([s.host_status is not None for s in data]):
                 columns += ('Host Status',)
                 column_headers += ('Host Status',)
 
@@ -2699,16 +2720,17 @@ class ListServer(command.Lister):
                 utils.get_item_properties(
                     s, columns,
                     mixed_case_fields=(
-                        'OS-EXT-STS:task_state',
-                        'OS-EXT-STS:power_state',
-                        'OS-EXT-AZ:availability_zone',
-                        'OS-EXT-SRV-ATTR:host',
+                        'task_state',
+                        'power_state',
+                        'availability_zone',
+                        'host',
                     ),
                     formatters={
-                        'OS-EXT-STS:power_state': PowerStateColumn,
-                        'networks': format_columns.DictListColumn,
+                        'power_state': PowerStateColumn,
+                        'addresses': AddressesColumn,
                         'metadata': format_columns.DictColumn,
                         'security_groups_name': format_columns.ListColumn,
+                        'hypervisor_hostname': HostColumn,
                     },
                 ) for s in data
             ),
diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py
index 0558ef6213..8c62fa9d45 100644
--- a/openstackclient/tests/functional/compute/v2/test_server.py
+++ b/openstackclient/tests/functional/compute/v2/test_server.py
@@ -105,7 +105,7 @@ class ServerTests(common.ComputeTestCase):
                 'server list -f json --deleted --marker ' + name2
             ))
         except exceptions.CommandFailed as e:
-            self.assertIn('marker [%s] not found (HTTP 400)' % (name2),
+            self.assertIn('marker [%s] not found' % (name2),
                           e.stderr.decode('utf-8'))
 
     def test_server_list_with_changes_before(self):
diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py
index f59c954a3b..45aff6a370 100644
--- a/openstackclient/tests/unit/compute/v2/test_server.py
+++ b/openstackclient/tests/unit/compute/v2/test_server.py
@@ -117,6 +117,21 @@ class TestServer(compute_fakes.TestComputev2):
         # Set object methods to be tested. Could be overwritten in subclass.
         self.methods = {}
 
+        patcher = mock.patch.object(
+            sdk_utils, 'supports_microversion', return_value=True)
+        self.addCleanup(patcher.stop)
+        self.supports_microversion_mock = patcher.start()
+        self._set_mock_microversion(
+            self.app.client_manager.compute.api_version.get_string())
+
+    def _set_mock_microversion(self, mock_v):
+        """Set a specific microversion for the mock supports_microversion()."""
+        self.supports_microversion_mock.reset_mock(return_value=True)
+
+        self.supports_microversion_mock.side_effect = (
+            lambda _, v:
+            api_versions.APIVersion(v) <= api_versions.APIVersion(mock_v))
+
     def setup_servers_mock(self, count):
         # If we are creating more than one server, make one of them
         # boot-from-volume
@@ -4448,32 +4463,25 @@ class _TestServerList(TestServer):
     def setUp(self):
         super(_TestServerList, self).setUp()
 
-        self.search_opts = {
+        # Default params of the core function of the command in the case of no
+        # commandline option specified.
+        self.kwargs = {
             'reservation_id': None,
             'ip': None,
             'ip6': None,
             'name': None,
-            'instance_name': None,
             'status': None,
             'flavor': None,
             'image': None,
             'host': None,
-            'tenant_id': None,
-            'all_tenants': False,
+            'project_id': None,
+            'all_projects': False,
             'user_id': None,
             'deleted': False,
             'changes-since': None,
             'changes-before': None,
         }
 
-        # Default params of the core function of the command in the case of no
-        # commandline option specified.
-        self.kwargs = {
-            'search_opts': self.search_opts,
-            'marker': None,
-            'limit': None,
-        }
-
         # The fake servers' attributes. Use the original attributes names in
         # nova, not the ones printed by "server list" command.
         self.attrs = {
@@ -4488,10 +4496,6 @@ class _TestServerList(TestServer):
             'Metadata': format_columns.DictColumn({}),
         }
 
-        # The servers to be listed.
-        self.servers = self.setup_servers_mock(3)
-        self.servers_mock.list.return_value = self.servers
-
         self.image = image_fakes.create_one_image()
 
         # self.images_mock.return_value = [self.image]
@@ -4499,7 +4503,12 @@ class _TestServerList(TestServer):
         self.get_image_mock.return_value = self.image
 
         self.flavor = compute_fakes.FakeFlavor.create_one_flavor()
-        self.flavors_mock.get.return_value = self.flavor
+        self.sdk_client.find_flavor.return_value = self.flavor
+        self.attrs['flavor'] = {'original_name': self.flavor.name}
+
+        # The servers to be listed.
+        self.servers = self.setup_sdk_servers_mock(3)
+        self.sdk_client.servers.return_value = self.servers
 
         # Get the command object to test
         self.cmd = server.ListServer(self.app, None)
@@ -4518,7 +4527,7 @@ class TestServerList(_TestServerList):
         ]
 
         Flavor = collections.namedtuple('Flavor', 'id name')
-        self.flavors_mock.list.return_value = [
+        self.sdk_client.flavors.return_value = [
             Flavor(id=s.flavor['id'], name=self.flavor.name)
             for s in self.servers
         ]
@@ -4528,7 +4537,7 @@ class TestServerList(_TestServerList):
                 s.id,
                 s.name,
                 s.status,
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                 self.flavor.name,
@@ -4547,9 +4556,9 @@ class TestServerList(_TestServerList):
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_called()
-        self.flavors_mock.list.assert_called()
+        self.sdk_client.flavors.assert_called()
         # we did not pass image or flavor, so gets on those must be absent
         self.assertFalse(self.flavors_mock.get.call_count)
         self.assertFalse(self.get_image_mock.call_count)
@@ -4564,14 +4573,14 @@ class TestServerList(_TestServerList):
             ('deleted', False),
         ]
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
-        self.servers_mock.list.return_value = []
+        self.sdk_client.servers.return_value = []
         self.data = ()
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_not_called()
-        self.flavors_mock.list.assert_not_called()
+        self.sdk_client.flavors.assert_not_called()
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
 
@@ -4581,19 +4590,19 @@ class TestServerList(_TestServerList):
                 s.id,
                 s.name,
                 s.status,
-                getattr(s, 'OS-EXT-STS:task_state'),
+                getattr(s, 'task_state'),
                 server.PowerStateColumn(
-                    getattr(s, 'OS-EXT-STS:power_state')
+                    getattr(s, 'power_state')
                 ),
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                 s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                 self.flavor.name,
                 s.flavor['id'],
-                getattr(s, 'OS-EXT-AZ:availability_zone'),
-                getattr(s, 'OS-EXT-SRV-ATTR:host'),
-                s.Metadata,
+                getattr(s, 'availability_zone'),
+                server.HostColumn(getattr(s, 'hypervisor_hostname')),
+                format_columns.DictColumn(s.metadata),
             ) for s in self.servers
         )
         arglist = [
@@ -4606,12 +4615,12 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
 
         columns, data = self.cmd.take_action(parsed_args)
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         image_ids = {s.image['id'] for s in self.servers if s.image}
         self.images_mock.assert_called_once_with(
             id=f'in:{",".join(image_ids)}',
         )
-        self.flavors_mock.list.assert_called_once_with(is_public=None)
+        self.sdk_client.flavors.assert_called_once_with(is_public=None)
         self.assertEqual(self.columns_long, columns)
         self.assertEqual(self.data, tuple(data))
 
@@ -4637,7 +4646,7 @@ class TestServerList(_TestServerList):
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertIn('Project ID', columns)
         self.assertIn('User ID', columns)
         self.assertIn('Created At', columns)
@@ -4656,7 +4665,7 @@ class TestServerList(_TestServerList):
                 s.id,
                 s.name,
                 s.status,
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                 s.flavor['id']
@@ -4674,9 +4683,9 @@ class TestServerList(_TestServerList):
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_not_called()
-        self.flavors_mock.list.assert_not_called()
+        self.sdk_client.flavors.assert_not_called()
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
 
@@ -4686,7 +4695,7 @@ class TestServerList(_TestServerList):
                 s.id,
                 s.name,
                 s.status,
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                 s.flavor['id']
@@ -4704,9 +4713,9 @@ class TestServerList(_TestServerList):
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_not_called()
-        self.flavors_mock.list.assert_not_called()
+        self.sdk_client.flavors.assert_not_called()
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
 
@@ -4723,11 +4732,11 @@ class TestServerList(_TestServerList):
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_not_called()
-        self.flavors_mock.list.assert_not_called()
+        self.sdk_client.flavors.assert_not_called()
         self.get_image_mock.assert_called()
-        self.flavors_mock.get.assert_called()
+        self.sdk_client.find_flavor.assert_called()
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
@@ -4747,10 +4756,10 @@ class TestServerList(_TestServerList):
         self.find_image_mock.assert_called_with(self.image.id,
                                                 ignore_missing=False)
 
-        self.search_opts['image'] = self.image.id
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['image'] = self.image.id
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_not_called()
-        self.flavors_mock.list.assert_called_once()
+        self.sdk_client.flavors.assert_called_once()
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
@@ -4767,12 +4776,13 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.flavors_mock.get.assert_has_calls([mock.call(self.flavor.id)])
+        self.sdk_client.find_flavor.assert_has_calls(
+            [mock.call(self.flavor.id)])
 
-        self.search_opts['flavor'] = self.flavor.id
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['flavor'] = self.flavor.id
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.images_mock.assert_called_once()
-        self.flavors_mock.list.assert_not_called()
+        self.sdk_client.flavors.assert_not_called()
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
@@ -4791,9 +4801,9 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['changes-since'] = '2016-03-04T06:27:59Z'
-        self.search_opts['deleted'] = True
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['changes-since'] = '2016-03-04T06:27:59Z'
+        self.kwargs['deleted'] = True
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
@@ -4820,8 +4830,7 @@ class TestServerList(_TestServerList):
         )
 
     def test_server_list_with_tag(self):
-        self.app.client_manager.compute.api_version = api_versions.APIVersion(
-            '2.26')
+        self._set_mock_microversion('2.26')
 
         arglist = [
             '--tag', 'tag1',
@@ -4834,16 +4843,15 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['tags'] = 'tag1,tag2'
+        self.kwargs['tags'] = 'tag1,tag2'
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
 
     def test_server_list_with_tag_pre_v225(self):
-        self.app.client_manager.compute.api_version = api_versions.APIVersion(
-            '2.25')
+        self._set_mock_microversion('2.25')
 
         arglist = [
             '--tag', 'tag1',
@@ -4863,9 +4871,7 @@ class TestServerList(_TestServerList):
             str(ex))
 
     def test_server_list_with_not_tag(self):
-        self.app.client_manager.compute.api_version = api_versions.APIVersion(
-            '2.26')
-
+        self._set_mock_microversion('2.26')
         arglist = [
             '--not-tag', 'tag1',
             '--not-tag', 'tag2',
@@ -4877,16 +4883,15 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['not-tags'] = 'tag1,tag2'
+        self.kwargs['not-tags'] = 'tag1,tag2'
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertEqual(self.columns, columns)
         self.assertEqual(self.data, tuple(data))
 
     def test_server_list_with_not_tag_pre_v226(self):
-        self.app.client_manager.compute.api_version = api_versions.APIVersion(
-            '2.25')
+        self._set_mock_microversion('2.25')
 
         arglist = [
             '--not-tag', 'tag1',
@@ -4916,8 +4921,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['availability_zone'] = 'test-az'
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['availability_zone'] = 'test-az'
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -4932,8 +4937,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['key_name'] = 'test-key'
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['key_name'] = 'test-key'
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -4948,8 +4953,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['config_drive'] = True
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['config_drive'] = True
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -4964,8 +4969,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['config_drive'] = False
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['config_drive'] = False
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -4980,8 +4985,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['progress'] = '100'
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['progress'] = '100'
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -5005,8 +5010,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['vm_state'] = 'active'
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['vm_state'] = 'active'
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -5021,8 +5026,8 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['task_state'] = 'deleting'
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['task_state'] = 'deleting'
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
@@ -5037,33 +5042,31 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['power_state'] = 1
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['power_state'] = 1
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
         self.assertEqual(self.columns, columns)
         self.assertEqual(tuple(self.data), tuple(data))
 
     def test_server_list_long_with_host_status_v216(self):
-        self.app.client_manager.compute.api_version = \
-            api_versions.APIVersion('2.16')
-
+        self._set_mock_microversion('2.16')
         self.data1 = tuple(
             (
                 s.id,
                 s.name,
                 s.status,
-                getattr(s, 'OS-EXT-STS:task_state'),
+                getattr(s, 'task_state'),
                 server.PowerStateColumn(
-                    getattr(s, 'OS-EXT-STS:power_state')
+                    getattr(s, 'power_state')
                 ),
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                 s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                 self.flavor.name,
                 s.flavor['id'],
-                getattr(s, 'OS-EXT-AZ:availability_zone'),
-                getattr(s, 'OS-EXT-SRV-ATTR:host'),
-                s.Metadata,
+                getattr(s, 'availability_zone'),
+                server.HostColumn(getattr(s, 'hypervisor_hostname')),
+                format_columns.DictColumn(s.metadata),
             ) for s in self.servers)
 
         arglist = [
@@ -5078,18 +5081,18 @@ class TestServerList(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertEqual(self.columns_long, columns)
         self.assertEqual(tuple(self.data1), tuple(data))
 
         # Next test with host_status in the data -- the column should be
         # present in this case.
-        self.servers_mock.reset_mock()
+        self.sdk_client.servers.reset_mock()
 
         self.attrs['host_status'] = 'UP'
-        servers = self.setup_servers_mock(3)
-        self.servers_mock.list.return_value = servers
+        servers = self.setup_sdk_servers_mock(3)
+        self.sdk_client.servers.return_value = servers
 
         # Make sure the returned image and flavor IDs match the servers.
         Image = collections.namedtuple('Image', 'id name')
@@ -5099,12 +5102,6 @@ class TestServerList(_TestServerList):
             for s in servers if s.image
         ]
 
-        Flavor = collections.namedtuple('Flavor', 'id name')
-        self.flavors_mock.list.return_value = [
-            Flavor(id=s.flavor['id'], name=self.flavor.name)
-            for s in servers
-        ]
-
         # Add the expected host_status column and data.
         columns_long = self.columns_long + ('Host Status',)
         self.data2 = tuple(
@@ -5112,25 +5109,25 @@ class TestServerList(_TestServerList):
                 s.id,
                 s.name,
                 s.status,
-                getattr(s, 'OS-EXT-STS:task_state'),
+                getattr(s, 'task_state'),
                 server.PowerStateColumn(
-                    getattr(s, 'OS-EXT-STS:power_state')
+                    getattr(s, 'power_state')
                 ),
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                 s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
                 self.flavor.name,
                 s.flavor['id'],
-                getattr(s, 'OS-EXT-AZ:availability_zone'),
-                getattr(s, 'OS-EXT-SRV-ATTR:host'),
-                s.Metadata,
+                getattr(s, 'availability_zone'),
+                server.HostColumn(getattr(s, 'hypervisor_hostname')),
+                format_columns.DictColumn(s.metadata),
                 s.host_status,
             ) for s in servers)
 
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertEqual(columns_long, columns)
         self.assertEqual(tuple(self.data2), tuple(data))
@@ -5178,8 +5175,8 @@ class TestServerListV273(_TestServerList):
         }
 
         # The servers to be listed.
-        self.servers = self.setup_servers_mock(3)
-        self.servers_mock.list.return_value = self.servers
+        self.servers = self.setup_sdk_servers_mock(3)
+        self.sdk_client.servers.return_value = self.servers
 
         Image = collections.namedtuple('Image', 'id name')
         self.images_mock.return_value = [
@@ -5190,14 +5187,14 @@ class TestServerListV273(_TestServerList):
 
         # The flavor information is embedded, so now reason for this to be
         # called
-        self.flavors_mock.list = mock.NonCallableMock()
+        self.sdk_client.flavors = mock.NonCallableMock()
 
         self.data = tuple(
             (
                 s.id,
                 s.name,
                 s.status,
-                format_columns.DictListColumn(s.networks),
+                server.AddressesColumn(s.addresses),
                 # Image will be an empty string if boot-from-volume
                 self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
                 self.flavor.name,
@@ -5221,8 +5218,7 @@ class TestServerListV273(_TestServerList):
 
     def test_server_list_with_locked(self):
 
-        self.app.client_manager.compute.api_version = \
-            api_versions.APIVersion('2.73')
+        self._set_mock_microversion('2.73')
         arglist = [
             '--locked'
         ]
@@ -5233,16 +5229,15 @@ class TestServerListV273(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['locked'] = True
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['locked'] = True
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertCountEqual(self.columns, columns)
         self.assertCountEqual(self.data, tuple(data))
 
     def test_server_list_with_unlocked_v273(self):
+        self._set_mock_microversion('2.73')
 
-        self.app.client_manager.compute.api_version = \
-            api_versions.APIVersion('2.73')
         arglist = [
             '--unlocked'
         ]
@@ -5253,16 +5248,15 @@ class TestServerListV273(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['locked'] = False
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.kwargs['locked'] = False
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertCountEqual(self.columns, columns)
         self.assertCountEqual(self.data, tuple(data))
 
     def test_server_list_with_locked_and_unlocked(self):
 
-        self.app.client_manager.compute.api_version = \
-            api_versions.APIVersion('2.73')
+        self._set_mock_microversion('2.73')
         arglist = [
             '--locked',
             '--unlocked'
@@ -5278,8 +5272,7 @@ class TestServerListV273(_TestServerList):
         self.assertIn('Argument parse failed', str(ex))
 
     def test_server_list_with_changes_before(self):
-        self.app.client_manager.compute.api_version = (
-            api_versions.APIVersion('2.66'))
+        self._set_mock_microversion('2.66')
         arglist = [
             '--changes-before', '2016-03-05T06:27:59Z',
             '--deleted'
@@ -5292,10 +5285,10 @@ class TestServerListV273(_TestServerList):
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.search_opts['changes-before'] = '2016-03-05T06:27:59Z'
-        self.search_opts['deleted'] = True
+        self.kwargs['changes-before'] = '2016-03-05T06:27:59Z'
+        self.kwargs['deleted'] = True
 
-        self.servers_mock.list.assert_called_with(**self.kwargs)
+        self.sdk_client.servers.assert_called_with(**self.kwargs)
 
         self.assertCountEqual(self.columns, columns)
         self.assertCountEqual(self.data, tuple(data))
@@ -5303,9 +5296,7 @@ class TestServerListV273(_TestServerList):
     @mock.patch.object(iso8601, 'parse_date', side_effect=iso8601.ParseError)
     def test_server_list_with_invalid_changes_before(
             self, mock_parse_isotime):
-        self.app.client_manager.compute.api_version = (
-            api_versions.APIVersion('2.66'))
-
+        self._set_mock_microversion('2.66')
         arglist = [
             '--changes-before', 'Invalid time value',
         ]
@@ -5325,8 +5316,7 @@ class TestServerListV273(_TestServerList):
         )
 
     def test_server_with_changes_before_pre_v266(self):
-        self.app.client_manager.compute.api_version = (
-            api_versions.APIVersion('2.65'))
+        self._set_mock_microversion('2.65')
 
         arglist = [
             '--changes-before', '2016-03-05T06:27:59Z',
@@ -5344,8 +5334,7 @@ class TestServerListV273(_TestServerList):
                           parsed_args)
 
     def test_server_list_v269_with_partial_constructs(self):
-        self.app.client_manager.compute.api_version = \
-            api_versions.APIVersion('2.69')
+        self._set_mock_microversion('2.69')
         arglist = []
         verifylist = []
         parsed_args = self.check_parser(self.cmd, arglist, verifylist)
@@ -5371,10 +5360,10 @@ class TestServerListV273(_TestServerList):
             # it will fail at formatting the networks info later on.
             "networks": {}
         }
-        server = compute_fakes.fakes.FakeResource(
+        fake_server = compute_fakes.fakes.FakeResource(
             info=server_dict,
         )
-        self.servers.append(server)
+        self.servers.append(fake_server)
         columns, data = self.cmd.take_action(parsed_args)
         # get the first three servers out since our interest is in the partial
         # server.
@@ -5384,7 +5373,7 @@ class TestServerListV273(_TestServerList):
         partial_server = next(data)
         expected_row = (
             'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', '',
-            'UNKNOWN', format_columns.DictListColumn({}), '', '')
+            'UNKNOWN', server.AddressesColumn(''), '', '')
         self.assertEqual(expected_row, partial_server)
 
 
diff --git a/releasenotes/notes/nova-gaps-server-list-to-sdk-88c8bfc10a9e3032.yaml b/releasenotes/notes/nova-gaps-server-list-to-sdk-88c8bfc10a9e3032.yaml
new file mode 100644
index 0000000000..01d690e3e9
--- /dev/null
+++ b/releasenotes/notes/nova-gaps-server-list-to-sdk-88c8bfc10a9e3032.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    The ``server list`` command now uses the OpenStack SDK instead of the
+    Python nova bindings.