diff --git a/os_brick/initiator/connectors/nvmeof.py b/os_brick/initiator/connectors/nvmeof.py index 4835ae1c4..cfedee1a9 100644 --- a/os_brick/initiator/connectors/nvmeof.py +++ b/os_brick/initiator/connectors/nvmeof.py @@ -43,6 +43,8 @@ LOG = logging.getLogger(__name__) class NVMeOFConnector(base.BaseLinuxConnector): """Connector class to attach/detach NVMe-oF volumes.""" + native_multipath_supported = None + def __init__(self, root_helper, driver=None, use_multipath=False, device_scan_attempts=DEVICE_SCAN_ATTEMPTS_DEFAULT, *args, **kwargs): @@ -52,6 +54,10 @@ class NVMeOFConnector(base.BaseLinuxConnector): device_scan_attempts=device_scan_attempts, *args, **kwargs) self.use_multipath = use_multipath + self._set_native_multipath_supported() + if self.use_multipath and not \ + NVMeOFConnector.native_multipath_supported: + LOG.warning('native multipath is not enabled') @staticmethod def get_search_path(): @@ -99,7 +105,6 @@ class NVMeOFConnector(base.BaseLinuxConnector): execute = kwargs.get('execute') or priv_rootwrap.execute nvmf = NVMeOFConnector(root_helper=root_helper, execute=execute) ret = {} - nqn = None uuid = nvmf._get_host_uuid() suuid = nvmf._get_system_uuid() @@ -111,6 +116,7 @@ class NVMeOFConnector(base.BaseLinuxConnector): ret['system uuid'] = suuid # compatibility if nqn: ret['nqn'] = nqn + ret['nvme_native_multipath'] = cls._set_native_multipath_supported() return ret def _get_host_uuid(self): @@ -148,6 +154,22 @@ class NVMeOFConnector(base.BaseLinuxConnector): out = "" return out.strip() + @classmethod + def _set_native_multipath_supported(cls): + if cls.native_multipath_supported is None: + cls.native_multipath_supported = \ + cls._is_native_multipath_supported() + return cls.native_multipath_supported + + @staticmethod + def _is_native_multipath_supported(): + try: + with open('/sys/module/nvme_core/parameters/multipath', 'rt') as f: + return f.read().strip() == 'Y' + except Exception: + LOG.warning("Could not find nvme_core/parameters/multipath") + return False + def _get_nvme_devices(self): nvme_devices = [] # match nvme devices like /dev/nvme10n10 @@ -321,7 +343,7 @@ class NVMeOFConnector(base.BaseLinuxConnector): :returns: dict """ if connection_properties.get('vol_uuid'): # compatibility - return self._connect_volume_replicated(connection_properties) + return self._connect_volume_by_uuid(connection_properties) current_nvme_devices = self._get_nvme_devices() device_info = {'type': 'block'} @@ -425,7 +447,7 @@ class NVMeOFConnector(base.BaseLinuxConnector): raise exception.VolumePathsNotFound() @utils.trace - def _connect_volume_replicated(self, connection_properties): + def _connect_volume_by_uuid(self, connection_properties): """connect to volume on host connection_properties for NVMe-oF must include: @@ -433,7 +455,6 @@ class NVMeOFConnector(base.BaseLinuxConnector): target_nqn - NVMe-oF Qualified Name vol_uuid - UUID for volume/replica """ - volume_replicas = connection_properties.get('volume_replicas') replica_count = connection_properties.get('replica_count') volume_alias = connection_properties.get('alias') @@ -499,6 +520,8 @@ class NVMeOFConnector(base.BaseLinuxConnector): NVMeOFConnector.run_mdadm( self, ['mdadm', '--grow', '--size', 'max', device_path]) else: + target_nqn = None + vol_uuid = None if not volume_replicas: target_nqn = connection_properties['target_nqn'] vol_uuid = connection_properties['vol_uuid'] @@ -511,28 +534,43 @@ class NVMeOFConnector(base.BaseLinuxConnector): return self._linuxscsi.get_device_size(device_path) def _connect_target_volume(self, target_nqn, vol_uuid, portals): - try: - NVMeOFConnector._get_nvme_controller(self, target_nqn) - NVMeOFConnector.rescan(self, target_nqn, vol_uuid) - except exception.VolumeDeviceNotFound: - if not NVMeOFConnector.connect_to_portals( - self, target_nqn, portals): - LOG.error("No successful connections to: %s", target_nqn) - raise exception.VolumeDeviceNotFound(device=target_nqn) - dev_path = NVMeOFConnector.get_nvme_device_path( - self, target_nqn, vol_uuid) + nvme_ctrls = NVMeOFConnector.rescan(self, target_nqn) + any_new_connect = NVMeOFConnector.connect_to_portals(self, target_nqn, + portals, + nvme_ctrls) + if not any_new_connect and len(nvme_ctrls) == 0: + # no new connections and any pre-exists controllers + LOG.error("No successful connections to: %s", target_nqn) + raise exception.VolumeDeviceNotFound(device=target_nqn) + if any_new_connect: + # new connections - refresh controllers map + nvme_ctrls = \ + NVMeOFConnector.get_live_nvme_controllers_map(self, target_nqn) + nvme_ctrls_values = list(nvme_ctrls.values()) + dev_path = NVMeOFConnector.get_nvme_device_path(self, target_nqn, + vol_uuid, + nvme_ctrls_values) if not dev_path: LOG.error("Target %s volume %s not found", target_nqn, vol_uuid) raise exception.VolumeDeviceNotFound(device=vol_uuid) return dev_path @staticmethod - def connect_to_portals(executor, target_nqn, target_portals): - """connect to any of NVMe-oF target portals""" - any_connect = False + def connect_to_portals(executor, target_nqn, target_portals, nvme_ctrls): + # connect to any of NVMe-oF target portals - + # check if the controller exist before trying to connect + # in multipath connect all given target portals + any_new_connect = False + no_multipath = not executor.use_multipath or not \ + NVMeOFConnector.native_multipath_supported for portal in target_portals: portal_address = portal[0] portal_port = portal[1] + if NVMeOFConnector.is_portal_connected(portal_address, portal_port, + nvme_ctrls): + if no_multipath: + break + continue if portal[2] == 'RoCEv2': portal_transport = 'rdma' else: @@ -542,14 +580,30 @@ class NVMeOFConnector(base.BaseLinuxConnector): portal_transport, '-n', target_nqn, '-Q', '128', '-l', '-1') try: NVMeOFConnector.run_nvme_cli(executor, nvme_command) - any_connect = True - break + any_new_connect = True + if no_multipath: + break except Exception: LOG.exception("Could not connect to portal %s", portal) - return any_connect + return any_new_connect @staticmethod - def _get_nvme_controller(executor, target_nqn): + def is_portal_connected(portal_address, portal_port, nvme_ctrls): + address = f"traddr={portal_address},trsvcid={portal_port}" + return address in nvme_ctrls + + @staticmethod + def get_nvme_controllers(executor, target_nqn): + nvme_controllers = \ + NVMeOFConnector.get_live_nvme_controllers_map(executor, target_nqn) + if len(nvme_controllers) > 0: + return nvme_controllers.values() + raise exception.VolumeDeviceNotFound(device=target_nqn) + + @staticmethod + def get_live_nvme_controllers_map(executor, target_nqn): + """returns map of all live controllers and their addresses """ + nvme_controllers = dict() ctrls = glob.glob('/sys/class/nvme-fabrics/ctl/nvme*') for ctrl in ctrls: try: @@ -561,31 +615,45 @@ class NVMeOFConnector(base.BaseLinuxConnector): state, _err = executor._execute( 'cat', ctrl + '/state', run_as_root=True, root_helper=executor._root_helper) - if 'live' not in state: + if 'live' in state: + address_file = ctrl + '/address' + try: + with open(address_file, 'rt') as f: + address = f.read().strip() + except Exception: + LOG.warning("Failed to read file %s", + address_file) + continue + ctrl_name = os.path.basename(ctrl) + LOG.debug("[!] address: %s|%s", address, ctrl_name) + nvme_controllers[address] = ctrl_name + else: LOG.debug("nvmeof ctrl device not live: %s", ctrl) - raise exception.VolumeDeviceNotFound(device=ctrl) - return ctrl[ctrl.rfind('/') + 1:] except putils.ProcessExecutionError as e: LOG.exception(e) - - raise exception.VolumeDeviceNotFound(device=target_nqn) + return nvme_controllers @staticmethod @utils.retry(exception.VolumeDeviceNotFound, retries=5) - def get_nvme_device_path(executor, target_nqn, vol_uuid): - nvme_ctrl = NVMeOFConnector._get_nvme_controller(executor, target_nqn) - uuid_paths = glob.glob('/sys/class/block/' + nvme_ctrl + 'n*/uuid') - for uuid_path in uuid_paths: - try: - uuid_lines, _err = executor._execute( - 'cat', uuid_path, run_as_root=True, - root_helper=executor._root_helper) - if uuid_lines.split('\n')[0] == vol_uuid: - ignore = len('/uuid') - return '/dev/' + uuid_path[ - uuid_path.rfind('/', 0, -ignore) + 1: -ignore] - except putils.ProcessExecutionError as e: - LOG.exception(e) + def get_nvme_device_path(executor, target_nqn, vol_uuid, nvme_ctrls=None): + if not nvme_ctrls: + nvme_ctrls = NVMeOFConnector.get_nvme_controllers(executor, + target_nqn) + LOG.debug("[!] nvme_ctrls: %s", nvme_ctrls) + for nvme_ctrl in nvme_ctrls: + uuid_paths = glob.glob('/sys/class/block/' + nvme_ctrl + 'n*/uuid') + for uuid_path in uuid_paths: + try: + uuid_lines, _err = executor._execute( + 'cat', uuid_path, run_as_root=True, + root_helper=executor._root_helper) + if uuid_lines.split('\n')[0] == vol_uuid: + ignore = len('/uuid') + ns_ind = uuid_path.rfind('/', 0, -ignore) + nvme_device = uuid_path[ns_ind + 1: -ignore] + return '/dev/' + nvme_device + except putils.ProcessExecutionError as e: + LOG.exception(e) raise exception.VolumeDeviceNotFound(device=vol_uuid) def _handle_replicated_volume(self, host_device_paths, @@ -758,7 +826,7 @@ class NVMeOFConnector(base.BaseLinuxConnector): LOG.debug('[!] cmd = ' + str(cmd)) NVMeOFConnector.run_mdadm(executor, cmd) - # sometimes under load, md is not created right away so we wait + # sometimes under load, md is not created right away, so we wait for i in range(60): try: is_exist = os.path.exists("/dev/md/" + name) @@ -836,15 +904,17 @@ class NVMeOFConnector(base.BaseLinuxConnector): return out, err @staticmethod - def rescan(executor, target_nqn, vol_uuid): - ctr_device = ( - NVMeOFConnector.get_search_path() + - NVMeOFConnector._get_nvme_controller(executor, target_nqn)) - nvme_command = ('ns-rescan', ctr_device) - try: - NVMeOFConnector.run_nvme_cli(executor, nvme_command) - except Exception as e: - raise exception.CommandExecutionFailed(e, cmd=nvme_command) + def rescan(executor, target_nqn): + nvme_ctrls = NVMeOFConnector.get_live_nvme_controllers_map(executor, + target_nqn) + for nvme_ctrl in nvme_ctrls.values(): + ctr_device = (NVMeOFConnector.get_search_path() + nvme_ctrl) + nvme_command = ('ns-rescan', ctr_device) + try: + NVMeOFConnector.run_nvme_cli(executor, nvme_command) + except Exception as e: + LOG.exception(e) + return nvme_ctrls def _get_fs_type(self, device_path): cmd = ['blkid', device_path, '-s', 'TYPE', '-o', 'value'] diff --git a/os_brick/tests/initiator/connectors/test_nvmeof.py b/os_brick/tests/initiator/connectors/test_nvmeof.py index 18faae3c9..6420ba21e 100644 --- a/os_brick/tests/initiator/connectors/test_nvmeof.py +++ b/os_brick/tests/initiator/connectors/test_nvmeof.py @@ -50,7 +50,8 @@ connection_properties = { 'replica_count': 3 } fake_portal = ('fake', 'portal', 'tcp') - +fake_controller = '/sys/class/nvme-fabrics/ctl/nvme1' +fake_controllers_map = {'traddr=fakeaddress,trsvcid=4430': 'nvme1'} nvme_list_subsystems_stdout = """ { "Subsystems" : [ @@ -137,6 +138,9 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): uuid = self.connector._get_host_uuid() self.assertIsNone(uuid) + @mock.patch.object(nvmeof.NVMeOFConnector, + '_is_native_multipath_supported', + return_value=True) @mock.patch.object(nvmeof.NVMeOFConnector, 'nvme_present', return_value=True) @mock.patch.object(utils, 'get_host_nqn', @@ -147,11 +151,15 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): return_value=None) def test_get_connector_properties_without_sysuuid(self, mock_host_uuid, mock_sysuuid, mock_nqn, - mock_nvme_present): + mock_nvme_present, + mock_nat_mpath_support): props = self.connector.get_connector_properties('sudo') - expected_props = {'nqn': 'fakenqn'} + expected_props = {'nqn': 'fakenqn', 'nvme_native_multipath': False} self.assertEqual(expected_props, props) + @mock.patch.object(nvmeof.NVMeOFConnector, + '_is_native_multipath_supported', + return_value=True) @mock.patch.object(nvmeof.NVMeOFConnector, 'nvme_present') @mock.patch.object(utils, 'get_host_nqn', autospec=True) @mock.patch.object(nvmeof.NVMeOFConnector, '_get_system_uuid', @@ -159,14 +167,15 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): @mock.patch.object(nvmeof.NVMeOFConnector, '_get_host_uuid', autospec=True) def test_get_connector_properties_with_sysuuid(self, mock_host_uuid, mock_sysuuid, mock_nqn, - mock_nvme_present): + mock_nvme_present, + mock_native_mpath_support): mock_host_uuid.return_value = HOST_UUID mock_sysuuid.return_value = SYS_UUID mock_nqn.return_value = HOST_NQN mock_nvme_present.return_value = True props = self.connector.get_connector_properties('sudo') expected_props = {"system uuid": SYS_UUID, "nqn": HOST_NQN, - "uuid": HOST_UUID} + "uuid": HOST_UUID, 'nvme_native_multipath': False} self.assertEqual(expected_props, props) def test_get_volume_paths_unreplicated(self): @@ -277,7 +286,7 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): self, mock_connect_target_volume): mock_connect_target_volume.return_value = '/dev/nvme0n1' self.assertEqual( - self.connector._connect_volume_replicated( + self.connector._connect_volume_by_uuid( { 'target_nqn': 'fakenqn', 'vol_uuid': 'fakeuuid', @@ -493,34 +502,30 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): ) mock_device_size.assert_called_with(device_path) - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') @mock.patch.object(nvmeof.NVMeOFConnector, 'rescan') @mock.patch.object(nvmeof.NVMeOFConnector, 'get_nvme_device_path') def test__connect_target_volume_with_connected_device( - self, mock_device_path, mock_rescan, mock_controller): + self, mock_device_path, mock_rescan): mock_device_path.return_value = '/dev/nvme0n1' self.assertEqual( self.connector._connect_target_volume( 'fakenqn', 'fakeuuid', [('fake', 'portal', 'tcp')]), '/dev/nvme0n1') - mock_controller.assert_called_with(self.connector, 'fakenqn') - mock_rescan.assert_called_with(self.connector, 'fakenqn', 'fakeuuid') + mock_rescan.assert_called_with(self.connector, 'fakenqn') mock_device_path.assert_called_with( - self.connector, 'fakenqn', 'fakeuuid') + self.connector, 'fakenqn', 'fakeuuid', list({}.values())) @mock.patch.object(nvmeof.NVMeOFConnector, 'connect_to_portals') @mock.patch.object(nvmeof.NVMeOFConnector, 'get_nvme_device_path') def test__connect_target_volume_not_connected( self, mock_device_path, mock_portals): mock_device_path.side_effect = exception.VolumeDeviceNotFound() - mock_portals.return_value = True + mock_portals.return_value = False self.assertRaises(exception.VolumeDeviceNotFound, self.connector._connect_target_volume, TARGET_NQN, VOL_UUID, [('fake', 'portal', 'tcp')]) - mock_device_path.assert_called_with( - self.connector, TARGET_NQN, VOL_UUID) - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_nvme_controllers') @mock.patch.object(nvmeof.NVMeOFConnector, 'connect_to_portals') def test__connect_target_volume_no_portals_con( self, mock_portals, mock_controller): @@ -530,21 +535,30 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): self.connector._connect_target_volume, 'fakenqn', 'fakeuuid', [fake_portal]) - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') - @mock.patch.object(nvmeof.NVMeOFConnector, 'connect_to_portals') @mock.patch.object(nvmeof.NVMeOFConnector, 'get_nvme_device_path') + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_live_nvme_controllers_map') + @mock.patch.object(nvmeof.NVMeOFConnector, 'connect_to_portals') + @mock.patch.object(nvmeof.NVMeOFConnector, 'rescan') def test__connect_target_volume_new_device_path( - self, mock_device_path, mock_connect_portal, mock_controller): - mock_controller.side_effect = exception.VolumeDeviceNotFound() + self, mock_rescan, mock_connect_portal, + mock_get_live_nvme_controllers_map, mock_device_path): mock_device_path.return_value = '/dev/nvme0n1' + mock_rescan.return_value = {} + mock_connect_portal.return_value = True + mock_get_live_nvme_controllers_map.return_value = fake_controllers_map self.assertEqual( self.connector._connect_target_volume( 'fakenqn', 'fakeuuid', [('fake', 'portal', 'tcp')]), '/dev/nvme0n1') + mock_rescan.assert_called_with(self.connector, 'fakenqn') mock_connect_portal.assert_called_with( - self.connector, 'fakenqn', [('fake', 'portal', 'tcp')]) + self.connector, 'fakenqn', [('fake', 'portal', 'tcp')], {}) + mock_get_live_nvme_controllers_map.assert_called_with(self.connector, + 'fakenqn') + fake_controllers_map_values = fake_controllers_map.values() mock_device_path.assert_called_with( - self.connector, 'fakenqn', 'fakeuuid') + self.connector, 'fakenqn', 'fakeuuid', + list(fake_controllers_map_values)) @mock.patch.object(nvmeof.NVMeOFConnector, 'run_nvme_cli') def test_connect_to_portals(self, mock_nvme_cli): @@ -553,7 +567,7 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): 'tcp', '-n', 'fakenqn', '-Q', '128', '-l', '-1') self.assertEqual( self.connector.connect_to_portals( - self.connector, 'fakenqn', [('10.0.0.1', 4420, 'tcp')]), + self.connector, 'fakenqn', [('10.0.0.1', 4420, 'tcp')], {}), True) mock_nvme_cli.assert_called_with(self.connector, nvme_command) @@ -565,7 +579,7 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): 'rdma', '-n', 'fakenqn', '-Q', '128', '-l', '-1') self.assertEqual( self.connector.connect_to_portals( - self.connector, 'fakenqn', [('10.0.0.1', 4420, 'RoCEv2')]), + self.connector, 'fakenqn', [('10.0.0.1', 4420, 'RoCEv2')], {}), False) mock_nvme_cli.assert_called_with(self.connector, nvme_command) @@ -735,25 +749,28 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): self.connector, ['mdadm', '--remove', '/dev/md/md1']) @mock.patch.object(nvmeof.NVMeOFConnector, 'run_nvme_cli') - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') - def test_rescan(self, mock_get_nvme_controller, mock_run_nvme_cli): - mock_get_nvme_controller.return_value = 'nvme1' + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_live_nvme_controllers_map') + def test_rescan(self, mock_get_live_nvme_controllers_map, + mock_run_nvme_cli): + mock_get_live_nvme_controllers_map.return_value = fake_controllers_map mock_run_nvme_cli.return_value = None - result = self.connector.rescan(EXECUTOR, TARGET_NQN, VOL_UUID) - self.assertIsNone(result) - mock_get_nvme_controller.assert_called_with(EXECUTOR, TARGET_NQN) + result = self.connector.rescan(EXECUTOR, TARGET_NQN) + self.assertEqual(fake_controllers_map, result) + mock_get_live_nvme_controllers_map.assert_called_with(EXECUTOR, + TARGET_NQN) nvme_command = ('ns-rescan', NVME_DEVICE_PATH) mock_run_nvme_cli.assert_called_with(EXECUTOR, nvme_command) @mock.patch.object(nvmeof.NVMeOFConnector, 'run_nvme_cli') - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') - def test_rescan_err(self, mock_get_nvme_controller, mock_run_nvme_cli): - mock_get_nvme_controller.return_value = 'nvme1' + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_live_nvme_controllers_map') + def test_rescan_err(self, mock_get_live_nvme_controllers_map, + mock_run_nvme_cli): + mock_get_live_nvme_controllers_map.return_value = fake_controllers_map mock_run_nvme_cli.side_effect = Exception() - self.assertRaises(exception.CommandExecutionFailed, - self.connector.rescan, EXECUTOR, TARGET_NQN, - VOL_UUID) - mock_get_nvme_controller.assert_called_with(EXECUTOR, TARGET_NQN) + result = self.connector.rescan(EXECUTOR, TARGET_NQN) + self.assertEqual(fake_controllers_map, result) + mock_get_live_nvme_controllers_map.assert_called_with(EXECUTOR, + TARGET_NQN) nvme_command = ('ns-rescan', NVME_DEVICE_PATH) mock_run_nvme_cli.assert_called_with(EXECUTOR, nvme_command) @@ -874,17 +891,17 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): @mock.patch.object(executor.Executor, '_execute') @mock.patch.object(glob, 'glob') - @mock.patch.object(nvmeof.NVMeOFConnector, '_get_nvme_controller') - def test_get_nvme_device_path(self, mock_get_nvme_controller, mock_glob, + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_nvme_controllers') + def test_get_nvme_device_path(self, mock_get_nvme_controllers, mock_glob, mock_execute): - mock_get_nvme_controller.return_value = 'nvme1' + mock_get_nvme_controllers.return_value = ['nvme1'] block_dev_path = '/sys/class/block/nvme1n*/uuid' - mock_glob.return_value = ['/sys/class/block/nvme1n1/uuid'] + mock_glob.side_effect = [['/sys/class/block/nvme1n1/uuid']] mock_execute.return_value = (VOL_UUID + "\n", "") cmd = ['cat', '/sys/class/block/nvme1n1/uuid'] result = self.connector.get_nvme_device_path(EXECUTOR, TARGET_NQN, VOL_UUID) - mock_get_nvme_controller.assert_called_with(EXECUTOR, TARGET_NQN) + mock_get_nvme_controllers.assert_called_with(EXECUTOR, TARGET_NQN) self.assertEqual(NVME_NS_PATH, result) mock_glob.assert_any_call(block_dev_path) args, kwargs = mock_execute.call_args @@ -909,29 +926,25 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): if 'state' in value: return 'live' + "\n", "" - @mock.patch.object(executor.Executor, '_execute', - side_effect=execute_side_effect) - @mock.patch.object(glob, 'glob') - def test_get_nvme_controller(self, mock_glob, mock_execute): - ctrl_path = '/sys/class/nvme-fabrics/ctl/nvme*' - mock_glob.side_effect = [['/sys/class/nvme-fabrics/ctl/nvme1']] - cmd = ['cat', '/sys/class/nvme-fabrics/ctl/nvme1/state'] - result = self.connector._get_nvme_controller(EXECUTOR, TARGET_NQN) - self.assertEqual('nvme1', result) - mock_glob.assert_any_call(ctrl_path) - args, kwargs = mock_execute.call_args - self.assertEqual(args[0], cmd[0]) - self.assertEqual(args[1], cmd[1]) + @mock.patch.object(nvmeof.NVMeOFConnector, 'get_live_nvme_controllers_map') + def test_get_nvme_controllers(self, mock_get_live_nvme_controllers_map): + mock_get_live_nvme_controllers_map.return_value = fake_controllers_map + result = self.connector.get_nvme_controllers(EXECUTOR, TARGET_NQN) + fake_controllers_map_values = fake_controllers_map.values() + self.assertEqual(list(fake_controllers_map_values)[0][1], + list(result)[0][1]) + mock_get_live_nvme_controllers_map.assert_called_with(EXECUTOR, + TARGET_NQN) @mock.patch.object(executor.Executor, '_execute', side_effect=execute_side_effect_not_live) @mock.patch.object(glob, 'glob') - def test_get_nvme_controller_not_live(self, mock_glob, mock_execute): + def test_get_nvme_controllers_not_live(self, mock_glob, mock_execute): ctrl_path = '/sys/class/nvme-fabrics/ctl/nvme*' mock_glob.side_effect = [['/sys/class/nvme-fabrics/ctl/nvme1']] cmd = ['cat', '/sys/class/nvme-fabrics/ctl/nvme1/state'] self.assertRaises(exception.VolumeDeviceNotFound, - self.connector._get_nvme_controller, EXECUTOR, + self.connector.get_nvme_controllers, EXECUTOR, TARGET_NQN) mock_glob.assert_any_call(ctrl_path) args, kwargs = mock_execute.call_args @@ -941,12 +954,12 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): @mock.patch.object(executor.Executor, '_execute', side_effect=execute_side_effect_not_found) @mock.patch.object(glob, 'glob') - def test_get_nvme_controller_not_found(self, mock_glob, mock_execute): + def test_get_nvme_controllers_not_found(self, mock_glob, mock_execute): ctrl_path = '/sys/class/nvme-fabrics/ctl/nvme*' mock_glob.side_effect = [['/sys/class/nvme-fabrics/ctl/nvme1']] cmd = ['cat', '/sys/class/nvme-fabrics/ctl/nvme1/state'] self.assertRaises(exception.VolumeDeviceNotFound, - self.connector._get_nvme_controller, EXECUTOR, + self.connector.get_nvme_controllers, EXECUTOR, TARGET_NQN) mock_glob.assert_any_call(ctrl_path) args, kwargs = mock_execute.call_args @@ -1086,6 +1099,7 @@ class NVMeOFConnectorTestCase(test_connector.ConnectorTestCase): self.assertFalse(result) def _get_host_nqn(self): + host_nqn = None try: with open('/etc/nvme/hostnqn', 'r') as f: host_nqn = f.read().strip() diff --git a/os_brick/tests/initiator/test_connector.py b/os_brick/tests/initiator/test_connector.py index 0b46b7233..2d7f7a433 100644 --- a/os_brick/tests/initiator/test_connector.py +++ b/os_brick/tests/initiator/test_connector.py @@ -42,6 +42,9 @@ class ZeroIntervalLoopingCall(loopingcall.FixedIntervalLoopingCall): class ConnectorUtilsTestCase(test_base.TestCase): + @mock.patch.object(nvmeof.NVMeOFConnector, + '_is_native_multipath_supported', + return_value=False) @mock.patch.object(nvmeof.NVMeOFConnector, '_get_system_uuid', return_value=None) @mock.patch.object(nvmeof.NVMeOFConnector, '_get_host_uuid', @@ -64,6 +67,7 @@ class ConnectorUtilsTestCase(test_base.TestCase): mock_nqn, mock_hostuuid, mock_sysuuid, + mock_native_multipath_supported, host='fakehost'): props_actual = connector.get_connector_properties('sudo', MY_IP, @@ -76,6 +80,7 @@ class ConnectorUtilsTestCase(test_base.TestCase): 'host': host, 'ip': MY_IP, 'multipath': multipath_result, + 'nvme_native_multipath': False, 'os_type': os_type, 'platform': platform, 'do_local_attach': False} diff --git a/releasenotes/notes/multipath-nvme-f77a53eb2717a44c.yaml b/releasenotes/notes/multipath-nvme-f77a53eb2717a44c.yaml new file mode 100644 index 000000000..daa7b94e4 --- /dev/null +++ b/releasenotes/notes/multipath-nvme-f77a53eb2717a44c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Phase 1 (Native) implementation of NVMeoF Multipathing. + See cinder-specs/specs/yoga/nvme-multipath