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-04-28 11:57:23
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Tue Apr 28 11:57:23 2026 rev:406 rq:1349510 version:5.0.0+20260427.5d7e4bd6

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2026-04-21 
12:47:12.588370388 +0200
+++ /work/SRC/openSUSE:Factory/.crmsh.new.11940/crmsh.changes   2026-04-28 
12:01:18.185808642 +0200
@@ -1,0 +2,29 @@
+Mon Apr 27 07:48:19 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260427.5d7e4bd6:
+  * Dev: log: Sync the file formatter for NO_COLOR_FORMATTERS
+
+-------------------------------------------------------------------
+Mon Apr 27 04:17:40 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260427.d586b0f4:
+  * Dev: unittests: Adjust unit test for previous commit
+  * Dev: ui_cluster: Add --now option for crm cluster enable/disable commands
+
+-------------------------------------------------------------------
+Fri Apr 24 06:50:33 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260424.abe22392:
+  * Dev: unittests: Adjust unit test for previous commit
+  * Dev: doc: Adjust crm.8.adoc about -F/--force option
+  * Dev: ui_sbd: Add -F/--force option for crm sbd configure/device/purge 
commands
+  * Dev: behave: Adjust functional test for previous commit
+  * Dev: ui_cluster: Add -F/--force option for 'crm cluster init' command
+
+-------------------------------------------------------------------
+Thu Apr 23 06:36:25 UTC 2026 - [email protected]
+
+- Update to version 5.0.0+20260423.d501c2ea:
+  * Dev: qdevice: Improve local address check for qnetd (bsc#1262094)
+
+-------------------------------------------------------------------

Old:
----
  crmsh-5.0.0+20260420.d13e03ac.tar.bz2

New:
----
  crmsh-5.0.0+20260427.5d7e4bd6.tar.bz2

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

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.T9dGYf/_old  2026-04-28 12:01:18.957840612 +0200
+++ /var/tmp/diff_new_pack.T9dGYf/_new  2026-04-28 12:01:18.961840778 +0200
@@ -41,7 +41,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        5.0.0+20260420.d13e03ac
+Version:        5.0.0+20260427.5d7e4bd6
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.T9dGYf/_old  2026-04-28 12:01:19.021843262 +0200
+++ /var/tmp/diff_new_pack.T9dGYf/_new  2026-04-28 12:01:19.025843429 +0200
@@ -9,7 +9,7 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">d13e03ac0144d910af7fdfcf06faffe71cfdffe8</param>
+  <param 
name="changesrevision">5d7e4bd6fb3ad447a1526a65c096f02563e4fb3e</param>
 </service>
 </servicedata>
 (No newline at EOF)

++++++ crmsh-5.0.0+20260420.d13e03ac.tar.bz2 -> 
crmsh-5.0.0+20260427.5d7e4bd6.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/constants.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/constants.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/constants.py        2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/constants.py        2026-04-27 
09:16:52.000000000 +0200
@@ -434,6 +434,7 @@
 If a nic name is provided, the first IP of that nic will be used. 
 Use multiple -i for more links. Note: Only one link is allowed for the non 
knet transport type
 """
+FORCE_HELP = "Make `crm` proceed with applying changes where it would normally 
ask the user to confirm before proceeding, or the operation risks interfering 
HA protection. This option is intended primarily for scripts and must be used 
with care."
 
 COROSYNC_STATUS_TYPES = ("ring", "quorum", "qdevice", "qnetd", "cpg")
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/log.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/log.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/log.py      2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/log.py      2026-04-27 
09:16:52.000000000 +0200
@@ -203,6 +203,13 @@
         return f"{s}.{int(record.msecs):03d}{tz}"
 
 
+LOGFILE_FORMATTER = {
+    "()": ISO8601Formatter,
+    "format": "%(asctime)s {} %(name)s: %(levelname)s: 
%(message)s".format(socket.gethostname()),
+    "datefmt": "%Y-%m-%dT%H:%M:%S",
+}
+
+
 LOGGING_CFG = {
     "version": 1,
     "disable_existing_loggers": "False",
@@ -223,11 +230,7 @@
                 DEBUG2: "%(levelname)s: %(funcName)s %(message)s",
             },
         },
-        "file": {
-            "()": ISO8601Formatter,
-            "format": "%(asctime)s {} %(name)s: %(levelname)s: 
%(message)s".format(socket.gethostname()),
-            "datefmt": "%Y-%m-%dT%H:%M:%S",
-        }
+        "file": LOGFILE_FORMATTER
     },
     "filters": {
         "filter": {
@@ -298,10 +301,7 @@
             DEBUG2: "%(levelname)s: %(funcName)s %(message)s",
         },
     },
-    "file": {
-        "format": "%(asctime)s {} %(name)s: %(levelname)s: 
%(message)s".format(socket.gethostname()),
-        "datefmt": "%b %d %H:%M:%S",
-    }
+    "file": LOGFILE_FORMATTER
 }
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/main.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/main.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/main.py     2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/main.py     2026-04-27 
09:16:52.000000000 +0200
@@ -113,9 +113,7 @@
                         "The default is color if the terminal emulation 
supports colors, " +
                         "else plain.")
     parser.add_argument("-F", "--force", action="store_true", default=False, 
dest="force",
-                        help="Make crm proceed with applying changes where it 
would normally " +
-                        "ask the user to confirm before proceeding. This 
option is mainly useful " +
-                        "in scripts, and should be used with care.")
+                        help=constants.FORCE_HELP)
     parser.add_argument("-n", "--no", action="store_true", default=False, 
dest="ask_no",
                         help="Automatically answer no when prompted")
     parser.add_argument("-w", "--wait", action="store_true", default=False, 
dest="wait",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/qdevice.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/qdevice.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/qdevice.py  2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/qdevice.py  2026-04-27 
09:16:52.000000000 +0200
@@ -1,3 +1,5 @@
+import ipaddress
+import json
 import os
 import re
 import socket
@@ -6,7 +8,7 @@
 from enum import Enum
 
 import crmsh.parallax
-from . import sh
+from . import sh, iproute2
 from . import utils
 from . import parallax
 from . import corosync
@@ -185,22 +187,25 @@
 
     @staticmethod
     def check_qnetd_addr(qnetd_addr):
-        qnetd_ip = None
-        try:
-            # socket.getaddrinfo works for both ipv4 and ipv6 address
-            # The function returns a list of 5-tuples with the following 
structure:
-            # (family, type, proto, canonname, sockaddr)
-            # sockaddr is a tuple describing a socket address, whose format 
depends on the returned family
-            # (a (address, port) 2-tuple for AF_INET, a (address, port, flow 
info, scope id) 4-tuple for AF_INET6)
-            res = socket.getaddrinfo(qnetd_addr, None)
-            qnetd_ip = res[0][-1][0]
-        except socket.error:
-            raise ValueError("host \"{}\" is unreachable".format(qnetd_addr))
-
         utils.ssh_port_reachable_check(qnetd_addr)
+        try:
+            qnetd_ip_addresses = [
+                ipaddress.ip_address(sockaddr[0])
+                for af, tpe, proto, canonname, sockaddr in 
socket.getaddrinfo(qnetd_addr, None)
+            ]
+        except socket.error as e:
+            raise ValueError(f"{e}: {qnetd_addr}")
+        try:
+            local_interfaces = iproute2.IPAddr(json.loads(
+                sh.LocalShell().get_stdout_or_raise_error(None, 'ip -j addr 
show')
+            ))
+        except ValueError:
+            return
+        local_ip_addresses = set(addr_info.ip for interface in 
local_interfaces.interfaces() for addr_info in interface.addr_info)
+        for ip_addr in qnetd_ip_addresses:
+            if ip_addr in local_ip_addresses:
+                raise ValueError("host for qnetd must be a remote one")
 
-        if utils.InterfacesInfo.ip_in_local(qnetd_ip):
-            raise ValueError("host for qnetd must be a remote one")
 
     @staticmethod
     def check_qdevice_port(qdevice_port):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/crmsh/service_manager.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/service_manager.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/service_manager.py  2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/service_manager.py  2026-04-27 
09:16:52.000000000 +0200
@@ -81,18 +81,20 @@
             cmd = "systemctl stop '{}'".format(name)
         return self._call(remote_addr, node_list, cmd)
 
-    def enable_service(self, name, remote_addr=None, node_list=[]):
+    def enable_service(self, name, remote_addr=None, node_list=[], now=False):
         """
         Enable service
         Return success node list
         """
-        cmd = "systemctl enable '{}'".format(name)
+        now_flag = "--now " if now else ""
+        cmd = f"systemctl {now_flag}enable {name}"
         return self._call(remote_addr, node_list, cmd)
 
-    def disable_service(self, name, remote_addr=None, node_list=[]):
+    def disable_service(self, name, remote_addr=None, node_list=[], now=False):
         """
         Disable service
         Return success node list
         """
-        cmd = "systemctl disable '{}'".format(name)
+        now_flag = "--now " if now else ""
+        cmd = f"systemctl {now_flag}disable {name}"
         return self._call(remote_addr, node_list, cmd)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_cluster.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_cluster.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_cluster.py       2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_cluster.py       2026-04-27 
09:16:52.000000000 +0200
@@ -32,6 +32,7 @@
 from . import sbd
 from . import log
 from .utils import TerminateSubCommand
+import crmsh.options
 
 logger = log.setup_logger(__name__)
 
@@ -168,7 +169,7 @@
         Starts the cluster stack on all nodes or specific node(s)
         '''
         try:
-            node_list = ui_utils.parse_and_validate_node_args("start", *args)
+            node_list, _ = ui_utils.parse_and_validate_node_args("start", 
*args)
         except utils.NoSSHError as msg:
             logger.error('%s', msg)
             logger.info("Please try 'crm cluster start' on each node")
@@ -251,7 +252,7 @@
         Stops the cluster stack on all nodes or specific node(s)
         '''
         try:
-            node_list = ui_utils.parse_and_validate_node_args("stop", *args)
+            node_list, _ = ui_utils.parse_and_validate_node_args("stop", *args)
         except utils.NoSSHError as msg:
             logger.error('%s', msg)
             logger.info("Please try 'crm cluster stop' on each node")
@@ -304,25 +305,29 @@
         '''
         Enable the cluster services on this node
         '''
-        node_list = ui_utils.parse_and_validate_node_args("enable", *args)
+        node_list, now_flag = ui_utils.parse_and_validate_node_args("enable", 
*args)
         service_manager = ServiceManager()
-        node_list = service_manager.enable_service("pacemaker.service", 
node_list=node_list)
+        node_list = service_manager.enable_service("pacemaker.service", 
node_list=node_list, now=now_flag)
         if service_manager.service_is_available("corosync-qdevice.service") 
and corosync.is_qdevice_configured():
-            service_manager.enable_service("corosync-qdevice.service", 
node_list=node_list)
+            service_manager.enable_service("corosync-qdevice.service", 
node_list=node_list, now=now_flag)
+
+        action_msg = "enabled and started" if now_flag else "enabled"
         for node in node_list:
-            logger.info("Cluster services enabled on %s", node)
+            logger.info("Cluster services %s on %s", action_msg, node)
 
     @command.skill_level('administrator')
     def do_disable(self, context, *args):
         '''
         Disable the cluster services on this node
         '''
-        node_list = ui_utils.parse_and_validate_node_args("disable", *args)
+        node_list, now_flag = ui_utils.parse_and_validate_node_args("disable", 
*args)
         service_manager = ServiceManager()
-        node_list = service_manager.disable_service("pacemaker.service", 
node_list=node_list)
-        service_manager.disable_service("corosync-qdevice.service", 
node_list=node_list)
+        node_list = service_manager.disable_service("pacemaker.service", 
node_list=node_list, now=now_flag)
+        service_manager.disable_service("corosync-qdevice.service", 
node_list=node_list, now=now_flag)
+
+        action_msg = "stopped and disabled" if now_flag else "disabled"
         for node in node_list:
-            logger.info("Cluster services disabled on %s", node)
+            logger.info("Cluster services %s on %s", action_msg, node)
 
     def _args_implicit(self, context, args, name):
         '''
@@ -434,6 +439,8 @@
                             help="Skip csync2 initialization (default, 
deprecated)")
         parser.add_argument('--use-ssh-agent', 
action=argparse.BooleanOptionalAction, dest='use_ssh_agent', default=True,
                             help="Try to use an existing key from ssh-agent 
(default)")
+        parser.add_argument("-F", "--force", dest="force", 
action="store_true", default=False,
+                            help=constants.FORCE_HELP)
 
         network_group = parser.add_argument_group("Network configuration", 
"Options for configuring the network and messaging layer.")
         network_group.add_argument("-i", "--interface", dest="nic_addr_list", 
metavar="IF", action=CustomAppendAction, default=[], 
help=constants.INTERFACE_HELP)
@@ -476,6 +483,8 @@
         if options is None or args is None:
             return
 
+        crmsh.options.force |= options.force
+
         stage = ""
         if len(args):
             stage = args[0]
@@ -582,6 +591,8 @@
         if options is None or args is None:
             return
 
+        crmsh.options.force |= options.force
+
         if options.cluster_node is not None and options.cluster_node not in 
args:
             args = list(args) + [options.cluster_node]
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_node.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_node.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_node.py  2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_node.py  2026-04-27 
09:16:52.000000000 +0200
@@ -301,7 +301,7 @@
             args = args[:-1]
 
         # Parse node option
-        node_list = ui_utils.parse_and_validate_node_args("standby", *args)
+        node_list, _ = ui_utils.parse_and_validate_node_args("standby", *args)
         if not node_list:
             return
 
@@ -389,7 +389,7 @@
         To avoid race condition for --all option, melt all online values into 
one cib replace session
         """
         # Parse node option
-        node_list = ui_utils.parse_and_validate_node_args("online", *args)
+        node_list, _ = ui_utils.parse_and_validate_node_args("online", *args)
         if not node_list:
             return
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_sbd.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_sbd.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_sbd.py   2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_sbd.py   2026-04-27 
09:16:52.000000000 +0200
@@ -12,6 +12,7 @@
 from crmsh import sh
 from crmsh import xmlutil
 from crmsh import constants
+from crmsh import options
 from crmsh.service_manager import ServiceManager
 
 
@@ -77,6 +78,15 @@
     return parameters_pool
 
 
+def _handle_force_option(args):
+    _force = False
+    if args and args[-1] in ("-F", "--force"):
+        _force = True
+        args = args[:-1]
+    options.force |= _force
+    return args
+
+
 class SBD(command.UI):
     '''
     Class for sbd sub-level
@@ -179,7 +189,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|driver>]\n"
+        return f"Usage:\n{show_usage}\ncrm sbd configure {timeout_usage_str} 
[watchdog-device=<device|driver>] [-F|--force]\n"
 
     def _show_sysconfig(self) -> None:
         '''
@@ -506,6 +516,7 @@
             logger.info("Please use 'crm cluster init sbd -s <dev1> [-s <dev2> 
[-s <dev3>]]' to configure the disk-based SBD first")
             return False
 
+        args = _handle_force_option(args)
         try:
             if not args:
                 raise self.SyntaxError("No argument")
@@ -538,6 +549,7 @@
         '''
         Implement sbd configure command
         '''
+        args = _handle_force_option(args)
         try:
             self._load_attributes()
             if not args:
@@ -602,6 +614,7 @@
         if not self._service_is_active(constants.SBD_SERVICE):
             return False
 
+        args = _handle_force_option(args)
         purge_crashdump = False
         if args:
             if args[0] == "crashdump":
@@ -611,7 +624,7 @@
                 purge_crashdump = True
             else:
                 logger.error("Invalid argument: %s", ' '.join(args))
-                logger.info("Usage: crm sbd purge [crashdump]")
+                logger.info("Usage: crm sbd purge [crashdump] [-F|--force]")
                 return False
 
         msg = "purging SBD crashdump" if purge_crashdump else "purging SBD"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_utils.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_utils.py
--- old/crmsh-5.0.0+20260420.d13e03ac/crmsh/ui_utils.py 2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/crmsh/ui_utils.py 2026-04-27 
09:16:52.000000000 +0200
@@ -165,10 +165,10 @@
                          (mknamed(), max_args-nskip, len(args)-nskip))
 
 
-def parse_and_validate_node_args(command_name, *args) -> list:
+def parse_and_validate_node_args(command_name, *args) -> tuple[list[str], 
bool]:
     '''
     Parses option for node-related commands
-    Then validates and returns the reachable node list
+    Then validates and returns the reachable node list and now flag
     '''
     action_target = "node" if command_name in ["standby", "online"] else 
"cluster service"
     action = f"{command_name} {action_target}"
@@ -193,6 +193,9 @@
     )
     parser.add_argument("-h", "--help", action="store_true", dest="help", 
help="Show this help message")
     parser.add_argument("--all", help=f"To {action} on all nodes", 
action="store_true", dest="all")
+    if command_name in ["enable", "disable"]:
+        help_str = "start" if command_name == "enable" else "stop"
+        parser.add_argument("--now", action="store_true", dest="now", 
help=f"To {help_str} cluster service immediately")
 
     options, args = parser.parse_known_args(args)
     if options.help:
@@ -204,4 +207,6 @@
         raise ValueError("Should either use --all or specific node(s)")
 
     include_remote = command_name in ["standby", "online"]
-    return utils.validate_and_get_reachable_nodes(args, options.all, 
include_remote)
+    nodes = utils.validate_and_get_reachable_nodes(args, options.all, 
include_remote)
+
+    return nodes, getattr(options, "now", False)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-5.0.0+20260420.d13e03ac/doc/crm.8.adoc 
new/crmsh-5.0.0+20260427.5d7e4bd6/doc/crm.8.adoc
--- old/crmsh-5.0.0+20260420.d13e03ac/doc/crm.8.adoc    2026-04-20 
08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/doc/crm.8.adoc    2026-04-27 
09:16:52.000000000 +0200
@@ -67,8 +67,9 @@
 
 *-F, --force*::
     Make `crm` proceed with applying changes where it would normally
-    ask the user to confirm before proceeding. This option is mainly
-    useful in scripts, and should be used with care.
+    ask the user to confirm before proceeding, or the operation risks
+    interfering HA protection. This option is intended primarily for
+    scripts and must be used with care
 
 *-w, --wait*::
     Make `crm` wait for the cluster transition to finish (for the
@@ -2235,11 +2236,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|driver>]
+crm sbd configure [watchdog-timeout=<integer>] [allocate-timeout=<integer>] 
[loop-timeout=<integer>] [msgwait-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device|driver>] 
[-F|--force]
 
 # For disk-less SBD
 crm sbd configure show [sysconfig|property]
-crm sbd configure [watchdog-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device|driver>]
+crm sbd configure [watchdog-timeout=<integer>] 
[crashdump-watchdog-timeout=<integer>] [watchdog-device=<device|driver>] 
[-F|--force]
 ...............
 
 example:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/test/features/steps/const.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/test/features/steps/const.py
--- old/crmsh-5.0.0+20260420.d13e03ac/test/features/steps/const.py      
2026-04-20 08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/test/features/steps/const.py      
2026-04-27 09:16:52.000000000 +0200
@@ -25,10 +25,11 @@
                         Choose one of the output options: plain, color-always,
                         color, or uppercase. The default is color if the
                         terminal emulation supports colors, else plain.
-  -F, --force           Make crm proceed with applying changes where it would
-                        normally ask the user to confirm before proceeding.
-                        This option is mainly useful in scripts, and should be
-                        used with care.
+  -F, --force           Make `crm` proceed with applying changes where it
+                        would normally ask the user to confirm before
+                        proceeding, or the operation risks interfering HA
+                        protection. This option is intended primarily for
+                        scripts and must be used with care.
   -n, --no              Automatically answer no when prompted
   -w, --wait            Make crm wait for the cluster transition to finish
                         (for the changes to take effect) after each processed
@@ -82,6 +83,11 @@
                         Skip csync2 initialization (default, deprecated)
   --use-ssh-agent, --no-use-ssh-agent
                         Try to use an existing key from ssh-agent (default)
+  -F, --force           Make `crm` proceed with applying changes where it
+                        would normally ask the user to confirm before
+                        proceeding, or the operation risks interfering HA
+                        protection. This option is intended primarily for
+                        scripts and must be used with care.
 
 Network configuration:
   Options for configuring the network and messaging layer.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_qdevice.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_qdevice.py
--- old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_qdevice.py    
2026-04-20 08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_qdevice.py    
2026-04-27 09:16:52.000000000 +0200
@@ -1,5 +1,6 @@
 import unittest
 import socket
+import json
 
 try:
     from unittest import mock
@@ -184,25 +185,46 @@
         res = self.qdevice_with_ip.qdevice_p12_on_local
         self.assertEqual(res, 
"/etc/corosync/qdevice/net/nssdb/qdevice-net-node.p12")
 
-    @mock.patch('crmsh.utils.InterfacesInfo.ip_in_local')
+    @mock.patch('crmsh.sh.LocalShell')
     @mock.patch('crmsh.utils.ssh_port_reachable_check')
     @mock.patch('socket.getaddrinfo')
-    def test_check_qnetd_addr_local(self, mock_getaddrinfo, mock_reachable, 
mock_in_local):
-        mock_getaddrinfo.return_value = [(None, ("10.10.10.123",)),]
-        mock_in_local.return_value = True
+    def test_check_qnetd_addr_local(self, mock_getaddrinfo, mock_reachable, 
mock_shell):
+        mock_getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 
socket.IPPROTO_TCP, '', ("10.10.10.123", 0)),]
+        mock_shell.return_value.get_stdout_or_raise_error.return_value = 
json.dumps([
+            {
+                "ifname": "eth0",
+                "flags": ["UP", "LOWER_UP"],
+                "addr_info": [{"local": "10.10.10.123", "prefixlen": 24}]
+            }
+        ])
         with self.assertRaises(ValueError) as err:
             qdevice.QDevice.check_qnetd_addr("qnetd-node")
         excepted_err_string = "host for qnetd must be a remote one"
         self.assertEqual(excepted_err_string, str(err.exception))
 
+    @mock.patch('crmsh.utils.ssh_port_reachable_check')
     @mock.patch('socket.getaddrinfo')
-    def test_check_qnetd_addr(self, mock_getaddrinfo):
-        mock_getaddrinfo.side_effect = socket.error
+    def test_check_qnetd_addr(self, mock_getaddrinfo, mock_reachable):
+        mock_getaddrinfo.side_effect = socket.error("getaddrinfo failed")
         with self.assertRaises(ValueError) as err:
             qdevice.QDevice.check_qnetd_addr("qnetd-node")
-        excepted_err_string = "host \"qnetd-node\" is unreachable"
+        excepted_err_string = "getaddrinfo failed: qnetd-node"
         self.assertEqual(excepted_err_string, str(err.exception))
 
+    @mock.patch('crmsh.sh.LocalShell')
+    @mock.patch('crmsh.utils.ssh_port_reachable_check')
+    @mock.patch('socket.getaddrinfo')
+    def test_check_qnetd_addr_success(self, mock_getaddrinfo, mock_reachable, 
mock_shell):
+        mock_getaddrinfo.return_value = [(socket.AF_INET, socket.SOCK_STREAM, 
socket.IPPROTO_TCP, '', ("10.10.10.124", 0)),]
+        mock_shell.return_value.get_stdout_or_raise_error.return_value = 
json.dumps([
+            {
+                "ifname": "eth0",
+                "flags": ["UP", "LOWER_UP"],
+                "addr_info": [{"local": "10.10.10.123", "prefixlen": 24}]
+            }
+        ])
+        qdevice.QDevice.check_qnetd_addr("qnetd-node")
+
     @mock.patch('crmsh.utils.valid_port')
     def test_check_qdevice_port(self, mock_port):
         mock_port.return_value = False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_service_manager.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_service_manager.py
--- old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_service_manager.py    
2026-04-20 08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_service_manager.py    
2026-04-27 09:16:52.000000000 +0200
@@ -76,9 +76,9 @@
     def test_enable_service(self, mock_call_with_parallax: mock.MagicMock):
         self.service_manager._call.return_value = ['node1']
         self.assertEqual(['node1'], 
self.service_manager.enable_service('service1', remote_addr='node1'))
-        self.service_manager._call.assert_called_once_with('node1', [], 
"systemctl enable 'service1'")
+        self.service_manager._call.assert_called_once_with('node1', [], 
"systemctl enable service1")
 
     def test_disable_service(self, mock_call_with_parallax: mock.MagicMock):
         self.service_manager._call.return_value = ['node1']
         self.assertEqual(['node1'], 
self.service_manager.disable_service('service1', remote_addr='node1'))
-        self.service_manager._call.assert_called_once_with('node1', [], 
"systemctl disable 'service1'")
+        self.service_manager._call.assert_called_once_with('node1', [], 
"systemctl disable service1")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_ui_cluster.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_ui_cluster.py
--- old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_ui_cluster.py 
2026-04-20 08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_ui_cluster.py 
2026-04-27 09:16:52.000000000 +0200
@@ -43,7 +43,7 @@
     def test_do_start_already_started(self, mock_qdevice_configured, 
mock_parse_nodes, mock_active, mock_info):
         mock_qdevice_configured.return_value = False
         context_inst = mock.Mock()
-        mock_parse_nodes.return_value = ["node1", "node2"]
+        mock_parse_nodes.return_value = ["node1", "node2"], False
         mock_active.side_effect = [True, True]
         self.ui_cluster_inst.do_start(context_inst, "node1", "node2")
         mock_parse_nodes.assert_called_once_with("start", "node1", "node2")
@@ -67,7 +67,7 @@
     def test_do_start(self, mock_parse_nodes, mock_active, mock_start, 
mock_qdevice_configured, mock_info, mock_error, mock_start_pacemaker, 
mock_check_qdevice):
         context_inst = mock.Mock()
         mock_start_pacemaker.return_value = ["node1"]
-        mock_parse_nodes.return_value = ["node1", "node2"]
+        mock_parse_nodes.return_value = ["node1", "node2"], False
         mock_active.side_effect = [False, False, False, False]
         mock_qdevice_configured.return_value = True
 
@@ -88,7 +88,7 @@
     @mock.patch('crmsh.ui_cluster.Cluster._node_ready_to_stop_cluster_service')
     @mock.patch('crmsh.ui_utils.parse_and_validate_node_args')
     def test_do_stop_return(self, mock_parse_nodes, 
mock_node_ready_to_stop_cluster_service, mock_dc):
-        mock_parse_nodes.return_value = ["node1", "node2"]
+        mock_parse_nodes.return_value = ["node1", "node2"], False
         mock_node_ready_to_stop_cluster_service.side_effect = [False, False]
 
         context_inst = mock.Mock()
@@ -108,7 +108,7 @@
     @mock.patch('crmsh.ui_utils.parse_and_validate_node_args')
     def test_do_stop(self, mock_parse_nodes, 
mock_node_ready_to_stop_cluster_service, mock_dc,
                      mock_set_dlm, mock_service_manager, mock_info, 
mock_debug, mock_is_in_maintenance_mode):
-        mock_parse_nodes.return_value = ["node1", "node2"]
+        mock_parse_nodes.return_value = ["node1", "node2"], False
         mock_node_ready_to_stop_cluster_service.side_effect = [True, False]
         mock_service_manager_inst = mock.Mock()
         mock_service_manager.return_value = mock_service_manager_inst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_ui_sbd.py 
new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_ui_sbd.py
--- old/crmsh-5.0.0+20260420.d13e03ac/test/unittests/test_ui_sbd.py     
2026-04-20 08:35:08.000000000 +0200
+++ new/crmsh-5.0.0+20260427.5d7e4bd6/test/unittests/test_ui_sbd.py     
2026-04-27 09:16:52.000000000 +0200
@@ -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|driver>]\n"
+        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device|driver>] [-F|--force]\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|driver>]\n"
+        expected = f"Usage:\n{show_usage}\ncrm sbd configure 
{timeout_usage_str} [watchdog-device=<device|driver>] [-F|--force]\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()

Reply via email to