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-03-11 20:55:56
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Wed Mar 11 20:55:56 2026 rev:398 rq:1338205 version:5.0.0+20260309.5a3c6578

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2026-02-26 
18:59:43.649240492 +0100
+++ /work/SRC/openSUSE:Factory/.crmsh.new.8177/crmsh.changes    2026-03-11 
20:57:30.947358707 +0100
@@ -1,0 +2,39 @@
+Tue Mar 10 07:01:35 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260309.5a3c6578:
+  * Dev: unittests: Adjust unit test for previous commit
+  * Dev: doc: Mention about watchdog-device option also acceptes driver name
+  * Dev: watchdog: Improve the fatal error logging message
+  * Dev: behave: Adjust functional test for previous commit
+  * Dev: ui_cluster: Hint the watchdog option should be used with sbd option
+
+-------------------------------------------------------------------
+Mon Mar  9 08:57:41 UTC 2026 - Nicholas Yang <[email protected]>
+
+- Remove unused crmsh.tmpfiles.d.conf
+
+-------------------------------------------------------------------
+Wed Mar 04 04:50:20 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260304.1d483274:
+  * Apply suggestion from @Copilot
+  * Apply suggestion from @Copilot
+  * Dev: spec: create dirs in /var with tmpfiles.d (jsc#PED-14865)
+
+-------------------------------------------------------------------
+Tue Mar 03 04:41:44 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260303.386d7066:
+  * Dev: behave: Adjust functional test for previous commits
+  * Dev: unittests: Adjust unit test for previous commit
+  * Dev: qdevice: Leverage maintenance mode while adding and removing qdevice
+  * Dev: qdevice: Configure the QDevice statically
+  * Dev: qdevice: Remove duplicated code for checking qdevice installation
+
+-------------------------------------------------------------------
+Fri Feb 27 08:14:34 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260227.3fb70725:
+  * Fix: fix asyncio usage with python 3.14 and later
+
+-------------------------------------------------------------------

Old:
----
  crmsh-5.0.0+20260226.8b99a4c5.tar.bz2
  crmsh.tmpfiles.d.conf

New:
----
  crmsh-5.0.0+20260309.5a3c6578.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.DkCvSy/_old  2026-03-11 20:57:32.179409537 +0100
+++ /var/tmp/diff_new_pack.DkCvSy/_new  2026-03-11 20:57:32.183409702 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package crmsh
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -41,11 +41,10 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        5.0.0+20260226.8b99a4c5
+Version:        5.0.0+20260309.5a3c6578
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2
-Source1:        %{name}.tmpfiles.d.conf
 
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
 %if 0%{?suse_version}
@@ -60,11 +59,11 @@
 Requires:       python3-lxml
 Requires:       python3-packaging
 Recommends:     bash-completion
+BuildRequires:  python3-PyYAML
 BuildRequires:  python3-lxml
-BuildRequires:  python3-setuptools
 BuildRequires:  python3-pip
+BuildRequires:  python3-setuptools
 BuildRequires:  python3-wheel
-BuildRequires:  python3-PyYAML
 
 %if 0%{?suse_version}
 # only require csync2 on SUSE since bootstrap
@@ -85,9 +84,8 @@
 %else
 Requires:       python3-dateutil
 BuildRequires:  pyproject-rpm-macros
-BuildRequires:  python3-devel
-BuildRequires:  python3-setuptools
 BuildRequires:  python3-dateutil
+BuildRequires:  python3-devel
 %endif
 
 # Required for core functionality
@@ -252,8 +250,9 @@
 
 %config %{_sysconfdir}/crm
 
-%dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
-%dir %attr (770, %{uname}, %{gname}) %{_var}/log/crmsh
+%ghost %dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
+%ghost %dir %attr (770, %{uname}, %{gname}) %{_var}/log/crmsh
+
 %{_datadir}/bash-completion/completions/crm
 
 %if %{use_firewalld}

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.DkCvSy/_old  2026-03-11 20:57:32.247412343 +0100
+++ /var/tmp/diff_new_pack.DkCvSy/_new  2026-03-11 20:57:32.251412508 +0100
@@ -9,7 +9,7 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">c57573d04cc8f9f0a4162d330529bc609b721c2d</param>
+  <param 
name="changesrevision">5a3c65789ebe0308f5b38e20ec0c50ff80fafc04</param>
 </service>
 </servicedata>
 (No newline at EOF)

++++++ crmsh-5.0.0+20260226.8b99a4c5.tar.bz2 -> 
crmsh-5.0.0+20260309.5a3c6578.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/Makefile 
new/crmsh-5.0.0+20260309.5a3c6578/Makefile
--- old/crmsh-5.0.0+20260226.8b99a4c5/Makefile  2026-02-26 10:31:40.000000000 
+0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/Makefile  2026-03-09 15:27:39.000000000 
+0100
@@ -40,8 +40,6 @@
 
 install-non-python: non-python
        # additional directories
-       install -d -m0770 $(DESTDIR)$(localstatedir)/cache/crm
-       install -d -m0770 $(DESTDIR)$(localstatedir)/log/crmsh
        install -d -m0755 $(DESTDIR)${tmpfilesdir}
        # install configuration
        install -Dm0644 -t $(DESTDIR)$(confdir)/crm etc/{crm.conf,profiles.yml}
@@ -68,8 +66,6 @@
 
 uninstall-non-python:
        $(RM) -r $(DESTDIR)$(confdir)/crm
-       $(RM) -r $(DESTDIR)$(localstatedir)/cache/crm
-       $(RM) -r $(DESTDIR)$(localstatedir)/log/crm
        $(RM) -r $(DESTDIR)$(datadir)/crmsh
        $(RM) -r $(DESTDIR)$(datadir)/bash-completion/completions/crm
        $(RM) $(DESTDIR)$(mandir)/man8/crm.8
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/bootstrap.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/bootstrap.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/bootstrap.py        2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/bootstrap.py        2026-03-09 
15:27:39.000000000 +0100
@@ -75,7 +75,7 @@
         "/etc/drbd.conf", "/etc/drbd.d", "/etc/ha.d/ldirectord.cf", 
"/etc/lvm/lvm.conf", "/etc/multipath.conf",
         "/etc/samba/smb.conf", SYSCONFIG_NFS, SYSCONFIG_PCMK, 
PCMK_REMOTE_AUTH, PROFILES_FILE, CRM_CFG)
 
-INIT_STAGES_EXTERNAL = ("ssh", "firewalld", "csync2", "corosync", "sbd", 
"cluster", "ocfs2", "gfs2", "admin", "qdevice")
+INIT_STAGES_EXTERNAL = ("ssh", "firewalld", "csync2", "corosync", "cluster", 
"ocfs2", "gfs2", "admin", "sbd", "qdevice")
 INIT_STAGES_INTERNAL = ("qnetd_remote", )
 INIT_STAGES_ALL = INIT_STAGES_EXTERNAL + INIT_STAGES_INTERNAL
 JOIN_STAGES_EXTERNAL = ("ssh", "firewalld", "ssh_merge", "cluster")
@@ -153,7 +153,7 @@
         ctx.initialize_user()
         return ctx
 
-    def initialize_qdevice(self):
+    def _initialize_qdevice(self):
         """
         Initialize qdevice instance
         """
@@ -232,6 +232,9 @@
         """
         Validate sbd options
         """
+        no_sbd_option = not self.sbd_devices and not self.diskless_sbd
+        if self.watchdog and no_sbd_option:
+            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:
@@ -324,6 +327,7 @@
         for package in self.CORE_PACKAGES:
             if not utils.package_is_installed(package):
                 utils.fatal(f"Package '{package}' is not installed")
+        self._initialize_qdevice()
         if self.qdevice_inst:
             self.qdevice_inst.valid_qdevice_options()
         if self.ocfs2_devices or self.gfs2_devices or self.stage in ("ocfs2", 
"gfs2"):
@@ -722,6 +726,14 @@
 
     if not start_pacemaker(enable_flag=True):
         utils.fatal("Failed to start cluster services")
+
+    if _context and _context.type == "init":
+        if corosync.is_qdevice_configured():
+            logger.info("Starting and enable corosync-qdevice.service on %s", 
utils.this_node())
+            service_manager.start_service("corosync-qdevice.service", 
enable=True)
+        elif service_manager.service_is_enabled("corosync-qdevice.service"):
+            service_manager.disable_service("corosync-qdevice.service")
+
     wait_for_cluster()
 
 
@@ -1465,8 +1477,8 @@
     """
     Initial cluster configuration.
     """
+    service_manager = ServiceManager()
     if _context.stage == "cluster":
-        service_manager = ServiceManager()
         if service_manager.service_is_enabled(constants.SBD_SERVICE):
             service_manager.disable_service(constants.SBD_SERVICE)
 
@@ -1530,11 +1542,10 @@
     if not confirm("Do you want to configure QDevice?"):
         return
     while True:
-        try:
-            qdevice.QDevice.check_package_installed("corosync-qdevice")
+        if utils.package_is_installed("corosync-qdevice"):
             break
-        except ValueError as err:
-            logger.error(err)
+        else:
+            logger.error("Package corosync-qdevice is not installed")
             if confirm("Please install the package manually and press 'y' to 
continue"):
                 continue
             else:
@@ -1623,21 +1634,37 @@
         return
 
     logger.info("""Configure Qdevice/Qnetd:""")
-    utils.check_all_nodes_reachable("setup Qdevice")
-    cluster_node_list = utils.list_cluster_nodes()
-    for node in cluster_node_list:
-        if not 
ServiceManager().service_is_available("corosync-qdevice.service", node):
-            utils.fatal("corosync-qdevice.service is not available on 
{}".format(node))
 
+    is_qdevice_stage = _context.stage == "qdevice"
+    if is_qdevice_stage:
+        qdevice_reload_policy = 
qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_ADD)
+        if qdevice_reload_policy == 
qdevice.QdevicePolicy.QDEVICE_RESTART_LATER:
+            with utils.leverage_maintenance_mode() as enabled:
+                if not utils.able_to_restart_cluster(enabled):
+                    return
+                do_init_qdevice(is_qdevice_stage)
+            return
+
+    do_init_qdevice(is_qdevice_stage)
+
+
+def do_init_qdevice(in_stage: bool = False):
+    cluster_node_list = qdevice.get_node_list(in_stage)
     _setup_passwordless_ssh_for_qnetd(cluster_node_list)
 
     qdevice_inst = _context.qdevice_inst
     if corosync.is_qdevice_configured() and not confirm("Qdevice is already 
configured - overwrite?"):
-        qdevice_inst.start_qdevice_service()
+        if in_stage:
+            qdevice_inst.start_qdevice_service()
         return
+
     qdevice_inst.set_cluster_name()
-    qdevice_inst.valid_qnetd()
-    qdevice_inst.config_and_start_qdevice()
+    qdevice_inst.validate_and_start_qnetd()
+    qdevice_inst.certificate_and_config_qdevice()
+
+    if in_stage:
+        qdevice_inst.start_qdevice_service()
+
     adjust_properties()
 
 
@@ -2084,7 +2111,7 @@
     """
     Doing qdevice certificate process and start qdevice service on join node
     """
-    with logger_utils.status_long("Starting corosync-qdevice.service"):
+    with logger_utils.status_long(f"Starting and enable 
corosync-qdevice.service on {utils.this_node()}"):
         if corosync.is_qdevice_tls_on():
             qnetd_addr = corosync.get_value("quorum.device.net.host")
             qdevice_inst = qdevice.QDevice(qnetd_addr, cluster_node=seed_host)
@@ -2238,6 +2265,8 @@
     _context = context
     stage = _context.stage
 
+    _context.validate()
+
     init()
 
     _context.load_profiles()
@@ -2266,9 +2295,9 @@
         lock_inst = lock.Lock()
         try:
             with lock_inst.lock():
+                init_qdevice()
                 init_cluster()
                 init_admin()
-                init_qdevice()
                 init_ocfs2()
                 init_gfs2()
         except lock.ClaimLockError as err:
@@ -2327,6 +2356,8 @@
     global _context
     _context = context
 
+    _context.validate()
+
     init()
     _context.init_sbd_manager()
 
@@ -2397,7 +2428,17 @@
 
     utils.check_all_nodes_reachable("removing QDevice from the cluster")
     qdevice_reload_policy = 
qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_REMOVE)
+    if qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_RESTART_LATER:
+        with utils.leverage_maintenance_mode() as enabled:
+            if not utils.able_to_restart_cluster(enabled):
+                return
+            do_remove_qdevice(qdevice.QdevicePolicy.QDEVICE_RESTART)
+        return
 
+    do_remove_qdevice(qdevice_reload_policy)
+
+
+def do_remove_qdevice(qdevice_reload_policy: qdevice.QdevicePolicy) -> None:
     logger.info("Disable corosync-qdevice.service")
     invoke("crm cluster run 'systemctl disable corosync-qdevice'")
     if qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_RELOAD:
@@ -2411,11 +2452,10 @@
         corosync.configure_two_node(removing=True)
         sync_path(corosync.conf())
     if qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_RELOAD:
+        logger.info("Reloading cluster configuration after removing QDevice")
         sh.cluster_shell().get_stdout_or_raise_error("corosync-cfgtool -R")
     elif qdevice_reload_policy == qdevice.QdevicePolicy.QDEVICE_RESTART:
         restart_cluster()
-    else:
-        logger.warning("To remove qdevice service, need to restart cluster 
service manually on each node")
 
     adjust_properties()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/qdevice.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/qdevice.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/qdevice.py  2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/qdevice.py  2026-03-09 
15:27:39.000000000 +0100
@@ -32,7 +32,7 @@
     QDEVICE_RESTART_LATER = 2
 
 
-def evaluate_qdevice_quorum_effect(mode, diskless_sbd=False, is_stage=False):
+def evaluate_qdevice_quorum_effect(mode):
     """
     While adding/removing qdevice, get current expected votes and actual total 
votes,
     to calculate after adding/removing qdevice, whether cluster has quorum
@@ -45,14 +45,12 @@
         expected_votes += 1
     elif mode == QDEVICE_REMOVE:
         actual_votes -= 1
+    diskless_sbd = sbd.SBDUtils.is_using_diskless_sbd()
 
     if utils.calculate_quorate_status(expected_votes, actual_votes) and not 
diskless_sbd:
         # safe to use reload
         return QdevicePolicy.QDEVICE_RELOAD
-    elif mode == QDEVICE_ADD and not is_stage:
-        # Add qdevice from init process, safe to restart
-        return QdevicePolicy.QDEVICE_RESTART
-    elif xmlutil.CrmMonXmlParser().is_non_stonith_resource_running():
+    elif xmlutil.CrmMonXmlParser().is_non_stonith_resource_running() and not 
utils.is_cluster_in_maintenance_mode():
         # will lose quorum, with non-stonith resource running
         # no reload, no restart cluster service
         # just leave a warning
@@ -97,6 +95,14 @@
     return wrapper
 
 
+def get_node_list(is_stage: bool) -> list[str]:
+    me = utils.this_node()
+    if is_stage:
+        return utils.list_cluster_nodes() or [me]
+    else:
+        return [me]
+
+
 class QDevice(object):
     """Class to manage qdevice configuration and services
 
@@ -129,7 +135,6 @@
         self.cluster_name = cluster_name
         self.qdevice_reload_policy = QdevicePolicy.QDEVICE_RESTART
         self.is_stage = is_stage
-        self.using_diskless_sbd = False
 
     @property
     def qnetd_cacert_on_qnetd(self):
@@ -250,16 +255,19 @@
             if not os.path.exists(cmd.split()[0]):
                 raise ValueError("command {} not exist".format(cmd.split()[0]))
 
-    @staticmethod
-    def check_package_installed(pkg, remote=None):
-        if not utils.package_is_installed(pkg, remote_addr=remote):
-            raise ValueError("Package \"{}\" not installed on {}".format(pkg, 
remote if remote else "this node"))
+    def check_corosync_qdevice_available(self):
+        service_manager = ServiceManager()
+        for node in get_node_list(self.is_stage):
+            if not 
service_manager.service_is_available("corosync-qdevice.service", 
remote_addr=node):
+                raise ValueError(f"corosync-qdevice.service is not available 
on {node}")
 
     def valid_qdevice_options(self):
         """
         Validate qdevice related options
         """
-        self.check_package_installed("corosync-qdevice")
+        if self.is_stage:
+            utils.check_all_nodes_reachable("setup Qdevice")
+        self.check_corosync_qdevice_available()
         self.check_qnetd_addr(self.qnetd_addr)
         self.check_qdevice_port(self.port)
         self.check_qdevice_algo(self.algo)
@@ -268,42 +276,28 @@
         self.check_qdevice_heuristics(self.cmds)
         self.check_qdevice_heuristics_mode(self.mode)
 
-    def valid_qnetd(self):
-        """
-        Validate on qnetd node
-        """
+    def validate_and_start_qnetd(self):
         exception_msg = ""
-        suggest = ""
+        suggestion_msg= ""
         shell = sh.cluster_shell()
-        if not utils.package_is_installed("corosync-qnetd", 
remote_addr=self.qnetd_addr):
-            exception_msg = "Package \"corosync-qnetd\" not installed on 
{}!".format(self.qnetd_addr)
-            suggest = "install \"corosync-qnetd\" on 
{}".format(self.qnetd_addr)
-        else:
+        if utils.package_is_installed("corosync-qnetd", 
remote_addr=self.qnetd_addr):
             self.init_tls_certs_on_qnetd()
+            self.config_qnetd_port()
             self.start_qnetd()
-            cmd = "corosync-qnetd-tool -l -c {}".format(self.cluster_name)
+            cmd = f"corosync-qnetd-tool -l -c {self.cluster_name}"
             if shell.get_stdout_or_raise_error(cmd, self.qnetd_addr):
-                exception_msg = "This cluster's name \"{}\" already exists on 
qnetd server!".format(self.cluster_name)
-                suggest = "consider to use the different cluster-name property"
+                exception_msg = f"This cluster's name \"{self.cluster_name}\" 
already exists on qnetd server!"
+                suggestion_msg = "Please consider to use the different 
cluster-name property"
+        else:
+            exception_msg = f"Package \"corosync-qnetd\" not installed on 
{self.qnetd_addr}!"
+            suggestion_msg = f"Please install \"corosync-qnetd\" on 
{self.qnetd_addr}"
 
         if exception_msg:
-            if self.is_stage:
-                exception_msg += "\nPlease {}.".format(suggest)
-            else:
-                exception_msg += "\nCluster service already successfully 
started on this node except qdevice service.\nIf you still want to use qdevice, 
{}.\nThen run command \"crm cluster init\" with \"qdevice\" stage, like:\n  crm 
cluster init qdevice qdevice_related_options\nThat command will setup qdevice 
separately.".format(suggest)
-            raise ValueError(exception_msg)
-
-    def enable_qnetd(self):
-        ServiceManager().enable_service(self.qnetd_service, 
remote_addr=self.qnetd_addr)
-
-    def disable_qnetd(self):
-        ServiceManager().disable_service(self.qnetd_service, 
remote_addr=self.qnetd_addr)
+            raise ValueError(f"{exception_msg}\n{suggestion_msg}")
 
     def start_qnetd(self):
-        ServiceManager().start_service(self.qnetd_service, 
remote_addr=self.qnetd_addr)
-
-    def stop_qnetd(self):
-        ServiceManager().stop_service(self.qnetd_service, 
remote_addr=self.qnetd_addr)
+        logger.info("Starting and enable corosync-qnetd.service on %s" % 
self.qnetd_addr)
+        ServiceManager().start_service(self.qnetd_service, enable=True, 
remote_addr=self.qnetd_addr)
 
     def set_cluster_name(self):
         if not self.cluster_name:
@@ -337,6 +331,8 @@
 
     def copy_qnetd_crt_to_cluster(self, log: typing.Callable[[str, 
typing.Optional[str]], None]):
         """Copy exported QNetd CA certificate (qnetd-cacert.crt) to every 
node"""
+        if not self.is_stage:
+            return
         node_list = utils.list_cluster_nodes_except_me()
         if not node_list:
             return
@@ -365,9 +361,9 @@
         On one of cluster node initialize database by running
         /usr/sbin/corosync-qdevice-net-certutil -i -c qnetd-cacert.crt
         """
-        node_list = utils.list_cluster_nodes()
-        cmd = "corosync-qdevice-net-certutil -i -c 
{}".format(self.qnetd_cacert_on_local)
-        desc = "Initialize database on {}".format(node_list)
+        node_list = get_node_list(self.is_stage)
+        cmd = f"corosync-qdevice-net-certutil -i -c 
{self.qnetd_cacert_on_local}"
+        desc = f"Initialize database on {node_list}"
         log(desc, cmd)
         crmsh.parallax.parallax_call(node_list, cmd)
 
@@ -412,6 +408,8 @@
 
     def copy_p12_to_cluster(self, log: typing.Callable[[str, 
typing.Optional[str]], None]):
         """Copy output qdevice-net-node.p12 to all other cluster nodes"""
+        if not self.is_stage:
+            return
         node_list = utils.list_cluster_nodes_except_me()
         if not node_list:
             return
@@ -424,6 +422,8 @@
         """Import cluster certificate and key on all other cluster nodes:
         /usr/sbin/corosync-qdevice-net-certutil -m -c qdevice-net-node.p12
         """
+        if not self.is_stage:
+            return
         node_list = utils.list_cluster_nodes_except_me()
         if not node_list:
             return
@@ -549,15 +549,16 @@
         inst.save()
 
     @staticmethod
-    def remove_qdevice_db(addr_list=[]):
+    def remove_qdevice_db(addr_list=[], is_stage=True):
         """
         Remove qdevice database
         """
         if not os.path.exists(QDevice.qdevice_db_path):
             return
-        node_list = addr_list if addr_list else utils.list_cluster_nodes()
-        cmd = "rm -rf {}/*".format(QDevice.qdevice_path)
+
+        cmd = f"rm -rf {QDevice.qdevice_path}/*"
         QDevice.log_only_to_file("Remove qdevice database", cmd)
+        node_list = addr_list or get_node_list(is_stage)
         parallax.parallax_call(node_list, cmd)
 
     @classmethod
@@ -576,17 +577,6 @@
         cmd = "test -f {crq_file} && rm -f 
{crq_file}".format(crq_file=cls_inst.qdevice_crq_on_qnetd)
         shell.get_stdout_or_raise_error(cmd, qnetd_host)
 
-    def config_qdevice(self) -> None:
-        """
-        Update configuration and reload corosync if necessary
-        """
-        self.write_qdevice_config()
-        with logger_utils.status_long("Update configuration"):
-            corosync.configure_two_node(qdevice_adding=True)
-            bootstrap.sync_path(corosync.conf())
-            if self.qdevice_reload_policy == QdevicePolicy.QDEVICE_RELOAD:
-                sh.cluster_shell().get_stdout_or_raise_error("corosync-cfgtool 
-R")
-
     def config_qnetd_port(self):
         """
         Enable qnetd port in firewalld
@@ -605,51 +595,47 @@
         shell.get_stdout_or_raise_error("firewall-cmd --reload", 
self.qnetd_addr)
 
     def start_qdevice_service(self):
-        """
-        Start qdevice and qnetd service
-        """
         logger.info("Enable corosync-qdevice.service in cluster")
         utils.cluster_run_cmd("systemctl enable corosync-qdevice")
+
+        self.qdevice_reload_policy = 
evaluate_qdevice_quorum_effect(QDEVICE_ADD)
+
         if self.qdevice_reload_policy == QdevicePolicy.QDEVICE_RELOAD:
+            logger.info("Reloading cluster configuration before starting 
corosync-qdevice.service")
+            sh.cluster_shell().get_stdout_or_raise_error("corosync-cfgtool -R")
             logger.info("Starting corosync-qdevice.service in cluster")
             utils.cluster_run_cmd("systemctl restart corosync-qdevice")
         elif self.qdevice_reload_policy == QdevicePolicy.QDEVICE_RESTART:
             bootstrap.restart_cluster()
-        else:
-            logger.warning("To use qdevice service, need to restart cluster 
service manually on each node")
-
-        logger.info("Enable corosync-qnetd.service on 
{}".format(self.qnetd_addr))
-        self.enable_qnetd()
-        logger.info("Starting corosync-qnetd.service on 
{}".format(self.qnetd_addr))
-        self.start_qnetd()
 
     def adjust_sbd_watchdog_timeout_with_qdevice(self):
         """
         Adjust SBD_WATCHDOG_TIMEOUT when configuring qdevice and diskless SBD
         """
-        self.using_diskless_sbd = sbd.SBDUtils.is_using_diskless_sbd()
-        # add qdevice after diskless sbd started
-        if self.using_diskless_sbd:
+        sbd_service_enabled = 
ServiceManager().service_is_enabled("sbd.service")
+        sbd_device = sbd.SBDUtils.get_sbd_device_from_config()
+        if sbd_service_enabled and not sbd_device: # configured diskless SBD
             res = 
sbd.SBDUtils.get_sbd_value_from_config("SBD_WATCHDOG_TIMEOUT")
             if not res or int(res) < 
sbd.SBDTimeout.SBD_WATCHDOG_TIMEOUT_DEFAULT_WITH_QDEVICE:
                 sbd_watchdog_timeout_qdevice = 
sbd.SBDTimeout.SBD_WATCHDOG_TIMEOUT_DEFAULT_WITH_QDEVICE
                 
sbd.SBDManager.update_sbd_configuration({"SBD_WATCHDOG_TIMEOUT": 
str(sbd_watchdog_timeout_qdevice)})
-                utils.set_property("stonith-watchdog-timeout", 2 * 
sbd_watchdog_timeout_qdevice)
+                if self.is_stage:
+                    utils.set_property("stonith-watchdog-timeout", 
2*sbd_watchdog_timeout_qdevice)
 
     @qnetd_lock_for_same_cluster_name
-    def config_and_start_qdevice(self):
-        """
-        Wrap function to collect functions to config and start qdevice
-        """
-        QDevice.remove_qdevice_db()
+    def certificate_and_config_qdevice(self):
+        QDevice.remove_qdevice_db(is_stage=self.is_stage)
+
         if self.tls == "on" or self.tls == 'required':
             with logger_utils.status_long("Qdevice certification process"):
                 self.certificate_process_on_init()
+
         self.adjust_sbd_watchdog_timeout_with_qdevice()
-        self.qdevice_reload_policy = 
evaluate_qdevice_quorum_effect(QDEVICE_ADD, self.using_diskless_sbd, 
self.is_stage)
-        self.config_qdevice()
-        self.config_qnetd_port()
-        self.start_qdevice_service()
+        self.write_qdevice_config()
+        if self.is_stage:
+            with logger_utils.status_long("Updating and syncing qdevice 
configuration"):
+                corosync.configure_two_node(qdevice_adding=True)
+                bootstrap.sync_path(corosync.conf())
 
     @staticmethod
     def check_qdevice_vote():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/ui_cluster.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/ui_cluster.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/ui_cluster.py       2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/ui_cluster.py       2026-03-09 
15:27:39.000000000 +0100
@@ -357,6 +357,7 @@
     ocfs2       Configure OCFS2 (requires -o <dev>) NOTE: this is a Technical 
Preview
     gfs2        Configure GFS2 (requires -g <dev>) NOTE: this is a Technical 
Preview
     admin       Create administration virtual IP (optional)
+    sbd         Configure SBD (requires -s <dev>)
     qdevice     Configure qdevice and qnetd
 
 Note:
@@ -426,7 +427,7 @@
         parser.add_argument("-S", "--enable-sbd", dest="diskless_sbd", 
action="store_true",
                             help="Enable SBD even if no SBD device is 
configured (diskless mode)")
         parser.add_argument("-w", "--watchdog", dest="watchdog", 
metavar="WATCHDOG",
-                            help="Use the given watchdog device or driver 
name")
+                            help="Use the given watchdog device or driver name 
(only valid together with -s or -S option)")
         parser.add_argument("-x", "--skip-csync2-sync", dest="skip_csync2", 
action="store_true",
                             help="Skip csync2 initialization (default, 
deprecated)")
         parser.add_argument('--use-ssh-agent', 
action=argparse.BooleanOptionalAction, dest='use_ssh_agent', default=True,
@@ -478,8 +479,6 @@
             stage = args[0]
 
         if options.qnetd_addr_input:
-            if not 
ServiceManager().service_is_available("corosync-qdevice.service"):
-                utils.fatal("corosync-qdevice.service is not available")
             if options.qdevice_heuristics_mode and not 
options.qdevice_heuristics:
                 parser.error("Option --qdevice-heuristics is required if want 
to configure heuristics mode")
             options.qdevice_heuristics_mode = options.qdevice_heuristics_mode 
or "sync"
@@ -494,8 +493,6 @@
         boot_context.args = args
         boot_context.cluster_is_running = 
ServiceManager(sh.ClusterShellAdaptorForLocalShell(sh.LocalShell())).service_is_active("pacemaker.service")
         boot_context.type = "init"
-        boot_context.initialize_qdevice()
-        boot_context.validate()
 
         bootstrap.bootstrap_init(boot_context)
         bootstrap.bootstrap_add(boot_context)
@@ -553,7 +550,6 @@
         join_context.stage = stage
         join_context.cluster_is_running = 
ServiceManager(sh.ClusterShellAdaptorForLocalShell(sh.LocalShell())).service_is_active("pacemaker.service")
         join_context.type = "join"
-        join_context.validate()
 
         bootstrap.bootstrap_join(join_context)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/ui_sbd.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/ui_sbd.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/ui_sbd.py   2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/ui_sbd.py   2026-03-09 
15:27:39.000000000 +0100
@@ -177,7 +177,7 @@
 
         timeout_usage_str = " ".join([f"[{t}-timeout=<integer>]" for t in 
timeout_types])
         show_usage = f"crm sbd configure show [{'|'.join(show_types)}]"
-        return f"Usage:\n{show_usage}\ncrm sbd configure {timeout_usage_str} 
[watchdog-device=<device>]\n"
+        return f"Usage:\n{show_usage}\ncrm sbd configure {timeout_usage_str} 
[watchdog-device=<device|driver>]\n"
 
     def _show_sysconfig(self) -> None:
         '''
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/utils.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/utils.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/utils.py    2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/utils.py    2026-03-09 
15:27:39.000000000 +0100
@@ -2502,8 +2502,12 @@
 
     crm_mon_inst = xmlutil.CrmMonXmlParser(peer_node)
     if crm_mon_inst.not_connected():
-        nodes_to_check = list_cluster_nodes_except_me()
-        offline_nodes = list_cluster_nodes_except_me()
+        try:
+            nodes_to_check = list_cluster_nodes_except_me()
+            offline_nodes = list_cluster_nodes_except_me()
+        except ValueError:
+            nodes_to_check = []
+            offline_nodes = []
     else:
         nodes_to_check = crm_mon_inst.get_node_list(online=True, 
node_type="member")
         offline_nodes = crm_mon_inst.get_node_list(online=False)
@@ -3042,7 +3046,7 @@
             except Exception:
                 pass
             await asyncio.sleep(interval_sec)
-    return 
asyncio.get_event_loop_policy().get_event_loop().run_until_complete(asyncio.wait_for(wrapper(),
 timeout_sec))
+    return asyncio.run(asyncio.wait_for(wrapper(), timeout_sec))
 
 
 def fetch_cluster_node_list_from_node(init_node):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/watchdog.py 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh/watchdog.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh/watchdog.py 2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh/watchdog.py 2026-03-09 
15:27:39.000000000 +0100
@@ -167,7 +167,7 @@
         # self._input is invalid, exit
         rc, _, _ = ShellUtils().get_stdout_stderr(f"modinfo {self._input}")
         if rc != 0:
-            utils.fatal("Should provide valid watchdog device or driver name 
by -w option")
+            utils.fatal("Should provide valid watchdog device or driver name")
 
         # self._input is a driver name, load it if it was unloaded
         if not self._driver_is_loaded(self._input):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh.spec.in 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh.spec.in
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh.spec.in     2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh.spec.in     2026-03-09 
15:27:39.000000000 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package crmsh
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -45,7 +45,6 @@
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2
-Source1:        %{name}.tmpfiles.d.conf
 
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
 %if 0%{?suse_version}
@@ -60,11 +59,11 @@
 Requires:       python3-lxml
 Requires:       python3-packaging
 Recommends:     bash-completion
+BuildRequires:  python3-PyYAML
 BuildRequires:  python3-lxml
-BuildRequires:  python3-setuptools
 BuildRequires:  python3-pip
+BuildRequires:  python3-setuptools
 BuildRequires:  python3-wheel
-BuildRequires:  python3-PyYAML
 
 %if 0%{?suse_version}
 # only require csync2 on SUSE since bootstrap
@@ -85,9 +84,8 @@
 %else
 Requires:       python3-dateutil
 BuildRequires:  pyproject-rpm-macros
-BuildRequires:  python3-devel
-BuildRequires:  python3-setuptools
 BuildRequires:  python3-dateutil
+BuildRequires:  python3-devel
 %endif
 
 # Required for core functionality
@@ -252,8 +250,9 @@
 
 %config %{_sysconfdir}/crm
 
-%dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
-%dir %attr (770, %{uname}, %{gname}) %{_var}/log/crmsh
+%ghost %dir %attr (770, %{uname}, %{gname}) %{_var}/cache/crm
+%ghost %dir %attr (770, %{uname}, %{gname}) %{_var}/log/crmsh
+
 %{_datadir}/bash-completion/completions/crm
 
 %if %{use_firewalld}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/crmsh.tmpfiles.d.conf 
new/crmsh-5.0.0+20260309.5a3c6578/crmsh.tmpfiles.d.conf
--- old/crmsh-5.0.0+20260226.8b99a4c5/crmsh.tmpfiles.d.conf     2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/crmsh.tmpfiles.d.conf     2026-03-09 
15:27:39.000000000 +0100
@@ -1 +1,2 @@
-d   /var/log/crmsh  0775    hacluster   haclient    -
+d   /var/log/crmsh  0770    hacluster   haclient    -
+D   /var/cache/crm  0770    hacluster   haclient    -
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260226.8b99a4c5/doc/crm.8.adoc 
new/crmsh-5.0.0+20260309.5a3c6578/doc/crm.8.adoc
--- old/crmsh-5.0.0+20260226.8b99a4c5/doc/crm.8.adoc    2026-02-26 
10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/doc/crm.8.adoc    2026-03-09 
15:27:39.000000000 +0100
@@ -2227,11 +2227,11 @@
 ...............
 # For disk-based SBD
 crm sbd configure show [disk_metadata|sysconfig|property]
-crm sbd configure [watchdog-timeout=<integer>] [allocate-timeout=<integer>] 
[loop-timeout=<integer>] [msgwait-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device>]
+crm sbd configure [watchdog-timeout=<integer>] [allocate-timeout=<integer>] 
[loop-timeout=<integer>] [msgwait-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device|driver>]
 
 # For disk-less SBD
 crm sbd configure show [sysconfig|property]
-crm sbd configure [watchdog-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device>]
+crm sbd configure [watchdog-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device|driver>]
 ...............
 
 example:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/features/bootstrap_options.feature 
new/crmsh-5.0.0+20260309.5a3c6578/test/features/bootstrap_options.feature
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/features/bootstrap_options.feature   
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/features/bootstrap_options.feature   
2026-03-09 15:27:39.000000000 +0100
@@ -42,7 +42,7 @@
   @clean
   Scenario: Stage validation
     When    Try "crm cluster init fdsf -y" on "hanode1"
-    Then    Expected "Invalid stage: fdsf(available stages: ssh, firewalld, 
csync2, corosync, sbd, cluster, ocfs2, gfs2, admin, qdevice)" in stderr
+    Then    Expected "Invalid stage: fdsf(available stages: ssh, firewalld, 
csync2, corosync, cluster, ocfs2, gfs2, admin, sbd, qdevice)" in stderr
     When    Try "crm cluster join fdsf -y" on "hanode1"
     Then    Expected "Invalid stage: fdsf(available stages: ssh, firewalld, 
ssh_merge, cluster)" in stderr
     When    Try "crm cluster join ssh -y" on "hanode1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/features/qdevice_setup_remove.feature 
new/crmsh-5.0.0+20260309.5a3c6578/test/features/qdevice_setup_remove.feature
--- 
old/crmsh-5.0.0+20260226.8b99a4c5/test/features/qdevice_setup_remove.feature    
    2026-02-26 10:31:40.000000000 +0100
+++ 
new/crmsh-5.0.0+20260309.5a3c6578/test/features/qdevice_setup_remove.feature    
    2026-03-09 15:27:39.000000000 +0100
@@ -15,8 +15,6 @@
   Scenario: Setup qdevice/qnetd during init/join process
     When    Run "crm cluster init --qnetd-hostname=qnetd-node -y" on "hanode1"
     Then    Cluster service is "started" on "hanode1"
-    # for bsc#1181415
-    Then    Expected "Restarting cluster service" in stdout
     And     Service "corosync-qdevice" is "started" on "hanode1"
     When    Run "crm cluster join -c hanode1 -y" on "hanode2"
     Then    Cluster service is "started" on "hanode2"
@@ -26,6 +24,9 @@
     And     Show status from qnetd
     And     Show corosync qdevice configuration
     And     Show qdevice status
+    And     Service "corosync-qdevice" is "enabled" on "hanode1"
+    And     Service "corosync-qdevice" is "enabled" on "hanode2"
+    And     Service "corosync-qnetd" is "enabled" on "qnetd-node"
 
   @clean
   Scenario: Setup qdevice/qnetd on running cluster
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/features/qdevice_validate.feature 
new/crmsh-5.0.0+20260309.5a3c6578/test/features/qdevice_validate.feature
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/features/qdevice_validate.feature    
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/features/qdevice_validate.feature    
2026-03-09 15:27:39.000000000 +0100
@@ -64,16 +64,8 @@
   Scenario: Node for qnetd not installed corosync-qnetd
     Given   Cluster service is "stopped" on "hanode2"
     When    Try "crm cluster init --qnetd-hostname=hanode2 -y"
-    Then    Except multiple lines
-      """
-      ERROR: cluster.init: Package "corosync-qnetd" not installed on hanode2!
-      Cluster service already successfully started on this node except qdevice 
service.
-      If you still want to use qdevice, install "corosync-qnetd" on hanode2.
-      Then run command "crm cluster init" with "qdevice" stage, like:
-        crm cluster init qdevice qdevice_related_options
-      That command will setup qdevice separately.
-      """
-    And     Cluster service is "started" on "hanode1"
+    Then    Expected "ERROR: cluster.init: Package "corosync-qnetd" not 
installed on hanode2!" in stderr
+    And     Cluster service is "stopped" on "hanode1"
 
   @clean
   Scenario: Raise error when adding qdevice stage with the same cluster name
@@ -94,17 +86,8 @@
     Given   Cluster service is "stopped" on "hanode2"
     When    Try "crm cluster init -n cluster1 --qnetd-hostname=qnetd-node -y" 
on "hanode2"
     When    Try "crm cluster init -n cluster1 --qnetd-hostname=qnetd-node -y"
-    Then    Except multiple lines
-      """
-      ERROR: cluster.init: This cluster's name "cluster1" already exists on 
qnetd server!
-      Cluster service already successfully started on this node except qdevice 
service.
-      If you still want to use qdevice, consider to use the different 
cluster-name property.
-      Then run command "crm cluster init" with "qdevice" stage, like:
-        crm cluster init qdevice qdevice_related_options
-      That command will setup qdevice separately.
-      """
-    And     Cluster service is "started" on "hanode1"
-    And     Cluster service is "started" on "hanode2"
+    Then    Expected "ERROR: cluster.init: This cluster's name "cluster1" 
already exists on qnetd server!" in stderr
+    And     Cluster service is "stopped" on "hanode1"
 
   @clean
   Scenario: Run qdevice stage on inactive cluster node
@@ -130,10 +113,10 @@
     Then    Cluster service is "started" on "hanode1"
     And     Service "corosync-qdevice" is "stopped" on "hanode1"
     When    Run "crm configure primitive d Dummy op monitor interval=3s" on 
"hanode1"
-    When    Run "crm cluster init qdevice --qnetd-hostname=qnetd-node -y" on 
"hanode1"
-    Then    Expected "WARNING: To use qdevice service, need to restart cluster 
service manually on each node" in stderr
+    When    Try "crm cluster init qdevice --qnetd-hostname=qnetd-node -y"
+    Then    Expected "Or use 'crm -F/--force' option to leverage maintenance 
mode" in stderr
     And     Service "corosync-qdevice" is "stopped" on "hanode1"
-    When    Run "crm cluster restart" on "hanode1"
+    When    Run "crm -F cluster init qdevice --qnetd-hostname=qnetd-node -y" 
on "hanode1"
     Then    Service "corosync-qdevice" is "started" on "hanode1"
 
   @clean
@@ -152,10 +135,10 @@
     Then    Cluster service is "started" on "hanode1"
     And     Service "corosync-qdevice" is "started" on "hanode1"
     When    Run "crm configure primitive d Dummy op monitor interval=3s" on 
"hanode1"
-    When    Run "crm cluster remove --qdevice -y" on "hanode1"
-    Then    Expected "WARNING: To remove qdevice service, need to restart 
cluster service manually on each node" in stderr
+    When    Try "crm cluster remove --qdevice -y"
+    Then    Expected "Or use 'crm -F/--force' option to leverage maintenance 
mode" in stderr
     Then    Cluster service is "started" on "hanode1"
     And     Service "corosync-qdevice" is "started" on "hanode1"
-    When    Run "crm cluster restart" on "hanode1"
+    When    Run "crm -F cluster remove --qdevice -y" on "hanode1"
     Then    Cluster service is "started" on "hanode1"
     And     Service "corosync-qdevice" is "stopped" on "hanode1"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/features/steps/const.py 
new/crmsh-5.0.0+20260309.5a3c6578/test/features/steps/const.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/features/steps/const.py      
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/features/steps/const.py      
2026-03-09 15:27:39.000000000 +0100
@@ -76,7 +76,8 @@
   -S, --enable-sbd      Enable SBD even if no SBD device is configured
                         (diskless mode)
   -w, --watchdog WATCHDOG
-                        Use the given watchdog device or driver name
+                        Use the given watchdog device or driver name (only
+                        valid together with -s or -S option)
   -x, --skip-csync2-sync
                         Skip csync2 initialization (default, deprecated)
   --use-ssh-agent, --no-use-ssh-agent
@@ -162,6 +163,7 @@
     ocfs2       Configure OCFS2 (requires -o <dev>) NOTE: this is a Technical 
Preview
     gfs2        Configure GFS2 (requires -g <dev>) NOTE: this is a Technical 
Preview
     admin       Create administration virtual IP (optional)
+    sbd         Configure SBD (requires -s <dev>)
     qdevice     Configure qdevice and qnetd
 
 Note:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_bootstrap.py 
new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_bootstrap.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_bootstrap.py  
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_bootstrap.py  
2026-03-09 15:27:39.000000000 +0100
@@ -106,7 +106,7 @@
 
     @mock.patch('crmsh.qdevice.QDevice')
     def test_initialize_qdevice_return(self, mock_qdevice):
-        self.ctx_inst.initialize_qdevice()
+        self.ctx_inst._initialize_qdevice()
         mock_qdevice.assert_not_called()
 
     @mock.patch('crmsh.qdevice.QDevice')
@@ -115,7 +115,7 @@
         ctx.qnetd_addr_input = "node3"
         ctx.qdevice_port = 123
         ctx.stage = ""
-        ctx.initialize_qdevice()
+        ctx._initialize_qdevice()
         mock_qdevice.assert_called_once_with(qnetd_addr='node3', port=123, 
ssh_user=None, algo=None, tie_breaker=None, tls=None, cmds=None, mode=None, 
is_stage=False)
 
     @mock.patch('crmsh.qdevice.QDevice')
@@ -124,7 +124,7 @@
         ctx.qnetd_addr_input = "alice@node3"
         ctx.qdevice_port = 123
         ctx.stage = ""
-        ctx.initialize_qdevice()
+        ctx._initialize_qdevice()
         mock_qdevice.assert_called_once_with(qnetd_addr='node3', port=123, 
ssh_user='alice', algo=None, tie_breaker=None, tls=None, cmds=None, mode=None, 
is_stage=False)
 
     @mock.patch('crmsh.utils.package_is_installed')
@@ -1203,129 +1203,99 @@
             ])
 
     @mock.patch('crmsh.service_manager.ServiceManager.disable_service')
-    @mock.patch('logging.Logger.info')
-    def test_init_qdevice_no_config(self, mock_status, mock_disable):
+    @mock.patch('crmsh.bootstrap.configure_qdevice_interactive')
+    def test_init_qdevice_no_config(self, mock_configure, mock_disable):
         bootstrap._context = mock.Mock(qdevice_inst=None)
         bootstrap.init_qdevice()
-        mock_status.assert_not_called()
+        mock_configure.assert_called_once_with()
         mock_disable.assert_called_once_with("corosync-qdevice.service")
 
-    @mock.patch('crmsh.utils.check_all_nodes_reachable')
-    
@mock.patch('crmsh.bootstrap._select_user_pair_for_ssh_for_secondary_components')
-    @mock.patch('crmsh.utils.HostUserConfig')
-    @mock.patch('crmsh.user_of_host.UserOfHost.instance')
-    @mock.patch('crmsh.utils.list_cluster_nodes')
-    @mock.patch('crmsh.bootstrap.confirm')
-    @mock.patch('crmsh.corosync.is_qdevice_configured')
-    @mock.patch('crmsh.bootstrap.configure_ssh_key')
-    @mock.patch('crmsh.utils.check_ssh_passwd_need')
-    @mock.patch('crmsh.sh.LocalShell')
+    @mock.patch('crmsh.bootstrap.do_init_qdevice')
+    @mock.patch('crmsh.utils.able_to_restart_cluster')
+    @mock.patch('crmsh.utils.leverage_maintenance_mode')
+    @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect')
     @mock.patch('logging.Logger.info')
-    def test_init_qdevice_already_configured(
-            self,
-            mock_status, mock_local_shell, mock_ssh, mock_configure_ssh_key,
-            mock_qdevice_configured, mock_confirm, mock_list_nodes, 
mock_user_of_host,
-            mock_host_user_config_class,
-            mock_select_user_pair_for_ssh,
-            mock_check_all_nodes
-    ):
-        mock_list_nodes.return_value = []
-        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
current_user="bob")
-        mock_ssh.return_value = False
-        mock_user_of_host.return_value = 
mock.MagicMock(crmsh.user_of_host.UserOfHost)
-        mock_qdevice_configured.return_value = True
-        mock_confirm.return_value = False
-        self.qdevice_with_ip.start_qdevice_service = mock.Mock()
-        mock_select_user_pair_for_ssh.return_value = ("bob", "bob", 
'qnetd-node')
+    def test_init_qdevice_unable_to_restart_cluster(self, mock_info, 
mock_evaluate_qdevice_quorum_effect, mock_leverage_maintenance_mode,
+            mock_able_to_restart_cluster, mock_do_init_qdevice):
+        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
stage="qdevice")
+        mock_evaluate_qdevice_quorum_effect.return_value = 
qdevice.QdevicePolicy.QDEVICE_RESTART_LATER
+        enable_value = True
+        cm = mock.Mock()
+        cm.__enter__ = mock.Mock(return_value=enable_value)
+        cm.__exit__ = mock.Mock(return_value=False)
+        mock_leverage_maintenance_mode.return_value = cm
+        mock_able_to_restart_cluster.return_value = False
 
         bootstrap.init_qdevice()
 
-        mock_status.assert_called_once_with("Configure Qdevice/Qnetd:")
-        mock_local_shell.assert_has_calls([
-            mock.call(additional_environ={'SSH_AUTH_SOCK': ''}),
-            mock.call(additional_environ={'SSH_AUTH_SOCK': ''}),
-        ])
-        mock_ssh.assert_called_once_with("bob", "bob", "qnetd-node", 
mock_local_shell.return_value)
-        mock_configure_ssh_key.assert_not_called()
-        
mock_host_user_config_class.return_value.save_remote.assert_called_once_with(mock_list_nodes.return_value)
-        mock_qdevice_configured.assert_called_once_with()
-        mock_confirm.assert_called_once_with("Qdevice is already configured - 
overwrite?")
-        self.qdevice_with_ip.start_qdevice_service.assert_called_once_with()
-        mock_check_all_nodes.assert_called_once_with("setup Qdevice")
+        mock_info.assert_called_once_with("Configure Qdevice/Qnetd:")
+        mock_able_to_restart_cluster.assert_called_once_with(True)
+        mock_do_init_qdevice.assert_not_called()
 
-    @mock.patch('crmsh.utils.check_all_nodes_reachable')
-    
@mock.patch('crmsh.bootstrap._select_user_pair_for_ssh_for_secondary_components')
-    @mock.patch('crmsh.utils.HostUserConfig')
-    @mock.patch('crmsh.user_of_host.UserOfHost.instance')
-    @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay')
-    @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults')
-    @mock.patch('crmsh.utils.list_cluster_nodes')
-    @mock.patch('crmsh.utils.this_node')
-    @mock.patch('crmsh.corosync.is_qdevice_configured')
-    @mock.patch('crmsh.bootstrap.configure_ssh_key')
-    @mock.patch('crmsh.utils.check_ssh_passwd_need')
-    @mock.patch('crmsh.sh.LocalShell')
+    @mock.patch('crmsh.bootstrap.do_init_qdevice')
+    @mock.patch('crmsh.utils.able_to_restart_cluster')
+    @mock.patch('crmsh.utils.leverage_maintenance_mode')
+    @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect')
     @mock.patch('logging.Logger.info')
-    def test_init_qdevice(self, mock_info, mock_local_shell, mock_ssh, 
mock_configure_ssh_key, mock_qdevice_configured,
-                          mock_this_node, mock_list_nodes, 
mock_adjust_priority, mock_adjust_fence_delay,
-                          mock_user_of_host, mock_host_user_config_class, 
mock_select_user_pair_for_ssh, mock_check_all_nodes):
-        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
current_user="bob")
-        mock_this_node.return_value = "192.0.2.100"
-        mock_list_nodes.return_value = []
-        mock_ssh.return_value = False
-        mock_user_of_host.return_value = 
mock.MagicMock(crmsh.user_of_host.UserOfHost)
-        mock_qdevice_configured.return_value = False
-        self.qdevice_with_ip.set_cluster_name = mock.Mock()
-        self.qdevice_with_ip.valid_qnetd = mock.Mock()
-        self.qdevice_with_ip.config_and_start_qdevice = mock.Mock()
-        mock_select_user_pair_for_ssh.return_value = ("bob", "bob", 
"qnetd-node")
+    def test_init_qdevice_able_to_restart_cluster(self, mock_info, 
mock_evaluate_qdevice_quorum_effect, mock_leverage_maintenance_mode,
+            mock_able_to_restart_cluster, mock_do_init_qdevice):
+        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
stage="qdevice")
+        mock_evaluate_qdevice_quorum_effect.return_value = 
qdevice.QdevicePolicy.QDEVICE_RESTART_LATER
+        enable_value = True
+        cm = mock.Mock()
+        cm.__enter__ = mock.Mock(return_value=enable_value)
+        cm.__exit__ = mock.Mock(return_value=False)
+        mock_leverage_maintenance_mode.return_value = cm
+        mock_able_to_restart_cluster.return_value = True
 
         bootstrap.init_qdevice()
 
         mock_info.assert_called_once_with("Configure Qdevice/Qnetd:")
-        mock_local_shell.assert_has_calls([
-            mock.call(additional_environ={'SSH_AUTH_SOCK': ''}),
-            mock.call(additional_environ={'SSH_AUTH_SOCK': ''}),
-        ])
-        mock_ssh.assert_called_once_with("bob", "bob", "qnetd-node", 
mock_local_shell.return_value)
-        mock_host_user_config_class.return_value.add.assert_has_calls([
-            mock.call('bob', '192.0.2.100'),
-            mock.call('bob', 'qnetd-node'),
-        ])
-        
mock_host_user_config_class.return_value.save_remote.assert_called_once_with(mock_list_nodes.return_value)
-        mock_qdevice_configured.assert_called_once_with()
-        self.qdevice_with_ip.set_cluster_name.assert_called_once_with()
-        self.qdevice_with_ip.valid_qnetd.assert_called_once_with()
-        self.qdevice_with_ip.config_and_start_qdevice.assert_called_once_with()
-        mock_check_all_nodes.assert_called_once_with("setup Qdevice")
+        mock_able_to_restart_cluster.assert_called_once_with(True)
+        mock_do_init_qdevice.assert_called_once_with(True)
 
-    @mock.patch('crmsh.utils.check_all_nodes_reachable')
-    @mock.patch('crmsh.utils.fatal')
-    @mock.patch('crmsh.utils.HostUserConfig')
-    @mock.patch('crmsh.service_manager.ServiceManager.service_is_available')
-    @mock.patch('crmsh.utils.list_cluster_nodes')
-    @mock.patch('logging.Logger.info')
-    def test_init_qdevice_service_not_available(
-            self,
-            mock_info, mock_list_nodes, mock_available,
-            mock_host_user_config_class,
-            mock_fatal,
-            mock_check_all_nodes
-    ):
-        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip)
+    @mock.patch('crmsh.bootstrap.confirm')
+    @mock.patch('crmsh.corosync.is_qdevice_configured')
+    @mock.patch('crmsh.bootstrap._setup_passwordless_ssh_for_qnetd')
+    @mock.patch('crmsh.qdevice.get_node_list')
+    def test_do_init_qdevice_already_configured(self, mock_list_nodes, 
mock_setup_passwordless_ssh_for_qnetd, mock_is_qdevice_configured, 
mock_confirm):
+        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
stage="qdevice")
         mock_list_nodes.return_value = ["node1"]
-        mock_available.return_value = False
-        mock_fatal.side_effect = SystemExit
+        mock_is_qdevice_configured.return_value = True
+        mock_confirm.return_value = False
+        bootstrap._context.qdevice_inst.start_qdevice_service = mock.Mock()
 
-        with self.assertRaises(SystemExit):
-            bootstrap.init_qdevice()
+        bootstrap.do_init_qdevice(True)
 
-        mock_host_user_config_class.return_value.save_local.assert_not_called()
-        
mock_host_user_config_class.return_value.save_remote.assert_not_called()
-        mock_fatal.assert_called_once_with("corosync-qdevice.service is not 
available on node1")
-        mock_available.assert_called_once_with("corosync-qdevice.service", 
"node1")
-        mock_info.assert_called_once_with("Configure Qdevice/Qnetd:")
-        mock_check_all_nodes.assert_called_once_with("setup Qdevice")
+        mock_list_nodes.assert_called_once_with(True)
+        mock_is_qdevice_configured.assert_called_once_with()
+        mock_confirm.assert_called_once_with("Qdevice is already configured - 
overwrite?")
+        
mock_setup_passwordless_ssh_for_qnetd.assert_called_once_with(["node1"])
+        
bootstrap._context.qdevice_inst.start_qdevice_service.assert_called_once_with()
+
+    @mock.patch('crmsh.bootstrap.adjust_properties')
+    @mock.patch('crmsh.corosync.is_qdevice_configured')
+    @mock.patch('crmsh.bootstrap._setup_passwordless_ssh_for_qnetd')
+    @mock.patch('crmsh.qdevice.get_node_list')
+    def test_do_init_qdevice(self, mock_list_nodes, 
mock_setup_passwordless_ssh_for_qnetd,
+            mock_is_qdevice_configured, mock_adjust_properties):
+        bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
stage="qdevice")
+        mock_list_nodes.return_value = ["node1"]
+        mock_is_qdevice_configured.return_value = False
+        self.qdevice_with_ip.set_cluster_name = mock.Mock()
+        self.qdevice_with_ip.validate_and_start_qnetd = mock.Mock()
+        self.qdevice_with_ip.certificate_and_config_qdevice = mock.Mock()
+        bootstrap._context.qdevice_inst.start_qdevice_service = mock.Mock()
+
+        bootstrap.do_init_qdevice(True)
+
+        mock_list_nodes.assert_called_once_with(True)
+        mock_is_qdevice_configured.assert_called_once_with()
+        
mock_setup_passwordless_ssh_for_qnetd.assert_called_once_with(["node1"])
+        self.qdevice_with_ip.set_cluster_name.assert_called_once_with()
+        self.qdevice_with_ip.validate_and_start_qnetd.assert_called_once_with()
+        
self.qdevice_with_ip.certificate_and_config_qdevice.assert_called_once_with()
+        
bootstrap._context.qdevice_inst.start_qdevice_service.assert_called_once_with()
 
     @mock.patch('crmsh.bootstrap.prompt_for_string')
     def test_configure_qdevice_interactive_return(self, mock_prompt):
@@ -1342,13 +1312,13 @@
         mock_confirm.assert_called_once_with("Do you want to configure 
QDevice?")
 
     @mock.patch('logging.Logger.error')
-    @mock.patch('crmsh.qdevice.QDevice.check_package_installed')
+    @mock.patch('crmsh.utils.package_is_installed')
     @mock.patch('logging.Logger.info')
     @mock.patch('crmsh.bootstrap.confirm')
     def test_configure_qdevice_interactive_not_installed(self, mock_confirm, 
mock_info, mock_installed, mock_error):
         bootstrap._context = mock.Mock(yes_to_all=False)
         mock_confirm.side_effect = [True, False]
-        mock_installed.side_effect = ValueError("corosync-qdevice not 
installed")
+        mock_installed.return_value = False
         bootstrap.configure_qdevice_interactive()
         mock_confirm.assert_has_calls([
             mock.call("Do you want to configure QDevice?"),
@@ -1357,12 +1327,13 @@
 
     @mock.patch('crmsh.qdevice.QDevice')
     @mock.patch('crmsh.bootstrap.prompt_for_string')
-    @mock.patch('crmsh.qdevice.QDevice.check_package_installed')
+    @mock.patch('crmsh.utils.package_is_installed')
     @mock.patch('logging.Logger.info')
     @mock.patch('crmsh.bootstrap.confirm')
     def test_configure_qdevice_interactive(self, mock_confirm, mock_info, 
mock_installed, mock_prompt, mock_qdevice):
         bootstrap._context = mock.Mock(yes_to_all=False)
         mock_confirm.return_value = True
+        mock_installed.return_value = True
         mock_prompt.side_effect = ["alice@qnetd-node", 5403, "ffsplit", 
"lowest", "on", None]
         mock_qdevice_inst = mock.Mock()
         mock_qdevice.return_value = mock_qdevice_inst
@@ -1450,12 +1421,14 @@
         mock_remove_db.assert_called_once_with()
         
mock_cluster_shell_inst.get_stdout_or_raise_error.assert_called_once_with("corosync-cfgtool
 -R")
 
+    @mock.patch('crmsh.utils.this_node')
     @mock.patch('crmsh.service_manager.ServiceManager.start_service')
     @mock.patch('crmsh.qdevice.QDevice')
     @mock.patch('crmsh.corosync.get_value')
     @mock.patch('crmsh.corosync.is_qdevice_tls_on')
     @mock.patch('crmsh.log.LoggerUtils.status_long')
-    def test_start_qdevice_on_join_node(self, mock_status_long, 
mock_qdevice_tls, mock_get_value, mock_qdevice, mock_start_service):
+    def test_start_qdevice_on_join_node(self, mock_status_long, 
mock_qdevice_tls, mock_get_value, mock_qdevice, mock_start_service, 
mock_this_node):
+        mock_this_node.return_value = "node1"
         mock_qdevice_tls.return_value = True
         mock_get_value.return_value = "10.10.10.123"
         mock_qdevice_inst = mock.Mock()
@@ -1464,7 +1437,7 @@
 
         bootstrap.start_qdevice_on_join_node("node2")
 
-        mock_status_long.assert_called_once_with("Starting 
corosync-qdevice.service")
+        mock_status_long.assert_called_once_with("Starting and enable 
corosync-qdevice.service on node1")
         mock_qdevice_tls.assert_called_once_with()
         mock_get_value.assert_called_once_with("quorum.device.net.host")
         mock_qdevice.assert_called_once_with("10.10.10.123", 
cluster_node="node2")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_qdevice.py 
new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_qdevice.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_qdevice.py    
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_qdevice.py    
2026-03-09 15:27:39.000000000 +0100
@@ -13,17 +13,6 @@
 
 @mock.patch('crmsh.utils.calculate_quorate_status')
 @mock.patch('crmsh.utils.get_quorum_votes_dict')
-def test_evaluate_qdevice_quorum_effect_restart(mock_get_dict, mock_quorate):
-    mock_get_dict.return_value = {'Expected': '1', 'Total': '1'}
-    mock_quorate.return_value = False
-    res = qdevice.evaluate_qdevice_quorum_effect(qdevice.QDEVICE_ADD, False, 
False)
-    assert res == qdevice.QdevicePolicy.QDEVICE_RESTART
-    mock_get_dict.assert_called_once_with()
-    mock_quorate.assert_called_once_with(2, 1)
-
-
[email protected]('crmsh.utils.calculate_quorate_status')
[email protected]('crmsh.utils.get_quorum_votes_dict')
 def test_evaluate_qdevice_quorum_effect_reload(mock_get_dict, mock_quorate):
     mock_get_dict.return_value = {'Expected': '2', 'Total': '2'}
     mock_quorate.return_value = True
@@ -269,14 +258,6 @@
         excepted_err_string = "command /usr/bin/testst not exist"
         self.assertEqual(excepted_err_string, str(err.exception))
     
-    @mock.patch('crmsh.utils.package_is_installed')
-    def test_check_package_installed(self, mock_installed):
-        mock_installed.return_value = False
-        with self.assertRaises(ValueError) as err:
-            qdevice.QDevice.check_package_installed("corosync-qdevice")
-        excepted_err_string = "Package \"corosync-qdevice\" not installed on 
this node"
-        self.assertEqual(excepted_err_string, str(err.exception))
-
     @mock.patch('crmsh.qdevice.QDevice.check_qdevice_heuristics_mode')
     @mock.patch('crmsh.qdevice.QDevice.check_qdevice_heuristics')
     @mock.patch('crmsh.qdevice.QDevice.check_qdevice_tls')
@@ -284,69 +265,56 @@
     @mock.patch('crmsh.qdevice.QDevice.check_qdevice_algo')
     @mock.patch('crmsh.qdevice.QDevice.check_qdevice_port')
     @mock.patch('crmsh.qdevice.QDevice.check_qnetd_addr')
-    @mock.patch('crmsh.qdevice.QDevice.check_package_installed')
+    @mock.patch('crmsh.qdevice.QDevice.check_corosync_qdevice_available')
     def test_valid_qdevice_options(self, mock_installed, mock_check_qnetd, 
mock_check_port,
             mock_check_algo, mock_check_tie, mock_check_tls, mock_check_h, 
mock_check_hm):
         self.qdevice_with_ip.valid_qdevice_options()
-        mock_installed.assert_called_once_with("corosync-qdevice")
+        mock_installed.assert_called_once_with()
         mock_check_qnetd.assert_called_once_with("10.10.10.123")
 
     @mock.patch("crmsh.utils.package_is_installed")
-    def test_valid_qnetd_not_installed(self, mock_installed):
+    @mock.patch("crmsh.sh.cluster_shell")
+    def test_validate_and_start_qnetd_not_installed(self, mock_cluster_shell, 
mock_installed):
         self.qdevice_with_ip.qnetd_ip = "10.10.10.123"
         mock_installed.return_value = False
-        excepted_err_string = 'Package "corosync-qnetd" not installed on 
10.10.10.123!\nCluster service already successfully started on this node except 
qdevice service.\nIf you still want to use qdevice, install "corosync-qnetd" on 
10.10.10.123.\nThen run command "crm cluster init" with "qdevice" stage, 
like:\n  crm cluster init qdevice qdevice_related_options\nThat command will 
setup qdevice separately.'
-        self.maxDiff = None
+        excepted_err_string = 'Package "corosync-qnetd" not installed on 
10.10.10.123!\nPlease install "corosync-qnetd" on 10.10.10.123'
 
         with self.assertRaises(ValueError) as err:
-            self.qdevice_with_ip.valid_qnetd()
+            self.qdevice_with_ip.validate_and_start_qnetd()
         self.assertEqual(excepted_err_string, str(err.exception))
 
         mock_installed.assert_called_once_with("corosync-qnetd", 
remote_addr="10.10.10.123")
 
-    @mock.patch("crmsh.sh.ClusterShell.get_stdout_or_raise_error")
     @mock.patch("crmsh.qdevice.QDevice.start_qnetd")
+    @mock.patch("crmsh.qdevice.QDevice.config_qnetd_port")
     @mock.patch("crmsh.qdevice.QDevice.init_tls_certs_on_qnetd")
     @mock.patch("crmsh.utils.package_is_installed")
-    def test_valid_qnetd_duplicated_cluster_name(
+    @mock.patch("crmsh.sh.cluster_shell")
+    def test_validate_and_start_qnetd_duplicated_cluster_name(
             self,
+            mock_cluster_shell,
             mock_installed,
             mock_init_tls_certs_on_qnetd,
-            mock_start_qnetd,
-            mock_run,
+            mock_config_port,
+            mock_start_qnetd
     ):
+        mock_cluster_inst = mock.Mock()
+        mock_cluster_shell.return_value = mock_cluster_inst
+        mock_cluster_inst.get_stdout_or_raise_error.return_value = "data"
         mock_installed.return_value = True
-        mock_run.return_value = "data"
-        excepted_err_string = "This cluster's name \"cluster1\" already exists 
on qnetd server!\nPlease consider to use the different cluster-name property."
-        self.maxDiff = None
+        excepted_err_string = "This cluster's name \"cluster1\" already exists 
on qnetd server!\nPlease consider to use the different cluster-name property"
 
         with self.assertRaises(ValueError) as err:
-            self.qdevice_with_stage_cluster_name.valid_qnetd()
+            self.qdevice_with_stage_cluster_name.validate_and_start_qnetd()
         self.assertEqual(excepted_err_string, str(err.exception))
 
         mock_installed.assert_called_once_with("corosync-qnetd", 
remote_addr="10.10.10.123")
         mock_init_tls_certs_on_qnetd.assert_called_once()
-        mock_run.assert_called_once_with("corosync-qnetd-tool -l -c cluster1", 
"10.10.10.123")
-
-    @mock.patch("crmsh.service_manager.ServiceManager.enable_service")
-    def test_enable_qnetd(self, mock_enable):
-        self.qdevice_with_ip.enable_qnetd()
-        mock_enable.assert_called_once_with("corosync-qnetd.service", 
remote_addr="10.10.10.123")
-
-    @mock.patch("crmsh.service_manager.ServiceManager.disable_service")
-    def test_disable_qnetd(self, mock_disable):
-        self.qdevice_with_ip.disable_qnetd()
-        mock_disable.assert_called_once_with("corosync-qnetd.service", 
remote_addr="10.10.10.123")
 
     @mock.patch("crmsh.service_manager.ServiceManager.start_service")
     def test_start_qnetd(self, mock_start):
         self.qdevice_with_ip.start_qnetd()
-        mock_start.assert_called_once_with("corosync-qnetd.service", 
remote_addr="10.10.10.123")
-
-    @mock.patch("crmsh.service_manager.ServiceManager.stop_service")
-    def test_stop_qnetd(self, mock_stop):
-        self.qdevice_with_ip.stop_qnetd()
-        mock_stop.assert_called_once_with("corosync-qnetd.service", 
remote_addr="10.10.10.123")
+        mock_start.assert_called_once_with("corosync-qnetd.service", 
enable=True, remote_addr="10.10.10.123")
 
     @mock.patch("crmsh.parallax.parallax_call")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_qnetd", 
new_callable=mock.PropertyMock)
@@ -413,6 +381,7 @@
     def test_copy_qnetd_crt_to_cluster_one_node(self, mock_copy, 
mock_this_node, mock_list_nodes):
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com"]
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.copy_qnetd_crt_to_cluster(mock_log)
@@ -433,6 +402,7 @@
         mock_dirname.return_value = "/etc/corosync/qdevice/net/10.10.10.123"
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com", "node2.com"]
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.copy_qnetd_crt_to_cluster(mock_log)
@@ -450,6 +420,7 @@
         mock_list_nodes.return_value = ["node1", "node2"]
         mock_qnetd_cacert_local.return_value = 
"/etc/corosync/qdevice/net/10.10.10.123/qnetd-cacert.crt"
         mock_call.return_value = [("node1", (0, None, None)), ("node2", (0, 
None, None))]
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.init_db_on_cluster(mock_log)
@@ -537,6 +508,7 @@
     def test_copy_p12_to_cluster_one_node(self, mock_copy, mock_this_node, 
mock_list_nodes):
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com"]
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.copy_p12_to_cluster(mock_log)
@@ -555,6 +527,7 @@
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com", "node2.com"]
         mock_p12_on_local.return_value = 
"/etc/corosync/qdevice/net/nssdb/qdevice-net-node.p12"
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.copy_p12_to_cluster(mock_log)
@@ -570,6 +543,7 @@
     @mock.patch("crmsh.utils.list_cluster_nodes_except_me")
     def test_import_p12_on_cluster_one_node(self, mock_list_nodes, mock_call):
         mock_list_nodes.return_value = []
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.import_p12_on_cluster(mock_log)
@@ -585,6 +559,7 @@
         mock_list_nodes.return_value = ["node2", "node3"]
         mock_p12_on_local.return_value = 
"/etc/corosync/qdevice/net/nssdb/qdevice-net-node.p12"
         mock_call.return_value = [("node2", (0, None, None)), ("node3", (0, 
None, None))]
+        self.qdevice_with_ip.is_stage = True
 
         mock_log = mock.MagicMock()
         self.qdevice_with_ip.import_p12_on_cluster(mock_log)
@@ -782,27 +757,26 @@
         mock_get_value.assert_called_once_with("quorum.device.net.host")
         mock_warning.assert_called_once_with("Qdevice's vote is 0, which 
simply means Qdevice can't talk to Qnetd(qnetd-node) for various reasons.")
 
-    @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect')
+    @mock.patch('crmsh.bootstrap.sync_path')
+    @mock.patch('crmsh.corosync.conf')
+    @mock.patch('crmsh.corosync.configure_two_node')
     @mock.patch('crmsh.log.LoggerUtils.status_long')
     @mock.patch('crmsh.qdevice.QDevice.remove_qdevice_db')
-    def test_config_and_start_qdevice(self, mock_rm_db, mock_status_long, 
mock_evaluate):
+    def test_certificate_and_config_qdevice(self, mock_rm_db, mock_status_long,
+            mock_configure_two_node, mock_conf, mock_sync_path):
         mock_status_long.return_value.__enter__ = mock.Mock()
         mock_status_long.return_value.__exit__ = mock.Mock()
-        self.qdevice_with_ip.certificate_process_on_init = mock.Mock()
-        self.qdevice_with_ip.adjust_sbd_watchdog_timeout_with_qdevice = 
mock.Mock()
-        self.qdevice_with_ip.config_qnetd_port = mock.Mock()
-        self.qdevice_with_ip.config_qdevice = mock.Mock()
-        self.qdevice_with_ip.start_qdevice_service = mock.Mock()
-
-        
self.qdevice_with_ip.config_and_start_qdevice.__wrapped__(self.qdevice_with_ip)
-
-        mock_rm_db.assert_called_once_with()
-        mock_status_long.assert_called_once_with("Qdevice certification 
process")
-        
self.qdevice_with_ip.certificate_process_on_init.assert_called_once_with()
-        
self.qdevice_with_ip.adjust_sbd_watchdog_timeout_with_qdevice.assert_called_once_with()
-        self.qdevice_with_ip.config_qdevice.assert_called_once_with()
-        self.qdevice_with_ip.start_qdevice_service.assert_called_once_with()
-
+        self.qdevice_with_stage_cluster_name.certificate_process_on_init = 
mock.Mock()
+        
self.qdevice_with_stage_cluster_name.adjust_sbd_watchdog_timeout_with_qdevice = 
mock.Mock()
+        self.qdevice_with_stage_cluster_name.write_qdevice_config = mock.Mock()
+        mock_conf.return_value = "/etc/corosync/corosync.conf"
+
+        
self.qdevice_with_stage_cluster_name.certificate_and_config_qdevice.__wrapped__(self.qdevice_with_stage_cluster_name)
+
+        mock_rm_db.assert_called_once_with(is_stage=True)
+        
self.qdevice_with_stage_cluster_name.certificate_process_on_init.assert_called_once_with()
+        mock_sync_path.assert_called_once_with("/etc/corosync/corosync.conf")
+    
     @mock.patch('crmsh.utils.check_port_open')
     @mock.patch('crmsh.qdevice.ServiceManager')
     def test_config_qnetd_port_no_firewall(self, mock_service, 
mock_check_port):
@@ -840,85 +814,65 @@
     @mock.patch('crmsh.utils.set_property')
     @mock.patch('crmsh.sbd.SBDManager.update_sbd_configuration')
     @mock.patch('crmsh.sbd.SBDUtils.get_sbd_value_from_config')
-    @mock.patch('crmsh.sbd.SBDUtils.is_using_diskless_sbd')
-    def test_adjust_sbd_watchdog_timeout_with_qdevice(self, 
mock_using_diskless_sbd, mock_get_sbd_value, mock_update_config, 
mock_set_property):
-        mock_using_diskless_sbd.return_value = True
+    @mock.patch('crmsh.sbd.SBDUtils.get_sbd_device_from_config')
+    @mock.patch('crmsh.qdevice.ServiceManager')
+    def test_adjust_sbd_watchdog_timeout_with_qdevice(self, 
mock_service_manager,
+            mock_get_sbd_device, mock_get_sbd_value, mock_update_config, 
mock_set):
+        mock_service_instance = mock.Mock()
+        mock_service_manager.return_value = mock_service_instance
+        mock_service_instance.service_is_enabled.return_value = True
+        mock_get_sbd_device.return_value = []
         mock_get_sbd_value.return_value = ""
 
         
self.qdevice_with_stage_cluster_name.adjust_sbd_watchdog_timeout_with_qdevice()
 
-        mock_using_diskless_sbd.assert_called_once_with()
         mock_get_sbd_value.assert_called_once_with("SBD_WATCHDOG_TIMEOUT")
         mock_update_config.assert_called_once_with({"SBD_WATCHDOG_TIMEOUT": 
str(sbd.SBDTimeout.SBD_WATCHDOG_TIMEOUT_DEFAULT_WITH_QDEVICE)})
-        mock_set_property.assert_called_once_with("stonith-watchdog-timeout", 
2*sbd.SBDTimeout.SBD_WATCHDOG_TIMEOUT_DEFAULT_WITH_QDEVICE)
+        mock_set.assert_called_once_with("stonith-watchdog-timeout", 
2*sbd.SBDTimeout.SBD_WATCHDOG_TIMEOUT_DEFAULT_WITH_QDEVICE)
 
-    @mock.patch('crmsh.qdevice.QDevice.start_qnetd')
-    @mock.patch('crmsh.qdevice.QDevice.enable_qnetd')
+    @mock.patch('crmsh.sh.cluster_shell')
     @mock.patch('crmsh.utils.cluster_run_cmd')
     @mock.patch('logging.Logger.info')
-    def test_start_qdevice_service_reload(self, mock_status, mock_cluster_run, 
mock_enable_qnetd, mock_start_qnetd):
-        self.qdevice_with_ip.qdevice_reload_policy = 
qdevice.QdevicePolicy.QDEVICE_RELOAD
+    @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect')
+    @mock.patch('crmsh.sbd.SBDUtils.is_using_diskless_sbd')
+    def test_start_qdevice_service_reload(self, mock_is_diskless_sbd, 
mock_evaluate_qdevice_quorum_effect,
+                                          mock_status, mock_cluster_run, 
mock_cluster_shell):
+        mock_cluster_shell_instance = mock.Mock()
+        mock_cluster_shell.return_value = mock_cluster_shell_instance
+        mock_cluster_shell_instance.get_stdout_or_raise_error = mock.Mock()
+        mock_is_diskless_sbd.return_value = False
+        mock_evaluate_qdevice_quorum_effect.return_value = 
qdevice.QdevicePolicy.QDEVICE_RELOAD
 
         self.qdevice_with_ip.start_qdevice_service()
 
         mock_status.assert_has_calls([
             mock.call("Enable corosync-qdevice.service in cluster"),
+            mock.call('Reloading cluster configuration before starting 
corosync-qdevice.service'),
             mock.call("Starting corosync-qdevice.service in cluster"),
-            mock.call("Enable corosync-qnetd.service on 10.10.10.123"),
-            mock.call("Starting corosync-qnetd.service on 10.10.10.123")
             ])
         mock_cluster_run.assert_has_calls([
             mock.call("systemctl enable corosync-qdevice"),
             mock.call("systemctl restart corosync-qdevice")
             ])
-        mock_enable_qnetd.assert_called_once_with()
-        mock_start_qnetd.assert_called_once_with()
 
-    @mock.patch('crmsh.qdevice.QDevice.start_qnetd')
-    @mock.patch('crmsh.qdevice.QDevice.enable_qnetd')
-    @mock.patch('crmsh.bootstrap.wait_for_cluster')
+    @mock.patch('crmsh.bootstrap.restart_cluster')
     @mock.patch('crmsh.utils.cluster_run_cmd')
     @mock.patch('logging.Logger.info')
-    def test_start_qdevice_service_restart(self, mock_status, 
mock_cluster_run, mock_wait, mock_enable_qnetd, mock_start_qnetd):
-        self.qdevice_with_ip.qdevice_reload_policy = 
qdevice.QdevicePolicy.QDEVICE_RESTART
-
-        self.qdevice_with_ip.start_qdevice_service()
-
-        mock_status.assert_has_calls([
-            mock.call("Enable corosync-qdevice.service in cluster"),
-            mock.call("Restarting cluster service"),
-            mock.call("Enable corosync-qnetd.service on 10.10.10.123"),
-            mock.call("Starting corosync-qnetd.service on 10.10.10.123")
-            ])
-        mock_wait.assert_called_once_with()
-        mock_cluster_run.assert_has_calls([
-            mock.call("systemctl enable corosync-qdevice"),
-            mock.call("crm cluster restart")
-            ])
-        mock_enable_qnetd.assert_called_once_with()
-        mock_start_qnetd.assert_called_once_with()
-
-    @mock.patch('crmsh.qdevice.QDevice.start_qnetd')
-    @mock.patch('crmsh.qdevice.QDevice.enable_qnetd')
-    @mock.patch('logging.Logger.warning')
-    @mock.patch('crmsh.utils.cluster_run_cmd')
-    @mock.patch('logging.Logger.info')
-    def test_start_qdevice_service_warn(self, mock_status, mock_cluster_run, 
mock_warn, mock_enable_qnetd, mock_start_qnetd):
-        self.qdevice_with_ip.qdevice_reload_policy = 
qdevice.QdevicePolicy.QDEVICE_RESTART_LATER
+    @mock.patch('crmsh.qdevice.evaluate_qdevice_quorum_effect')
+    @mock.patch('crmsh.sbd.SBDUtils.is_using_diskless_sbd')
+    def test_start_qdevice_service_restart(self, mock_is_diskless_sbd, 
mock_evaluate_qdevice_quorum_effect,
+                                          mock_status, mock_cluster_run, 
mock_restart):
+        mock_is_diskless_sbd.return_value = False
+        mock_evaluate_qdevice_quorum_effect.return_value = 
qdevice.QdevicePolicy.QDEVICE_RESTART
 
         self.qdevice_with_ip.start_qdevice_service()
 
         mock_status.assert_has_calls([
             mock.call("Enable corosync-qdevice.service in cluster"),
-            mock.call("Enable corosync-qnetd.service on 10.10.10.123"),
-            mock.call("Starting corosync-qnetd.service on 10.10.10.123")
             ])
         mock_cluster_run.assert_has_calls([
             mock.call("systemctl enable corosync-qdevice"),
             ])
-        mock_warn.assert_called_once_with("To use qdevice service, need to 
restart cluster service manually on each node")
-        mock_enable_qnetd.assert_called_once_with()
-        mock_start_qnetd.assert_called_once_with()
 
     @mock.patch('crmsh.corosync.is_qdevice_configured')
     def test_remove_certification_files_on_qnetd_return(self, mock_configured):
@@ -955,20 +909,3 @@
         self.qdevice_with_invalid_cmds_relative_path.remove_qdevice_config()
         mock_parser_inst.remove.assert_called_once_with("quorum.device")
         mock_parser_inst.save.assert_called_once()
-
-    @mock.patch('crmsh.sh.cluster_shell')
-    @mock.patch('crmsh.bootstrap.sync_path')
-    @mock.patch('crmsh.corosync.configure_two_node')
-    @mock.patch('crmsh.log.LoggerUtils.status_long')
-    @mock.patch('crmsh.qdevice.QDevice.write_qdevice_config')
-    def test_config_qdevice(self, mock_write_config, mock_status_long, 
mock_config_two_node, mock_sync_file, mock_cluster_shell):
-        mock_cluster_shell_instance = mock.Mock()
-        mock_cluster_shell.return_value = mock_cluster_shell_instance
-        mock_status_long.return_value.__enter__ = mock.Mock()
-        mock_status_long.return_value.__exit__ = mock.Mock()
-        self.qdevice_with_ip.qdevice_reload_policy = 
qdevice.QdevicePolicy.QDEVICE_RELOAD
-
-        self.qdevice_with_ip.config_qdevice()
-        mock_status_long.assert_called_once_with("Update configuration")
-        mock_config_two_node.assert_called_once_with(qdevice_adding=True)
-        
mock_cluster_shell_instance.get_stdout_or_raise_error.assert_called_once_with("corosync-cfgtool
 -R")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_ui_sbd.py 
new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_ui_sbd.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_ui_sbd.py     
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_ui_sbd.py     
2026-03-09 15:27:39.000000000 +0100
@@ -155,7 +155,7 @@
         mock_is_using_disk_based_sbd.return_value = True
         timeout_usage_str = " ".join([f"[{t}-timeout=<integer>]" for t in 
ui_sbd.SBD.DISKBASED_TIMEOUT_TYPES])
         show_usage = f"crm sbd configure show 
[{'|'.join(ui_sbd.SBD.SHOW_TYPES)}]"
-        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device>]\n"
+        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device|driver>]\n"
         self.assertEqual(self.sbd_instance_diskbased.configure_usage, expected)
         mock_is_using_disk_based_sbd.assert_called_once()
         mock_is_using_diskless_sbd.assert_not_called()
@@ -167,7 +167,7 @@
         mock_is_using_diskless_sbd.return_value = True
         timeout_usage_str = " ".join([f"[{t}-timeout=<integer>]" for t in 
ui_sbd.SBD.DISKLESS_TIMEOUT_TYPES])
         show_usage = f"crm sbd configure show 
[{'|'.join(ui_sbd.SBD.DISKLESS_SHOW_TYPES)}]"
-        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device>]\n"
+        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device|driver>]\n"
         self.assertEqual(self.sbd_instance_diskless.configure_usage, expected)
         mock_is_using_disk_based_sbd.assert_called_once()
         mock_is_using_diskless_sbd.assert_called_once()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_watchdog.py 
new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_watchdog.py
--- old/crmsh-5.0.0+20260226.8b99a4c5/test/unittests/test_watchdog.py   
2026-02-26 10:31:40.000000000 +0100
+++ new/crmsh-5.0.0+20260309.5a3c6578/test/unittests/test_watchdog.py   
2026-03-09 15:27:39.000000000 +0100
@@ -287,7 +287,7 @@
 
         mock_valid.assert_called_once_with("test")
         mock_run.assert_called_once_with("modinfo test")
-        mock_error.assert_called_once_with("Should provide valid watchdog 
device or driver name by -w option")
+        mock_error.assert_called_once_with("Should provide valid watchdog 
device or driver name")
 
     @mock.patch('crmsh.watchdog.Watchdog._get_device_through_driver')
     @mock.patch('crmsh.watchdog.Watchdog._load_watchdog_driver')

Reply via email to