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"

Reply via email to