Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2026-06-10 16:15:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new.2375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Wed Jun 10 16:15:11 2026 rev:411 rq:1358521 version:5.1.0+20260610.968f62d8 Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2026-06-05 14:56:53.395844354 +0200 +++ /work/SRC/openSUSE:Factory/.crmsh.new.2375/crmsh.changes 2026-06-10 16:19:08.661342715 +0200 @@ -1,0 +2,17 @@ +Wed Jun 10 10:59:29 UTC 2026 - [email protected] + +- Update to version 5.1.0+20260610.968f62d8: + * Dev: utils: Add class MultipathInspector to inspect multipath devices + +------------------------------------------------------------------- +Mon Jun 08 09:29:07 UTC 2026 - [email protected] + +- Update to version 5.1.0+20260608.2b1f0461: + * Dev: ui_sbd: Call SBDUtils.verify_sbd_device in multiple places + * Dev: sbd: Introduce function sbd.SBDUtils.get_sbd_device_metadata_raw + * Dev: sbd: Compare SBD device UUID optionally + * Dev: sbd: Call SBDUtils.verify_sbd_device when doing sbd health check + * Dev: bootstrap: Call join_sbd before changing corosync.conf + * Fix: sbd: Verify sbd devices on all nodes + +------------------------------------------------------------------- Old: ---- crmsh-5.1.0+20260604.45e70fd3.tar.bz2 New: ---- crmsh-5.1.0+20260610.968f62d8.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.RNNZPE/_old 2026-06-10 16:19:11.253450134 +0200 +++ /var/tmp/diff_new_pack.RNNZPE/_new 2026-06-10 16:19:11.253450134 +0200 @@ -41,7 +41,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0-or-later Group: %{pkg_group} -Version: 5.1.0+20260604.45e70fd3 +Version: 5.1.0+20260610.968f62d8 Release: 0 URL: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.RNNZPE/_old 2026-06-10 16:19:11.701468700 +0200 +++ /var/tmp/diff_new_pack.RNNZPE/_new 2026-06-10 16:19:11.729469860 +0200 @@ -9,7 +9,7 @@ </service> <service name="tar_scm"> <param name="url">https://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">8e3ca73e5ebe9d8f3d58a654d5ce30f246380215</param> + <param name="changesrevision">b3a04e59685c33f4cb2f70929483e9157a39d557</param> </service> </servicedata> (No newline at EOF) ++++++ crmsh-5.1.0+20260604.45e70fd3.tar.bz2 -> crmsh-5.1.0+20260610.968f62d8.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/crmsh/bootstrap.py new/crmsh-5.1.0+20260610.968f62d8/crmsh/bootstrap.py --- old/crmsh-5.1.0+20260604.45e70fd3/crmsh/bootstrap.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/crmsh/bootstrap.py 2026-06-10 12:28:20.000000000 +0200 @@ -263,8 +263,6 @@ utils.fatal("-w option should be used with -s or -S option") if self.sbd_devices and self.diskless_sbd: utils.fatal("Can't use -s and -S options together") - if self.sbd_devices: - sbd.SBDUtils.verify_sbd_device(self.sbd_devices) with_sbd_option = self.sbd_devices or self.diskless_sbd @@ -273,7 +271,10 @@ if not utils.calculate_quorate_status(): utils.fatal("Cluster is not quorate, can't run 'sbd' stage") - for node in utils.list_cluster_nodes(): + node_list = utils.list_cluster_nodes() + if self.sbd_devices: + sbd.SBDUtils.verify_sbd_device(self.sbd_devices, node_list) + for node in node_list: if not utils.package_is_installed("sbd", node): utils.fatal(sbd.SBDManager.SBD_NOT_INSTALLED_MSG + f" on {node}") if self.sbd_devices and not utils.package_is_installed("fence-agents-sbd", node): @@ -287,8 +288,10 @@ elif with_sbd_option: if not utils.package_is_installed("sbd"): utils.fatal(sbd.SBDManager.SBD_NOT_INSTALLED_MSG) - if self.sbd_devices and not utils.package_is_installed("fence-agents-sbd"): - utils.fatal(sbd.SBDManager.FENCE_SBD_NOT_INSTALLED_MSG) + if self.sbd_devices: + if not utils.package_is_installed("fence-agents-sbd"): + utils.fatal(sbd.SBDManager.FENCE_SBD_NOT_INSTALLED_MSG) + sbd.SBDUtils.verify_sbd_device(self.sbd_devices) def _validate_nodes_option(self): """ @@ -2051,6 +2054,8 @@ if not os.path.exists(corosync.conf()): utils.fatal("{} is not readable. Please ensure that hostnames are resolvable.".format(corosync.conf())) + _context.sbd_manager.join_sbd(remote_user, seed_host) + ringXaddr_res = [] for i in range(link_number): while True: @@ -2072,8 +2077,6 @@ sync_path(corosync.conf(), seed_host) shell.get_stdout_or_raise_error('corosync-cfgtool -R', seed_host) - _context.sbd_manager.join_sbd(remote_user, seed_host) - # Initialize the cluster before adjusting quorum. This is so # that we can query the cluster to find out how many nodes # there are (so as not to adjust multiple times if a previous diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/crmsh/cluster_fs.py new/crmsh-5.1.0+20260610.968f62d8/crmsh/cluster_fs.py --- old/crmsh-5.1.0+20260604.45e70fd3/crmsh/cluster_fs.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/crmsh/cluster_fs.py 2026-06-10 12:28:20.000000000 +0200 @@ -120,13 +120,16 @@ """ Verify OCFS2/GFS2 devices """ + node_list = utils.list_cluster_nodes() if self.use_stage else [utils.this_node()] for dev in self.devices: - if not utils.is_block_device(dev): - raise Error(f"{dev} doesn't look like a block device") + failed_nodes = utils.get_non_block_device_nodes(dev, node_list) + if failed_nodes: + raise Error(f"{dev} is not a block device on {', '.join(failed_nodes)}") if utils.is_dev_used_for_lvm(dev) and self.use_cluster_lvm2: raise Error(f"{dev} is a Logical Volume, cannot be used with the -C option") if utils.has_disk_mounted(dev): raise Error(f"{dev} is already mounted") + utils.MultipathInspector.check_device_under_multipath(dev) def _check_if_already_configured(self): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/crmsh/sbd.py new/crmsh-5.1.0+20260610.968f62d8/crmsh/sbd.py --- old/crmsh-5.1.0+20260604.45e70fd3/crmsh/sbd.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/crmsh/sbd.py 2026-06-10 12:28:20.000000000 +0200 @@ -25,18 +25,25 @@ Consolidate sbd related utility methods ''' @staticmethod + def get_sbd_device_metadata_raw(dev, remote=None) -> str: + ''' + Get raw metadata info from sbd device header + Raise ValueError if the command fails or the output is empty + ''' + cmd = f"sbd -d {shlex.quote(dev)} dump" + out = sh.cluster_shell().get_stdout_or_raise_error(cmd, remote) + if not out: # might not possible, but just in case to avoid further parsing error + raise ValueError(f"Cannot get metadata from SBD device {dev} on {remote or utils.this_node()}") + return out + + @staticmethod def get_sbd_device_metadata(dev, timeout_only=False, remote=None) -> dict: ''' Extract metadata from sbd device header ''' sbd_info = {} - - cmd = f"sbd -d {shlex.quote(dev)} dump" - rc, out, _ = sh.cluster_shell().get_rc_stdout_stderr_without_input(remote, cmd) - if rc != 0 or not out: - return sbd_info - pattern = r"UUID\s+:\s+(\S+)|Timeout\s+\((\w+)\)\s+:\s+(\d+)" + out = SBDUtils.get_sbd_device_metadata_raw(dev, remote) matches = re.findall(pattern, out) for uuid, timeout_type, timeout_value in matches: if uuid and not timeout_only: @@ -70,6 +77,8 @@ if not local_uuid: raise ValueError(f"Cannot get sbd device UUID for {dev} on {utils.this_node()}") for node in node_list: + if node == utils.this_node(): + continue remote_uuid = SBDUtils.get_device_uuid(dev, node) if not remote_uuid: raise ValueError(f"Cannot get sbd device UUID for {dev} on {node}") @@ -77,13 +86,20 @@ raise ValueError(f"Device {dev} doesn't have the same UUID with {node}") @staticmethod - def verify_sbd_device(dev_list, compare_node_list=None): + def verify_sbd_device(dev_list, node_list=None, compare_uuid=False): if len(dev_list) > SBDManager.SBD_DEVICE_MAX: raise ValueError(f"Maximum number of SBD device is {SBDManager.SBD_DEVICE_MAX}") + for dev in dev_list: - if not utils.is_block_device(dev): - raise ValueError(f"{dev} doesn't look like a block device") - SBDUtils.compare_device_uuid(dev, compare_node_list) + failed_nodes = utils.get_non_block_device_nodes(dev, node_list) + if failed_nodes: + raise ValueError(f"{dev} is not a block device on {', '.join(failed_nodes)}") + + utils.MultipathInspector.check_device_under_multipath(dev) + + if compare_uuid: + SBDUtils.compare_device_uuid(dev, node_list) + utils.detect_duplicate_device_path(dev_list) @staticmethod @@ -691,6 +707,14 @@ if not self._check_config_consistency(error_msg): raise FixAborted("All other checks aborted due to inconsistent configurations") + dev_list = SBDUtils.get_sbd_device_from_config() + if dev_list: + try: + nodes_to_check = [utils.this_node()] + (self.peer_node_list or []) + SBDUtils.verify_sbd_device(dev_list, nodes_to_check, compare_uuid=True) + except ValueError as e: + raise FixAborted(f"SBD device verification failed: {e}") + self._load_configurations_from_runtime() self._get_current_terms() @@ -1309,6 +1333,7 @@ def _wants_to_overwrite(self, configured_devices): wants_to_overwrite_msg = f"SBD_DEVICE in {self.SYSCONFIG_SBD} is already configured to use '{';'.join(configured_devices)}' - overwrite?" if not bootstrap.confirm(wants_to_overwrite_msg): + SBDUtils.verify_sbd_device(configured_devices) if not SBDUtils.check_devices_metadata_consistent(configured_devices): raise utils.TerminateSubCommand self.overwrite_sysconfig = False @@ -1462,7 +1487,7 @@ self._watchdog_inst.join_watchdog() if dev_list: - SBDUtils.verify_sbd_device(dev_list, compare_node_list=[peer_host]) + SBDUtils.verify_sbd_device(dev_list, [utils.this_node(), peer_host], compare_uuid=True) logger.info("Got {}SBD configuration".format("" if dev_list else "diskless ")) self.enable_sbd_service() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/crmsh/ui_sbd.py new/crmsh-5.1.0+20260610.968f62d8/crmsh/ui_sbd.py --- old/crmsh-5.1.0+20260604.45e70fd3/crmsh/ui_sbd.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/crmsh/ui_sbd.py 2026-06-10 12:28:20.000000000 +0200 @@ -215,8 +215,7 @@ if self.device_list_from_config: logger.info("crm sbd configure show disk_metadata") for dev in self.device_list_from_config: - print(self.cluster_shell.get_stdout_or_raise_error(f"sbd -d {dev} dump")) - print() + print(sbd.SBDUtils.get_sbd_device_metadata_raw(dev)) def _show_property(self) -> None: ''' @@ -384,6 +383,8 @@ ''' Configure disk-based SBD based on input parameters and runtime config ''' + sbd.SBDUtils.verify_sbd_device(self.device_list_from_config, self.cluster_nodes) + update_dict = {} watchdog_device = parameter_dict.get("watchdog-device") if watchdog_device != self.watchdog_device_from_config: @@ -407,8 +408,7 @@ result_dict = self._set_sbd_opts(crashdump_watchdog_timeout) update_dict = {**update_dict, **result_dict} - device_list = sbd.SBDUtils.get_sbd_device_from_config() - devices_consistent = sbd.SBDUtils.check_devices_metadata_consistent(device_list, quiet=True) + devices_consistent = sbd.SBDUtils.check_devices_metadata_consistent(self.device_list_from_config, quiet=True) if timeout_dict == self.device_meta_dict_runtime and not update_dict and devices_consistent: logger.info("No change in SBD configuration") return @@ -467,7 +467,7 @@ Implement sbd device add command, add devices to sbd configuration ''' all_device_list = self.device_list_from_config + devices_to_add - sbd.SBDUtils.verify_sbd_device(all_device_list) + sbd.SBDUtils.verify_sbd_device(all_device_list, self.cluster_nodes) devices_to_init, _ = sbd.SBDUtils.handle_input_sbd_devices( devices_to_add, @@ -487,6 +487,8 @@ ''' Implement sbd device remove command, remove devices from sbd configuration ''' + sbd.SBDUtils.verify_sbd_device(self.device_list_from_config, self.cluster_nodes) + for dev in devices_to_remove: if dev not in self.device_list_from_config: raise self.SyntaxError(f"Device {dev} is not in config") @@ -613,6 +615,8 @@ self._load_attributes() if not self._service_is_active(constants.SBD_SERVICE): return False + if self.device_list_from_config: + sbd.SBDUtils.verify_sbd_device(self.device_list_from_config, self.cluster_nodes) args = _handle_force_option(args) purge_crashdump = False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/crmsh/utils.py new/crmsh-5.1.0+20260610.968f62d8/crmsh/utils.py --- old/crmsh-5.1.0+20260604.45e70fd3/crmsh/utils.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/crmsh/utils.py 2026-06-10 12:28:20.000000000 +0200 @@ -27,6 +27,7 @@ import json import socket import selectors +import shlex from pathlib import Path from collections import defaultdict from contextlib import contextmanager, closing @@ -2622,15 +2623,21 @@ return [x for x in re.split(reg, string) if x] -def is_block_device(dev): +def get_non_block_device_nodes(dev, node_list=None) -> list[str]: """ - Check if dev is a block device + Return a list of nodes where the device is not a block device or does not exist """ - try: - rc = S_ISBLK(os.stat(dev).st_mode) - except OSError: - return False - return rc + shell = sh.cluster_shell() + cluster_nodes = node_list or [this_node()] + failed_nodes = [] + for node in cluster_nodes: + rc, _, _ = shell.get_rc_stdout_stderr_without_input( + node, + f"test -b {shlex.quote(dev)}" + ) + if rc != 0: + failed_nodes.append(node) + return failed_nodes def detect_duplicate_device_path(device_list: typing.List[str]): @@ -3625,4 +3632,58 @@ if not self._resolve_res: return False return not self._resolve_res.using_deprecated + + +@dataclass(frozen=True) +class DeviceInfo: + device: str + parent_device: str|None + under_multipath: bool + + +class MultipathInspector: + def __init__(self, dev): + self._shell = sh.cluster_shell() + self._device_info = self._inspect(dev) + + def _get_parent_device(self, dev) -> str: + resolved = Path(dev).resolve() + cmd = f"lsblk -dn -o PKNAME {shlex.quote(str(resolved))}" + _, out, _ = self._shell.get_rc_stdout_stderr_without_input(None, cmd) + return out or resolved.name + + def _get_multipath_mapping(self) -> dict[str, str]: + cmd = "multipathd show paths format \"%d %m\"" + rc, out, _ = self._shell.get_rc_stdout_stderr_without_input(None, cmd) + mapping = dict() + if rc != 0: + return mapping + for line in out.splitlines(): + parts = line.split() + if len(parts) < 2: + continue + dev_name, map_name = parts[0], parts[1] + if (dev_name, map_name) == ("dev", "multipath"): + continue + mapping[dev_name] = map_name + return mapping + + def _inspect(self, dev: str) -> DeviceInfo: + parent = self._get_parent_device(dev) + mapping = self._get_multipath_mapping() + return DeviceInfo( + device=dev, + parent_device=parent, + under_multipath=parent in mapping + ) + + def _is_under_multipath(self) -> bool: + return self._device_info.under_multipath + + @classmethod + def check_device_under_multipath(cls, dev): + inspector = cls(dev) + if inspector._is_under_multipath(): + error_msg = f"Device {dev} is under multipath, please provide the multipath device instead" + raise ValueError(error_msg) # vim:ts=4:sw=4:et: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/test/features/bootstrap_sbd_normal.feature new/crmsh-5.1.0+20260610.968f62d8/test/features/bootstrap_sbd_normal.feature --- old/crmsh-5.1.0+20260604.45e70fd3/test/features/bootstrap_sbd_normal.feature 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/test/features/bootstrap_sbd_normal.feature 2026-06-10 12:28:20.000000000 +0200 @@ -8,7 +8,7 @@ When Try "crm cluster init -s "/dev/sda1;/dev/sda2;/dev/sda3;/dev/sda4" -y" Then Except "ERROR: cluster.init: Maximum number of SBD device is 3" When Try "crm cluster init -s "/dev/sda1;/dev/sdaxxxx" -y" - Then Except "ERROR: cluster.init: /dev/sdaxxxx doesn't look like a block device" + Then Expected "/dev/sdaxxxx is not a block device on" in stderr When Try "crm cluster init -s "/dev/sda1;/dev/sda1" -y" Then Expected multiple lines in stderr """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_cluster_fs.py new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_cluster_fs.py --- old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_cluster_fs.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_cluster_fs.py 2026-06-10 12:28:20.000000000 +0200 @@ -88,17 +88,19 @@ self.gfs2_instance_one_device_with_mount_point._verify_options() self.assertIn("Mount point /mnt/gfs2 already mounted", str(context.exception)) - @mock.patch("crmsh.utils.is_block_device") - def test_verify_devices_not_block_device(self, mock_is_block_device): - mock_is_block_device.return_value = False + @mock.patch("crmsh.utils.list_cluster_nodes") + @mock.patch("crmsh.utils.get_non_block_device_nodes") + def test_verify_devices_not_block_device(self, mock_get_non_block_device_nodes, mock_list_cluster_nodes): + mock_list_cluster_nodes.return_value = ["node1", "node2"] + mock_get_non_block_device_nodes.return_value = ["node1"] with self.assertRaises(cluster_fs.Error) as context: self.ocfs2_instance_one_device._verify_devices() - self.assertIn("/dev/sda1 doesn't look like a block device", str(context.exception)) + self.assertIn("/dev/sda1 is not a block device on node1", str(context.exception)) @mock.patch("crmsh.utils.is_dev_used_for_lvm") - @mock.patch("crmsh.utils.is_block_device") - def test_verify_devices_clvm2_with_lv(self, mock_is_block_device, mock_is_dev_used_for_lvm): - mock_is_block_device.return_value = True + @mock.patch("crmsh.utils.get_non_block_device_nodes") + def test_verify_devices_clvm2_with_lv(self, mock_get_non_block_device_nodes, mock_is_dev_used_for_lvm): + mock_get_non_block_device_nodes.return_value = [] mock_is_dev_used_for_lvm.return_value = True with self.assertRaises(cluster_fs.Error) as context: self.gfs2_instance_one_device_clvm2._verify_devices() @@ -106,9 +108,9 @@ @mock.patch("crmsh.utils.has_disk_mounted") @mock.patch("crmsh.utils.is_dev_used_for_lvm") - @mock.patch("crmsh.utils.is_block_device") - def test_verify_devices_already_mounted(self, mock_is_block_device, mock_is_dev_used_for_lvm, mock_has_disk_mounted): - mock_is_block_device.return_value = True + @mock.patch("crmsh.utils.get_non_block_device_nodes") + def test_verify_devices_already_mounted(self, mock_get_non_block_device_nodes, mock_is_dev_used_for_lvm, mock_has_disk_mounted): + mock_get_non_block_device_nodes.return_value = [] mock_is_dev_used_for_lvm.return_value = False mock_has_disk_mounted.return_value = True with self.assertRaises(cluster_fs.Error) as context: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_sbd.py new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_sbd.py --- old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_sbd.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_sbd.py 2026-06-10 12:28:20.000000000 +0200 @@ -15,25 +15,27 @@ Timeout (msgwait) : 10 """ - @patch('crmsh.sh.cluster_shell') - def test_get_sbd_device_metadata_success(self, mock_cluster_shell): - mock_cluster_shell.return_value.get_rc_stdout_stderr_without_input.return_value = (0, self.TEST_DATA, None) + @patch('crmsh.sbd.SBDUtils.get_sbd_device_metadata_raw') + def test_get_sbd_device_metadata_success(self, mock_get_sbd_device_metadata_raw): + mock_get_sbd_device_metadata_raw.return_value = self.TEST_DATA result = SBDUtils.get_sbd_device_metadata("/dev/sbd_device") expected = {'uuid': '1234-5678', 'watchdog': 5, 'msgwait': 10} self.assertEqual(result, expected) - @patch('crmsh.sh.cluster_shell') - def test_get_sbd_device_metadata_timeout_only(self, mock_cluster_shell): - mock_cluster_shell.return_value.get_rc_stdout_stderr_without_input.return_value = (0, self.TEST_DATA, None) + @patch('crmsh.sbd.SBDUtils.get_sbd_device_metadata_raw') + def test_get_sbd_device_metadata_timeout_only(self, mock_get_sbd_device_metadata_raw): + mock_get_sbd_device_metadata_raw.return_value = self.TEST_DATA result = SBDUtils.get_sbd_device_metadata("/dev/sbd_device", timeout_only=True) expected = {'watchdog': 5, 'msgwait': 10} self.assertNotIn('uuid', result) self.assertEqual(result, expected) @patch('crmsh.sh.cluster_shell') - def test_get_sbd_device_metadata_failure(self, mock_cluster_shell): - mock_cluster_shell.return_value.get_rc_stdout_stderr_without_input.return_value = (1, None, None) - self.assertEqual(SBDUtils.get_sbd_device_metadata("/dev/sbd_device"), {}) + def test_get_sbd_device_metadata_raw_failure(self, mock_cluster_shell): + mock_cluster_shell.return_value.get_stdout_or_raise_error.side_effect = ValueError("Command failed") + with self.assertRaises(ValueError): + SBDUtils.get_sbd_device_metadata_raw("/dev/sbd_device") + mock_cluster_shell.return_value.get_stdout_or_raise_error.assert_called_once_with("sbd -d /dev/sbd_device dump", None) @patch('crmsh.sbd.SBDUtils.get_sbd_device_metadata') def test_get_device_uuid_success(self, mock_get_sbd_device_metadata): @@ -57,24 +59,26 @@ with self.assertRaises(ValueError): SBDUtils.compare_device_uuid("/dev/sbd_device", ["node1"]) - @patch('crmsh.utils.is_block_device') + @patch('crmsh.utils.get_non_block_device_nodes') @patch('crmsh.sbd.SBDUtils.compare_device_uuid') - def test_verify_sbd_device_exceeds_max(self, mock_compare_device_uuid, mock_is_block_device): + def test_verify_sbd_device_exceeds_max(self, mock_compare_device_uuid, mock_get_non_block_device_nodes): dev_list = [f"/dev/sbd_device_{i}" for i in range(SBDManager.SBD_DEVICE_MAX + 1)] - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as context: SBDUtils.verify_sbd_device(dev_list) + self.assertTrue(f"Maximum number of SBD device is {SBDManager.SBD_DEVICE_MAX}" in str(context.exception)) - @patch('crmsh.utils.is_block_device') + @patch('crmsh.utils.get_non_block_device_nodes') @patch('crmsh.sbd.SBDUtils.compare_device_uuid') - def test_verify_sbd_device_non_block(self, mock_compare_device_uuid, mock_is_block_device): - mock_is_block_device.return_value = False - with self.assertRaises(ValueError): + def test_verify_sbd_device_non_block(self, mock_compare_device_uuid, mock_get_non_block_device_nodes): + mock_get_non_block_device_nodes.return_value = ["node1"] + with self.assertRaises(ValueError) as context: SBDUtils.verify_sbd_device(["/dev/not_a_block_device"]) + self.assertTrue(f"/dev/not_a_block_device is not a block device on node1" in str(context.exception)) - @patch('crmsh.utils.is_block_device') + @patch('crmsh.utils.get_non_block_device_nodes') @patch('crmsh.sbd.SBDUtils.compare_device_uuid') - def test_verify_sbd_device_valid(self, mock_compare_device_uuid, mock_is_block_device): - mock_is_block_device.return_value = True + def test_verify_sbd_device_valid(self, mock_compare_device_uuid, mock_get_non_block_device_nodes): + mock_get_non_block_device_nodes.return_value = [] SBDUtils.verify_sbd_device(["/dev/sbd_device"], ["node1", "node2"]) @patch('crmsh.utils.parse_sysconfig') @@ -958,10 +962,11 @@ sbdmanager_instance._wants_to_overwrite.assert_not_called() sbdmanager_instance._prompt_for_sbd_device.assert_called_once() + @patch('crmsh.sbd.SBDUtils.verify_sbd_device') @patch('crmsh.sbd.SBDUtils.check_devices_metadata_consistent') @patch('crmsh.bootstrap.confirm') @patch('crmsh.sbd.ServiceManager') - def test_wants_to_overwrite_exception(self, mock_ServiceManager, mock_confirm, mock_check_devices_metadata_consistent): + def test_wants_to_overwrite_exception(self, mock_ServiceManager, mock_confirm, mock_check_devices_metadata_consistent, mock_verify_sbd_device): sbdmanager_instance = SBDManager() mock_confirm.return_value = False mock_check_devices_metadata_consistent.return_value = False @@ -984,10 +989,11 @@ sbd.SBDManager.warn_diskless_sbd() mock_logger_warning.assert_called_once_with('%s', SBDManager.DISKLESS_SBD_WARNING) + @patch('crmsh.sbd.SBDUtils.verify_sbd_device') @patch('crmsh.sbd.SBDUtils.check_devices_metadata_consistent') @patch('crmsh.bootstrap.confirm') @patch('crmsh.sbd.ServiceManager') - def test_wants_to_overwrite_return_false(self, mock_ServiceManager, mock_confirm, mock_check_devices_metadata_consistent): + def test_wants_to_overwrite_return_false(self, mock_ServiceManager, mock_confirm, mock_check_devices_metadata_consistent, mock_verify_sbd_device): sbdmanager_instance = SBDManager() mock_confirm.return_value = False mock_check_devices_metadata_consistent.return_value = True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_ui_sbd.py new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_ui_sbd.py --- old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_ui_sbd.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_ui_sbd.py 2026-06-10 12:28:20.000000000 +0200 @@ -181,15 +181,15 @@ mock_logger_info.assert_called_with("crm sbd configure show sysconfig") self.assertEqual(mock_stdout.getvalue(), "KEY1=value1\nKEY2=value2\n") + @mock.patch('crmsh.sbd.SBDUtils.get_sbd_device_metadata_raw') @mock.patch('logging.Logger.info') - def test_show_disk_metadata(self, mock_logger_info): - self.sbd_instance_diskbased.cluster_shell.get_stdout_or_raise_error.return_value = "disk metadata: data" + def test_show_disk_metadata(self, mock_logger_info, mock_get_sbd_device_metadata_raw): + mock_get_sbd_device_metadata_raw.return_value = "disk metadata: data" with mock.patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: self.sbd_instance_diskbased._show_disk_metadata() self.assertTrue(mock_logger_info.called) mock_logger_info.assert_called_with("crm sbd configure show disk_metadata") - self.assertEqual(mock_stdout.getvalue(), "disk metadata: data\n\n") - self.sbd_instance_diskbased.cluster_shell.get_stdout_or_raise_error.assert_called_with("sbd -d /dev/sda1 dump") + self.assertEqual(mock_stdout.getvalue(), "disk metadata: data\n") def test_do_configure_no_service(self): self.sbd_instance_diskbased._load_attributes = mock.Mock() @@ -361,9 +361,10 @@ mock_logger_error.assert_called_once_with('%s', "No argument") mock_print.assert_called_once_with("usage data") + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') @mock.patch('logging.Logger.info') @mock.patch('crmsh.sbd.SBDManager') - def test_configure_diskbase(self, mock_SBDManager, mock_logger_info): + def test_configure_diskbase(self, mock_SBDManager, mock_logger_info, mock_verify_sbd_device): parameter_dict = {"watchdog": 12, "watchdog-device": "/dev/watchdog100", "crashdump-watchdog": 12} self.sbd_instance_diskbased._should_configure_crashdump = mock.Mock(return_value=True) self.sbd_instance_diskbased._check_kdump_service = mock.Mock() @@ -381,9 +382,10 @@ self.sbd_instance_diskbased._check_kdump_service.assert_called_once() self.sbd_instance_diskbased._set_sbd_opts.assert_called_once() + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') @mock.patch('logging.Logger.info') @mock.patch('crmsh.sbd.SBDManager') - def test_configure_diskbase_no_change(self, mock_SBDManager, mock_logger_info): + def test_configure_diskbase_no_change(self, mock_SBDManager, mock_logger_info, mock_verify_sbd_device): parameter_dict = {"msgwait": 20, "watchdog": 10, "watchdog-device": "/dev/watchdog0"} self.sbd_instance_diskbased._should_configure_crashdump = mock.Mock(return_value=False) self.sbd_instance_diskbased._configure_diskbase(parameter_dict) @@ -423,7 +425,7 @@ mock_handle_input_sbd_devices.return_value = [["/dev/sda2"], ["/dev/sda1"]] mock_SBDManager.return_value.init_and_deploy_sbd = mock.Mock() self.sbd_instance_diskbased._device_add(["/dev/sda2"]) - mock_verify_sbd_device.assert_called_once_with(["/dev/sda1", "/dev/sda2"]) + mock_verify_sbd_device.assert_called_once_with(["/dev/sda1", "/dev/sda2"], self.sbd_instance_diskbased.cluster_nodes) mock_handle_input_sbd_devices.assert_called_once_with(["/dev/sda2"], ["/dev/sda1"]) mock_SBDManager.assert_called_once_with( device_list_to_init=["/dev/sda2"], @@ -432,22 +434,25 @@ ) mock_logger_info.assert_called_once_with("Append devices: %s", "/dev/sda2") - def test_device_remove_dev_not_in_config(self): + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') + def test_device_remove_dev_not_in_config(self, mock_verify_sbd_device): with self.assertRaises(ui_sbd.SBD.SyntaxError) as e: self.sbd_instance_diskbased._device_remove(["/dev/sda2"]) self.assertEqual(str(e.exception), "Device /dev/sda2 is not in config") - def test_device_remove_last_dev(self): + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') + def test_device_remove_last_dev(self, mock_verify_sbd_device): with self.assertRaises(ui_sbd.SBD.SyntaxError) as e: self.sbd_instance_diskbased._device_remove(["/dev/sda1"]) self.assertEqual(str(e.exception), "Not allowed to remove all devices") + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') @mock.patch('crmsh.utils.able_to_restart_cluster') @mock.patch('crmsh.utils.leverage_maintenance_mode') @mock.patch('crmsh.bootstrap.restart_cluster') @mock.patch('crmsh.sbd.SBDManager.update_sbd_configuration') @mock.patch('logging.Logger.info') - def test_device_remove(self, mock_logger_info, mock_update_sbd_configuration, mock_restart_cluster, mock_leverage_maintenance_mode, mock_able_to_restart_cluster): + def test_device_remove(self, mock_logger_info, mock_update_sbd_configuration, mock_restart_cluster, mock_leverage_maintenance_mode, mock_able_to_restart_cluster, mock_verify_sbd_device): enable_value = True cm = mock.Mock() cm.__enter__ = mock.Mock(return_value=enable_value) @@ -552,8 +557,9 @@ self.assertFalse(res) mock_purge_sbd_from_cluster.assert_not_called() + @mock.patch('crmsh.sbd.SBDUtils.verify_sbd_device') @mock.patch('crmsh.utils.check_all_nodes_reachable') - def test_do_purge(self, mock_check_all_nodes_reachable): + def test_do_purge(self, mock_check_all_nodes_reachable, mock_verify_sbd_device): self.sbd_instance_diskbased._load_attributes = mock.Mock() self.sbd_instance_diskbased._service_is_active = mock.Mock(return_value=True) self.sbd_instance_diskbased._purge_sbd = mock.Mock() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_utils.py new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_utils.py --- old/crmsh-5.1.0+20260604.45e70fd3/test/unittests/test_utils.py 2026-06-04 04:20:38.000000000 +0200 +++ new/crmsh-5.1.0+20260610.968f62d8/test/unittests/test_utils.py 2026-06-10 12:28:20.000000000 +0200 @@ -939,28 +939,14 @@ mock_diskless.assert_called_once_with() [email protected]('crmsh.utils.S_ISBLK') [email protected]('os.stat') -def test_is_block_device_error(mock_stat, mock_isblk): - mock_stat_inst = mock.Mock(st_mode=12345) - mock_stat.return_value = mock_stat_inst - mock_isblk.side_effect = OSError - res = utils.is_block_device("/dev/sda1") - assert res is False - mock_stat.assert_called_once_with("/dev/sda1") - mock_isblk.assert_called_once_with(12345) - - [email protected]('crmsh.utils.S_ISBLK') [email protected]('os.stat') -def test_is_block_device(mock_stat, mock_isblk): - mock_stat_inst = mock.Mock(st_mode=12345) - mock_stat.return_value = mock_stat_inst - mock_isblk.return_value = True - res = utils.is_block_device("/dev/sda1") - assert res is True - mock_stat.assert_called_once_with("/dev/sda1") - mock_isblk.assert_called_once_with(12345) [email protected]('crmsh.sh.cluster_shell') +def test_get_non_block_device_nodes(mock_cluster_shell): + mock_cluster_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_cluster_shell_inst + mock_cluster_shell_inst.get_rc_stdout_stderr_without_input.return_value = (1, None, None) + res = utils.get_non_block_device_nodes("/dev/sda1", ["node1"]) + assert res == ["node1"] + mock_cluster_shell_inst.get_rc_stdout_stderr_without_input.assert_called_once_with("node1", "test -b /dev/sda1") @mock.patch('crmsh.utils.ssh_port_reachable_check') @@ -1492,3 +1478,105 @@ assert res == ["node2"] mock_error.assert_called_once_with("From the view of node '%s', node '%s' is not a member of the cluster", 'node1', 'node2') + + +class TestMultipathInspector(unittest.TestCase): + + @mock.patch('crmsh.sh.cluster_shell') + def test_init(self, mock_cluster_shell): + """Test MultipathInspector initialization""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), # lsblk output for parent device + (0, "dev multipath\nsda mpatha", "") # multipathd show paths output + ] + + inspector = utils.MultipathInspector("/dev/sda1") + + assert inspector._shell == mock_shell_inst + assert inspector._device_info.device == "/dev/sda1" + assert inspector._device_info.parent_device == "sda" + assert inspector._device_info.under_multipath is True + + @mock.patch('crmsh.sh.cluster_shell') + def test_get_parent_device(self, mock_cluster_shell): + """Test _get_parent_device method""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), # lsblk output for __init__ + (0, "", ""), # multipathd show paths output for __init__ + (0, "sda", "") # lsblk output for test call + ] + + inspector = utils.MultipathInspector("/dev/sda1") + parent = inspector._get_parent_device("/dev/sda1") + + assert parent == "sda" + + @mock.patch('crmsh.sh.cluster_shell') + def test_get_multipath_mapping(self, mock_cluster_shell): + """Test _get_multipath_mapping method with valid output""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + multipathd_output = """dev multipath +sda mpatha +sdb mpatha +sdc mpathb""" + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), # lsblk for __init__ + (0, multipathd_output, ""), # multipathd for __init__ + (0, multipathd_output, "") # multipathd for test call + ] + + inspector = utils.MultipathInspector("/dev/sda1") + mapping = inspector._get_multipath_mapping() + + assert mapping == {"sda": "mpatha", "sdb": "mpatha", "sdc": "mpathb"} + + @mock.patch('crmsh.sh.cluster_shell') + def test_inspect_device_under_multipath(self, mock_cluster_shell): + """Test _inspect method when device is under multipath""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), + (0, "dev multipath\nsda mpatha", "") + ] + + inspector = utils.MultipathInspector("/dev/sda1") + device_info = inspector._device_info + + assert device_info.device == "/dev/sda1" + assert device_info.parent_device == "sda" + assert device_info.under_multipath is True + + @mock.patch('crmsh.sh.cluster_shell') + def test_is_under_multipath_true(self, mock_cluster_shell): + """Test _is_under_multipath returns True""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), + (0, "dev multipath\nsda mpatha", "") + ] + + inspector = utils.MultipathInspector("/dev/sda1") + + assert inspector._is_under_multipath() is True + + @mock.patch('crmsh.sh.cluster_shell') + def test_check_device_under_multipath_raises_error(self, mock_cluster_shell): + """Test check_device_under_multipath raises ValueError when device is under multipath""" + mock_shell_inst = mock.Mock() + mock_cluster_shell.return_value = mock_shell_inst + mock_shell_inst.get_rc_stdout_stderr_without_input.side_effect = [ + (0, "sda", ""), + (0, "dev multipath\nsda mpatha", "") + ] + + with pytest.raises(ValueError) as exc_info: + utils.MultipathInspector.check_device_under_multipath("/dev/sda1") + + assert str(exc_info.value) == "Device /dev/sda1 is under multipath, please provide the multipath device instead"
