From: Tomer Haskalovitch Date: Wed, 15 Jul 2026 07:48:27 +0000 (+0300) Subject: mgr/dashboard: fix list_hosts and list_locations nvmeof cli commands empty table bug X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a41999233018d85171d3a94df580f028f35f951c;p=ceph.git mgr/dashboard: fix list_hosts and list_locations nvmeof cli commands empty table bug Signed-off-by: Tomer Haskalovitch --- diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index 832d3b28c16..05024205aba 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -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') diff --git a/src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py b/src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py index 0821da9fcce..d06359e4ed3 100644 --- a/src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py +++ b/src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py @@ -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': '', + '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 '' in result.stdout + assert '3' in result.stdout + assert '1' in result.stdout + finally: + del NvmeofCLICommand.COMMANDS[test_cmd]