]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: fix list_hosts and list_locations nvmeof cli commands empty table bug 70223/head
authorTomer Haskalovitch <tomer.haska@ibm.com>
Wed, 15 Jul 2026 07:48:27 +0000 (10:48 +0300)
committerTomer Haskalovitch <tomer.haska@ibm.com>
Wed, 22 Jul 2026 08:27:29 +0000 (11:27 +0300)
Signed-off-by: Tomer Haskalovitch <tomer.haska@ibm.com>
src/pybind/mgr/dashboard/controllers/nvmeof.py
src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py

index 832d3b28c16f15882ce7b25b301ed1ef12144dba..05024205aba5a92afe6da59c3e19bb178efc9042 100644 (file)
@@ -30,7 +30,7 @@ NVME_SCHEMA = {
 
 try:
     from ..services.nvmeof_client import NVMeoFClient, convert_to_model, \
-        empty_response, handle_nvmeof_error, pick
+        empty_response, handle_nvmeof_error, namedtuple_to_dict, pick
 except ImportError as e:
     logger.error("Failed to import NVMeoFClient and related components: %s", e)
 else:
@@ -1833,11 +1833,11 @@ else:
                     hosts=list(ns.hosts)
                 ))
 
-            return model.NamespaceHostsList(
+            return namedtuple_to_dict(model.NamespaceHostsList(
                 status=ns_list.status,
                 error_message=ns_list.error_message,
                 namespaces=host_infos
-            )
+            ))
 
         @ReadPermission
         @Endpoint('GET', 'list_locations')
@@ -1897,11 +1897,11 @@ else:
                     namespace_count=count
                 ))
 
-            return model.NamespaceLocationsList(
+            return namedtuple_to_dict(model.NamespaceLocationsList(
                 status=ns_list.status,
                 error_message=ns_list.error_message,
                 locations=location_infos
-            )
+            ))
 
         @ReadPermission
         @Endpoint('PUT', '{nsid}/set_auto_resize')
index 0821da9fcce578552bf6efbe67af781ebb4dde15..d06359e4ed38290b00f7c4388351e40785bffbe5 100644 (file)
@@ -1770,3 +1770,197 @@ class TestCliEmptyMessageForAllListCommands:
             assert '"hosts": []' in result.stdout
         finally:
             del NvmeofCLICommand.COMMANDS[test_cmd_hosts]
+
+
+class TestNamespaceListHosts:
+    def test_non_empty_hosts_renders_table(self):
+        from ..model.nvmeof import NamespaceHostsList
+
+        test_cmd = "nvmeof test namespace list_hosts non empty"
+
+        @NvmeofCLICommand(test_cmd, NamespaceHostsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'namespaces': [
+                    {
+                        'nqn': 'nqn.2001-07.com.ceph:1783923316687.group1',
+                        'nsid': 1,
+                        'hosts': [
+                            'nqn.2014-08.org.nvmexpress:uuid:8d78a673-8b0f-4228-ae7e-89f5fd5c3a74'
+                        ],
+                    }
+                ],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            host_nqn = 'nqn.2014-08.org.nvmexpress:uuid:8d78a673-8b0f-4228-ae7e-89f5fd5c3a74'
+            assert host_nqn in result.stdout
+            assert '1' in result.stdout  # nsid
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]
+
+    def test_empty_hosts_renders_empty_table(self):
+        from ..model.nvmeof import NamespaceHostsList
+
+        test_cmd = "nvmeof test namespace list_hosts empty"
+
+        @NvmeofCLICommand(test_cmd, NamespaceHostsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'namespaces': [
+                    {
+                        'nqn': 'nqn.2001-07.com.ceph:1783923316687.group1',
+                        'nsid': 2,
+                        'hosts': [],
+                    }
+                ],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            assert 'None' in result.stdout
+            assert '2' in result.stdout  # nsid
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]
+
+    def test_multiple_hosts_per_namespace(self):
+        from ..model.nvmeof import NamespaceHostsList
+
+        test_cmd = "nvmeof test namespace list_hosts multi"
+
+        @NvmeofCLICommand(test_cmd, NamespaceHostsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'namespaces': [
+                    {
+                        'nqn': 'nqn.2001-07.com.ceph:test',
+                        'nsid': 1,
+                        'hosts': [
+                            'nqn.2014-08.org.nvmexpress:uuid:host-aaa',
+                            'nqn.2014-08.org.nvmexpress:uuid:host-bbb',
+                        ],
+                    },
+                    {
+                        'nqn': 'nqn.2001-07.com.ceph:test',
+                        'nsid': 2,
+                        'hosts': [],
+                    },
+                ],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            assert 'nqn.2014-08.org.nvmexpress:uuid:host-aaa' in result.stdout
+            assert 'nqn.2014-08.org.nvmexpress:uuid:host-bbb' in result.stdout
+            assert 'None' in result.stdout  # nsid 2 has no hosts
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]
+
+
+class TestNamespaceListLocations:
+    def test_non_empty_locations_renders_table(self):
+        from ..model.nvmeof import NamespaceLocationsList
+
+        test_cmd = "nvmeof test namespace list_locations non empty"
+
+        @NvmeofCLICommand(test_cmd, NamespaceLocationsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'locations': [
+                    {
+                        'subsystem': 'nqn.2001-07.com.ceph:test',
+                        'load_balancing_group': 1,
+                        'location': 'india',
+                        'namespace_count': 2,
+                    }
+                ],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            assert 'nqn.2001-07.com.ceph:test' in result.stdout
+            assert 'india' in result.stdout
+            assert '2' in result.stdout  # namespace_count
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]
+
+    def test_empty_locations_renders_empty_table(self):
+        from ..model.nvmeof import NamespaceLocationsList
+
+        test_cmd = "nvmeof test namespace list_locations empty"
+
+        @NvmeofCLICommand(test_cmd, NamespaceLocationsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'locations': [],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            assert result.stdout == ''
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]
+
+    def test_multiple_locations_aggregated(self):
+        from ..model.nvmeof import NamespaceLocationsList
+
+        test_cmd = "nvmeof test namespace list_locations multi"
+
+        @NvmeofCLICommand(test_cmd, NamespaceLocationsList)
+        def func(_):  # pylint: disable=unused-argument, unused-variable
+            return {
+                'status': 0,
+                'error_message': '',
+                'locations': [
+                    {
+                        'subsystem': 'nqn.2001-07.com.ceph:test',
+                        'load_balancing_group': 1,
+                        'location': 'india',
+                        'namespace_count': 3,
+                    },
+                    {
+                        'subsystem': 'nqn.2001-07.com.ceph:test',
+                        'load_balancing_group': 2,
+                        'location': '<default>',
+                        'namespace_count': 1,
+                    },
+                ],
+            }
+
+        try:
+            result = NvmeofCLICommand.COMMANDS[test_cmd].call(MagicMock(), {})
+
+            assert isinstance(result, HandleCommandResult)
+            assert result.retval == 0
+            assert 'india' in result.stdout
+            assert '<default>' in result.stdout
+            assert '3' in result.stdout
+            assert '1' in result.stdout
+        finally:
+            del NvmeofCLICommand.COMMANDS[test_cmd]