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')
