diff --git a/python/pacemaker/_cts/CTS.py b/python/pacemaker/_cts/CTS.py index 607b2e98e2a..c83b3d705f2 100644 --- a/python/pacemaker/_cts/CTS.py +++ b/python/pacemaker/_cts/CTS.py @@ -12,7 +12,7 @@ from pacemaker._cts.environment import EnvFactory from pacemaker._cts.input import should_continue from pacemaker._cts import logging -from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.remote import RemoteExec class CtsLab: @@ -60,9 +60,9 @@ def __contains__(self, key): def __getitem__(self, key): """Return the given environment key, or raise KeyError if it does not exist.""" # Throughout this file, pylint has trouble understanding that EnvFactory - # and RemoteFactory are singleton instances that can be treated as callable - # and subscriptable objects. Various warnings are disabled because of this. - # See also a comment about self._rsh in environment.py. + # is a singleton instance that can be treated as a callable and subscriptable + # object. Various warnings are disabled because of this. See also a comment + # about self._rsh in environment.py. # pylint: disable=unsubscriptable-object return self._env[key] @@ -133,14 +133,12 @@ def __init__(self, env): def _node_booted(self, node): """Return True if the given node is booted (responds to pings).""" - # pylint: disable=not-callable - (rc, _) = RemoteFactory().getInstance()("localhost", f"ping -nq -c1 -w1 {node}", verbose=0) + (rc, _) = RemoteExec().call("localhost", f"ping -nq -c1 -w1 {node}", verbose=0) return rc == 0 def _sshd_up(self, node): """Return true if sshd responds on the given node.""" - # pylint: disable=not-callable - (rc, _) = RemoteFactory().getInstance()(node, "true", verbose=0) + (rc, _) = RemoteExec().call(node, "true", verbose=0) return rc == 0 def wait_for_node(self, node, timeout=300): @@ -225,7 +223,7 @@ def signal(self, sig, node): search_re = f"({word_begin}valgrind )?.*{word_begin}{self.name}{word_end}" if sig in ["SIGKILL", "KILL", 9, "SIGTERM", "TERM", 15]: - (rc, _) = self._cm.rsh(node, f"pgrep --full '{search_re}'") + (rc, _) = self._cm.rsh.call(node, f"pgrep --full '{search_re}'") if rc == 1: # No matching process, so nothing to kill/terminate return @@ -237,6 +235,6 @@ def signal(self, sig, node): # 0: One or more processes were successfully signaled. # 1: No processes matched or none of them could be signalled. # This is why we check for no matching process above. - (rc, _) = self._cm.rsh(node, f"pkill --signal {sig} --full '{search_re}'") + (rc, _) = self._cm.rsh.call(node, f"pkill --signal {sig} --full '{search_re}'") if rc != 0: self._cm.log(f"ERROR: Sending signal {sig} to {self.name} failed on node {node}") diff --git a/python/pacemaker/_cts/audits.py b/python/pacemaker/_cts/audits.py index 95165d48d06..489e073e196 100644 --- a/python/pacemaker/_cts/audits.py +++ b/python/pacemaker/_cts/audits.py @@ -1,7 +1,7 @@ """Auditing classes for Pacemaker's Cluster Test Suite (CTS).""" __all__ = ["AuditConstraint", "AuditResource", "ClusterAudit", "audit_list"] -__copyright__ = "Copyright 2000-2025 the Pacemaker project contributors" +__copyright__ = "Copyright 2000-2026 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" import re @@ -82,16 +82,16 @@ def _restart_cluster_logging(self, nodes=None): for node in nodes: if self._cm.env["have_systemd"]: - (rc, _) = self._cm.rsh(node, "systemctl stop systemd-journald.socket") + (rc, _) = self._cm.rsh.call(node, "systemctl stop systemd-journald.socket") if rc != 0: self._cm.log(f"ERROR: Cannot stop 'systemd-journald' on {node}") - (rc, _) = self._cm.rsh(node, "systemctl start systemd-journald.service") + (rc, _) = self._cm.rsh.call(node, "systemctl start systemd-journald.service") if rc != 0: self._cm.log(f"ERROR: Cannot start 'systemd-journald' on {node}") if "syslogd" in self._cm.env: - (rc, _) = self._cm.rsh(node, f"service {self._cm.env['syslogd']} restart") + (rc, _) = self._cm.rsh.call(node, f"service {self._cm.env['syslogd']} restart") if rc != 0: self._cm.log(f"""ERROR: Cannot restart '{self._cm.env["syslogd"]}' on {node}""") @@ -136,10 +136,7 @@ def _test_logging(self): for node in self._cm.env["nodes"]: cmd = f"logger -p {self._cm.env['syslog_facility']}.info {prefix} {node} {suffix}" - - (rc, _) = self._cm.rsh(node, cmd, synchronous=False, verbose=0) - if rc != 0: - self._cm.log(f"ERROR: Cannot execute remote command [{cmd}] on {node}") + self._cm.rsh.call_async(node, cmd) for k, w in watch.items(): if watch_pref is None: @@ -213,7 +210,7 @@ def __call__(self): self._cm.ns.wait_for_all_nodes(self._cm.env["nodes"]) for node in self._cm.env["nodes"]: - (_, dfout) = self._cm.rsh(node, dfcmd, verbose=1) + (_, dfout) = self._cm.rsh.call(node, dfcmd, verbose=1) if not dfout: self._cm.log(f"ERROR: Cannot execute remote df command [{dfcmd}] on {node}") continue @@ -282,13 +279,13 @@ def _output_has_core(self, output, node): def _find_core_with_coredumpctl(self, node): """Use coredumpctl to find core dumps on the given node.""" - (_, lsout) = self._cm.rsh(node, "coredumpctl --no-legend --no-pager") + (_, lsout) = self._cm.rsh.call(node, "coredumpctl --no-legend --no-pager") return self._output_has_core(lsout, node) def _find_core_on_fs(self, node, paths): """Check for core dumps on the given node, under any of the given paths.""" - (_, lsout) = self._cm.rsh(node, f"ls -al {' '.join(paths)} | grep core.[0-9]", - verbose=1) + (_, lsout) = self._cm.rsh.call(node, f"ls -al {' '.join(paths)} | grep core.[0-9]", + verbose=1) return self._output_has_core(lsout, node) def __call__(self): @@ -320,7 +317,7 @@ def __call__(self): if self._cm.expected_status.get(node) == "down": clean = False - (_, lsout) = self._cm.rsh(node, "ls -al /dev/shm | grep qb-", verbose=1) + (_, lsout) = self._cm.rsh.call(node, "ls -al /dev/shm | grep qb-", verbose=1) for line in lsout: passed = False @@ -328,12 +325,12 @@ def __call__(self): self._cm.log(f"Warning: Stale IPC file on {node}: {line}") if clean: - (_, lsout) = self._cm.rsh(node, "ps axf | grep -e pacemaker -e corosync", verbose=1) + (_, lsout) = self._cm.rsh.call(node, "ps axf | grep -e pacemaker -e corosync", verbose=1) for line in lsout: self._cm.debug(f"ps[{node}]: {line}") - self._cm.rsh(node, "rm -rf /dev/shm/qb-*") + self._cm.rsh.call(node, "rm -rf /dev/shm/qb-*") else: self._cm.debug(f"Skipping {node}") @@ -511,8 +508,8 @@ def _setup(self): self.debug(f"No nodes active - skipping {self.name}") return False - (_, lines) = self._cm.rsh(self._target, "crm_resource --list-cts", - verbose=1) + (_, lines) = self._cm.rsh.call(self._target, "crm_resource --list-cts", + verbose=1) for line in lines: if re.search("^Resource", line): @@ -670,9 +667,9 @@ def __init__(self, cm): def _crm_location(self, resource): """Return a list of cluster nodes where a given resource is running.""" - (rc, lines) = self._cm.rsh(self._target, - f"crm_resource --locate -r {resource} -Q", - verbose=1) + (rc, lines) = self._cm.rsh.call(self._target, + f"crm_resource --locate -r {resource} -Q", + verbose=1) hosts = [] if rc == 0: @@ -823,7 +820,7 @@ def _audit_cib_contents(self, hostlist): passed = False else: - (rc, result) = self._cm.rsh( + (rc, result) = self._cm.rsh.call( node0, f"crm_diff -VV -cf --new {node_xml} --original {node0_xml}", verbose=1) if rc != 0: @@ -850,16 +847,16 @@ def _store_remote_cib(self, node, target): if not target: target = node - (rc, lines) = self._cm.rsh(node, self._cm.templates["CibQuery"], verbose=1) + (rc, lines) = self._cm.rsh.call(node, self._cm.templates["CibQuery"], verbose=1) if rc != 0: self._cm.log("Could not retrieve configuration") return None - self._cm.rsh("localhost", f"rm -f {filename}") + self._cm.rsh.call("localhost", f"rm -f {filename}") for line in lines: - self._cm.rsh("localhost", f"echo \'{line[:-1]}\' >> {filename}", verbose=0) + self._cm.rsh.call("localhost", f"echo \'{line[:-1]}\' >> {filename}", verbose=0) - if self._cm.rsh.copy(filename, f"root@{target}:{filename}", silent=True) != 0: + if self._cm.rsh.copy(filename, f"root@{target}:{filename}") != 0: self._cm.log("Could not store configuration") return None @@ -960,13 +957,13 @@ def _audit_partition(self, partition): # not in itself a reason to fail the audit (not what we're # checking for in this audit) - (_, out) = self._cm.rsh(node, self._cm.templates["StatusCmd"] % node, verbose=1) + (_, out) = self._cm.rsh.call(node, self._cm.templates["StatusCmd"] % node, verbose=1) self._node_state[node] = out[0].strip() - (_, out) = self._cm.rsh(node, self._cm.templates["EpochCmd"], verbose=1) + (_, out) = self._cm.rsh.call(node, self._cm.templates["EpochCmd"], verbose=1) self._node_epoch[node] = out[0].strip() - (_, out) = self._cm.rsh(node, self._cm.templates["QuorumCmd"], verbose=1) + (_, out) = self._cm.rsh.call(node, self._cm.templates["QuorumCmd"], verbose=1) self._node_quorum[node] = out[0].strip() self.debug(f"Node {node}: {self._node_state[node]} - {self._node_epoch[node]} - {self._node_quorum[node]}.") diff --git a/python/pacemaker/_cts/cib.py b/python/pacemaker/_cts/cib.py index 93f03cbf046..7e256847e2c 100644 --- a/python/pacemaker/_cts/cib.py +++ b/python/pacemaker/_cts/cib.py @@ -49,7 +49,7 @@ def __init__(self, cm, version, factory, tmpfile=None): def _show(self): """Query a cluster node for its generated CIB; log and return the result.""" output = "" - (_, result) = self._factory.rsh(self._factory.target, f"HOME=/root CIB_file={self._factory.tmpfile} cibadmin -Q", verbose=1) + (_, result) = self._factory.rsh.call(self._factory.target, f"HOME=/root CIB_file={self._factory.tmpfile} cibadmin -Q", verbose=1) for line in result: output += line @@ -105,7 +105,7 @@ def get_node_id(self, node_name): r"""{gsub(/.*nodeid:\s*/,"");gsub(/\s+.*$/,"");print}' %s""" \ % (node_name, BuildOptions.COROSYNC_CONFIG_FILE) - (rc, output) = self._factory.rsh(self._factory.target, awk, verbose=1) + (rc, output) = self._factory.rsh.call(self._factory.target, awk, verbose=1) if rc == 0 and len(output) == 1: try: @@ -124,7 +124,7 @@ def install(self, target): self._factory.tmpfile = f"{BuildOptions.CIB_DIR}/cib.xml" self.contents(target) - self._factory.rsh(self._factory.target, f"chown {BuildOptions.DAEMON_USER} {self._factory.tmpfile}") + self._factory.rsh.call(self._factory.target, f"chown {BuildOptions.DAEMON_USER} {self._factory.tmpfile}") self._factory.tmpfile = old @@ -138,7 +138,7 @@ def contents(self, target): if target: self._factory.target = target - self._factory.rsh(self._factory.target, f"HOME=/root cibadmin --empty {self.version} > {self._factory.tmpfile}") + self._factory.rsh.call(self._factory.target, f"HOME=/root cibadmin --empty {self.version} > {self._factory.tmpfile}") self._num_nodes = len(self._cm.env["nodes"]) no_quorum = "stop" @@ -300,7 +300,7 @@ def contents(self, target): self._cib = self._show() if self._factory.tmpfile != f"{BuildOptions.CIB_DIR}/cib.xml": - self._factory.rsh(self._factory.target, f"rm -f {self._factory.tmpfile}") + self._factory.rsh.call(self._factory.target, f"rm -f {self._factory.tmpfile}") return self._cib diff --git a/python/pacemaker/_cts/cibxml.py b/python/pacemaker/_cts/cibxml.py index 8c9179deb67..7e4ce0a5171 100644 --- a/python/pacemaker/_cts/cibxml.py +++ b/python/pacemaker/_cts/cibxml.py @@ -149,7 +149,7 @@ def _run(self, operation, xml, section, options=""): fixed = f"HOME=/root CIB_file={self._factory.tmpfile}" fixed += f" cibadmin --{operation} --scope {section} {options} --xml-text '{xml}'" - (rc, _) = self._factory.rsh(self._factory.target, fixed) + (rc, _) = self._factory.rsh.call(self._factory.target, fixed) if rc != 0: raise RuntimeError(f"Configure call failed: {fixed}") diff --git a/python/pacemaker/_cts/clustermanager.py b/python/pacemaker/_cts/clustermanager.py index eb890d33929..5ba97790c34 100644 --- a/python/pacemaker/_cts/clustermanager.py +++ b/python/pacemaker/_cts/clustermanager.py @@ -21,18 +21,9 @@ from pacemaker._cts.environment import EnvFactory from pacemaker._cts import logging from pacemaker._cts.patterns import PatternSelector -from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.remote import RemoteExec from pacemaker._cts.watcher import LogWatcher -# pylint doesn't understand that self._rsh is callable (it stores the -# singleton instance of RemoteExec, as returned by the getInstance method -# of RemoteFactory). -# @TODO See if type annotations fix this. - -# I think we could also fix this by getting rid of the getInstance methods, -# but that's a project for another day. For now, just disable the warning. -# pylint: disable=not-callable - # ClusterManager has a lot of methods. # pylint: disable=too-many-public-methods @@ -64,7 +55,7 @@ def __init__(self): self.ns = NodeStatus(self.env) self.our_node = os.uname()[1].lower() self.partitions_expected = 1 - self.rsh = RemoteFactory().getInstance() + self.rsh = RemoteExec() self.templates = PatternSelector(self.name) self._cib_factory = ConfigFactory(self) @@ -110,7 +101,7 @@ def install_support(self, command="install"): This includes various init scripts and data, daemons, fencing agents, etc. """ for node in self.env["nodes"]: - self.rsh(node, f"{BuildOptions.DAEMON_DIR}/cts-support {command}") + self.rsh.call(node, f"{BuildOptions.DAEMON_DIR}/cts-support {command}") def prepare_fencing_watcher(self): """Return a LogWatcher object that watches for fencing log messages.""" @@ -236,7 +227,7 @@ def _install_config(self, node): return self._cib_sync[node] = True - self.rsh(node, f"rm -f {BuildOptions.CIB_DIR}/cib*") + self.rsh.call(node, f"rm -f {BuildOptions.CIB_DIR}/cib*") # Only install the CIB on the first node, all the other ones will pick it up from there if self._cib_installed: @@ -255,7 +246,7 @@ def _install_config(self, node): self.log(f"Installing Generated CIB on node {node}") self._cib.install(node) - self.rsh(node, f"chown {BuildOptions.DAEMON_USER} {BuildOptions.CIB_DIR}/cib.xml") + self.rsh.call(node, f"chown {BuildOptions.DAEMON_USER} {BuildOptions.CIB_DIR}/cib.xml") def start_cm(self, node, verbose=False): """Start up the cluster manager on a given node.""" @@ -293,7 +284,7 @@ def start_cm(self, node, verbose=False): stonith = self.prepare_fencing_watcher() watch.set_watch() - (rc, _) = self.rsh(node, self.templates["StartCmd"]) + (rc, _) = self.rsh.call(node, self.templates["StartCmd"]) if rc != 0: logging.log(f"Warn: Start command failed on node {node}") self.fencing_cleanup(node, stonith) @@ -323,7 +314,7 @@ def start_cm_async(self, node, verbose=False): log_fn(f"Starting {self.name} on node {node}") self._install_config(node) - self.rsh(node, self.templates["StartCmd"], synchronous=False) + self.rsh.call_async(node, self.templates["StartCmd"]) self.expected_status[node] = "up" def stop_cm(self, node, verbose=False, force=False): @@ -334,7 +325,7 @@ def stop_cm(self, node, verbose=False, force=False): if self.expected_status[node] != "up" and not force: return True - (rc, _) = self.rsh(node, self.templates["StopCmd"]) + (rc, _) = self.rsh.call(node, self.templates["StopCmd"]) if rc == 0: # Make sure we can continue even if corosync leaks self.expected_status[node] = "down" @@ -348,7 +339,7 @@ def stop_cm_async(self, node): """Stop the cluster manager on a given node without blocking.""" self.debug(f"Stopping {self.name} on node {node}") - self.rsh(node, self.templates["StopCmd"], synchronous=False) + self.rsh.call_async(node, self.templates["StopCmd"]) self.expected_status[node] = "down" def startall(self, nodelist=None, verbose=False, quick=False): @@ -437,7 +428,7 @@ def isolate_node(self, target, nodes=None): if node == target: continue - (rc, _) = self.rsh(target, self.templates["BreakCommCmd"] % node) + (rc, _) = self.rsh.call(target, self.templates["BreakCommCmd"] % node) if rc != 0: logging.log(f"Could not break the communication between {target} and {node}: {rc}") return False @@ -457,8 +448,8 @@ def unisolate_node(self, target, nodes=None): # Limit the amount of time we have asynchronous connectivity for # Restore both sides as simultaneously as possible - self.rsh(target, self.templates["FixCommCmd"] % node, synchronous=False) - self.rsh(node, self.templates["FixCommCmd"] % target, synchronous=False) + self.rsh.call_async(target, self.templates["FixCommCmd"] % node) + self.rsh.call_async(node, self.templates["FixCommCmd"] % target) self.debug(f"Communication restored between {target} and {node}") def prepare(self): @@ -503,7 +494,7 @@ def test_node_cm(self, node): self.env["log_kind"], "ClusterIdle") idle_watch.set_watch() - (_, out) = self.rsh(node, self.templates["StatusCmd"] % node, verbose=1) + (_, out) = self.rsh.call(node, self.templates["StatusCmd"] % node, verbose=1) if not out: out = "" @@ -577,7 +568,7 @@ def _partition_stable(self, nodes, timeout=None): for node in nodes.split(): # have each node dump its current state - self.rsh(node, self.templates["StatusCmd"] % node, verbose=1) + self.rsh.call(node, self.templates["StatusCmd"] % node, verbose=1) ret = idle_watch.look() @@ -621,7 +612,7 @@ def is_node_dc(self, node, status_line=None): Check the given status_line, or query the cluster if None. """ if not status_line: - (_, out) = self.rsh(node, self.templates["StatusCmd"] % node, verbose=1) + (_, out) = self.rsh.call(node, self.templates["StatusCmd"] % node, verbose=1) if out: status_line = out[0].strip() @@ -648,7 +639,7 @@ def is_node_dc(self, node, status_line=None): def active_resources(self, node): """Return a list of primitive resources active on the given node.""" - (_, output) = self.rsh(node, "crm_resource -c", verbose=1) + (_, output) = self.rsh.call(node, "crm_resource -c", verbose=1) resources = [] for line in output: if not re.search("^Resource", line): @@ -668,7 +659,7 @@ def resource_location(self, rid): continue cmd = self.templates["RscRunning"] % rid - (rc, lines) = self.rsh(node, cmd) + (rc, lines) = self.rsh.call(node, cmd) if rc == 127: self.log(f"Command '{cmd}' failed. Binary or pacemaker-cts package not installed?") @@ -694,7 +685,7 @@ def find_partitions(self): self.debug(f"Node {node} is down... skipping") continue - (_, out) = self.rsh(node, self.templates["PartitionCmd"], verbose=1) + (_, out) = self.rsh.call(node, self.templates["PartitionCmd"], verbose=1) if not out: self.log(f"no partition details for {node}") @@ -737,7 +728,7 @@ def has_quorum(self, node_list): if self.expected_status[node] != "up": continue - (rc, quorum) = self.rsh(node, self.templates["QuorumCmd"], verbose=1) + (rc, quorum) = self.rsh.call(node, self.templates["QuorumCmd"], verbose=1) if rc != ExitStatus.OK: self.debug(f"WARN: Quorum check on {node} returned error ({rc})") continue @@ -762,7 +753,7 @@ def components(self): def in_standby_mode(self, node): """Return whether or not the node is in Standby.""" - (_, out) = self.rsh(node, self.templates["StandbyQueryCmd"] % node, verbose=1) + (_, out) = self.rsh.call(node, self.templates["StandbyQueryCmd"] % node, verbose=1) if not out: return False @@ -787,7 +778,7 @@ def set_standby_mode(self, node, status): else: cmd = self.templates["StandbyCmd"] % (node, "off") - (rc, _) = self.rsh(node, cmd) + (rc, _) = self.rsh.call(node, cmd) return rc == 0 def add_dummy_rsc(self, node, rid): @@ -804,13 +795,13 @@ def add_dummy_rsc(self, node, rid): '""" - self.rsh(node, self.templates['CibAddXml'] % rsc_xml) - self.rsh(node, self.templates['CibAddXml'] % constraint_xml) + self.rsh.call(node, self.templates['CibAddXml'] % rsc_xml) + self.rsh.call(node, self.templates['CibAddXml'] % constraint_xml) def remove_dummy_rsc(self, node, rid): """Remove the previously added dummy resource given by rid on the given node.""" constraint = f"\"//rsc_location[@rsc='{rid}']\"" rsc = f"\"//primitive[@id='{rid}']\"" - self.rsh(node, self.templates['CibDelXpath'] % constraint) - self.rsh(node, self.templates['CibDelXpath'] % rsc) + self.rsh.call(node, self.templates['CibDelXpath'] % constraint) + self.rsh.call(node, self.templates['CibDelXpath'] % rsc) diff --git a/python/pacemaker/_cts/environment.py b/python/pacemaker/_cts/environment.py index 629bd71cadd..4dffc2cdeb6 100644 --- a/python/pacemaker/_cts/environment.py +++ b/python/pacemaker/_cts/environment.py @@ -15,7 +15,7 @@ from pacemaker.buildoptions import BuildOptions from pacemaker._cts import logging -from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.remote import RemoteExec from pacemaker._cts.watcher import LogKind @@ -26,15 +26,6 @@ class Environment: This consists largely of processing and storing command line parameters. """ - # pylint doesn't understand that self._rsh is callable (it stores the - # singleton instance of RemoteExec, as returned by the getInstance method - # of RemoteFactory). - # @TODO See if type annotations fix this. - - # I think we could also fix this by getting rid of the getInstance methods, - # but that's a project for another day. For now, just disable the warning. - # pylint: disable=not-callable - def __init__(self, args): """ Create a new Environment instance. @@ -67,7 +58,7 @@ def __init__(self, args): self.random_gen = random.Random() - self._rsh = RemoteFactory().getInstance() + self._rsh = RemoteExec() self._parse_args(args) @@ -117,7 +108,7 @@ def random_node(self): def _detect_systemd(self, node): """Detect whether systemd is in use on the target node.""" if "have_systemd" not in self.data: - (rc, _) = self._rsh(node, "systemctl list-units", verbose=0) + (rc, _) = self._rsh.call(node, "systemctl list-units", verbose=0) self["have_systemd"] = rc == 0 def _detect_syslog(self, node): @@ -127,10 +118,10 @@ def _detect_syslog(self, node): if self["have_systemd"]: # Systemd - (_, lines) = self._rsh(node, r"systemctl list-units | grep syslog.*\.service.*active.*running | sed 's:.service.*::'", verbose=1) + (_, lines) = self._rsh.call(node, r"systemctl list-units | grep syslog.*\.service.*active.*running | sed 's:.service.*::'", verbose=1) else: # SYS-V - (_, lines) = self._rsh(node, "chkconfig --list | grep syslog.*on | awk '{print $1}' | head -n 1", verbose=1) + (_, lines) = self._rsh.call(node, "chkconfig --list | grep syslog.*on | awk '{print $1}' | head -n 1", verbose=1) with suppress(IndexError): self["syslogd"] = lines[0].strip() @@ -139,22 +130,22 @@ def disable_service(self, node, service): """Disable the given service on the given node.""" if self["have_systemd"]: # Systemd - (rc, _) = self._rsh(node, f"systemctl disable {service}") + (rc, _) = self._rsh.call(node, f"systemctl disable {service}") return rc # SYS-V - (rc, _) = self._rsh(node, f"chkconfig {service} off") + (rc, _) = self._rsh.call(node, f"chkconfig {service} off") return rc def enable_service(self, node, service): """Enable the given service on the given node.""" if self["have_systemd"]: # Systemd - (rc, _) = self._rsh(node, f"systemctl enable {service}") + (rc, _) = self._rsh.call(node, f"systemctl enable {service}") return rc # SYS-V - (rc, _) = self._rsh(node, f"chkconfig {service} on") + (rc, _) = self._rsh.call(node, f"chkconfig {service} on") return rc def service_is_enabled(self, node, service): @@ -166,11 +157,11 @@ def service_is_enabled(self, node, service): # explicitly "enabled" instead of the return code. For example it returns # 0 if the service is "static" or "indirect", but they don't really count # as "enabled". - (rc, _) = self._rsh(node, f"systemctl is-enabled {service} | grep enabled") + (rc, _) = self._rsh.call(node, f"systemctl is-enabled {service} | grep enabled") return rc == 0 # SYS-V - (rc, _) = self._rsh(node, f"chkconfig --list | grep -e {service}.*on") + (rc, _) = self._rsh.call(node, f"chkconfig --list | grep -e {service}.*on") return rc == 0 def _detect_at_boot(self, node): @@ -181,10 +172,10 @@ def _detect_at_boot(self, node): def _detect_ip_offset(self, node): """Detect the offset for IPaddr resources.""" if self["create_resources"] and "IPBase" not in self.data: - (_, lines) = self._rsh(node, "ip addr | grep inet | grep -v -e link -e inet6 -e '/32' -e ' lo' | awk '{print $2}'", verbose=0) + (_, lines) = self._rsh.call(node, "ip addr | grep inet | grep -v -e link -e inet6 -e '/32' -e ' lo' | awk '{print $2}'", verbose=0) network = lines[0].strip() - (_, lines) = self._rsh(node, "nmap -sn -n %s | grep 'scan report' | awk '{print $NF}' | sed 's:(::' | sed 's:)::' | sort -V | tail -n 1" % network, verbose=0) + (_, lines) = self._rsh.call(node, "nmap -sn -n %s | grep 'scan report' | awk '{print $NF}' | sed 's:(::' | sed 's:)::' | sort -V | tail -n 1" % network, verbose=0) try: self["IPBase"] = lines[0].strip() diff --git a/python/pacemaker/_cts/patterns.py b/python/pacemaker/_cts/patterns.py index d8225f24af8..4b69cfe9a14 100644 --- a/python/pacemaker/_cts/patterns.py +++ b/python/pacemaker/_cts/patterns.py @@ -1,7 +1,7 @@ """Pattern-holding classes for Pacemaker's Cluster Test Suite (CTS).""" __all__ = ["PatternSelector"] -__copyright__ = "Copyright 2008-2025 the Pacemaker project contributors" +__copyright__ = "Copyright 2008-2026 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+)" from pacemaker.buildoptions import BuildOptions @@ -343,7 +343,7 @@ def __init__(self): ] -patternVariants = { +pattern_variants = { "crm-base": BasePatterns, "crm-corosync": Corosync2Patterns } @@ -363,11 +363,11 @@ def __init__(self, name="crm-corosync"): self._name = name # If no name was given, use the default. Otherwise, look up the appropriate - # class in patternVariants, instantiate it, and use that. + # class in pattern_variants, instantiate it, and use that. if not name: self._base = Corosync2Patterns() else: - self._base = patternVariants[name]() + self._base = pattern_variants[name]() def __getitem__(self, key): """ diff --git a/python/pacemaker/_cts/remote.py b/python/pacemaker/_cts/remote.py index 6c5ab6f561f..015483d3fe7 100644 --- a/python/pacemaker/_cts/remote.py +++ b/python/pacemaker/_cts/remote.py @@ -1,41 +1,18 @@ """Remote command runner for Pacemaker's Cluster Test Suite (CTS).""" -__all__ = ["RemoteExec", "RemoteFactory"] +__all__ = ["RemoteExec"] __copyright__ = "Copyright 2014-2026 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" import re import os -from subprocess import Popen, PIPE +import subprocess from threading import Thread from pacemaker._cts import logging -def convert2string(lines): - """ - Convert byte strings to UTF-8 strings. - - Lists of byte strings are converted to a list of UTF-8 strings. All other - text formats are passed through. - """ - if isinstance(lines, bytes): - return lines.decode("utf-8") - - if isinstance(lines, list): - lst = [] - for line in lines: - if isinstance(line, bytes): - line = line.decode("utf-8") - - lst.append(line) - - return lst - - return lines - - class AsyncCmd(Thread): """A class for doing the hard work of running a command on another machine.""" @@ -66,7 +43,9 @@ def run(self): if not self._proc: # pylint: disable=consider-using-with - self._proc = Popen(self._command, stdout=PIPE, stderr=PIPE, close_fds=True, shell=True) + self._proc = subprocess.Popen(self._command, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True, + shell=True, universal_newlines=True) logging.debug(f"cmd: async: target={self._node}, pid={self._proc.pid}: {self._command}") self._proc.wait() @@ -83,12 +62,9 @@ def run(self): for line in err: logging.debug(f"cmd: stderr[{self._proc.pid}]: {line}") - err = convert2string(err) - if self._proc.stdout: out = self._proc.stdout.readlines() self._proc.stdout.close() - out = convert2string(out) if self._delegate: self._delegate.async_complete(self._proc.pid, self._proc.returncode, out, err) @@ -101,18 +77,18 @@ class RemoteExec: It runs a command on another machine using ssh and scp. """ - def __init__(self, command, cp_command, silent=False): - """ - Create a new RemoteExec instance. - - Arguments: - command -- The ssh command string to use for remote execution - cp_command -- The scp command string to use for copying files - silent -- Should we log command status? - """ - self.command = command - self.cp_command = cp_command - self._silent = silent + def __init__(self): + """Create a new RemoteExec instance.""" + + # @TODO This should be an argument list that gets used with subprocess, + # but making that change will require changing everywhere that __call__ + # or call_async pass a command string. + # + # -n: no stdin, -x: no X11, + # -o ServerAliveInterval=5: disconnect after 3*5s if the server + # stops responding + self._command = "ssh -l root -n -x -o ServerAliveInterval=5 " \ + "-o ConnectTimeout=10 -o StrictHostKeyChecking=off" self._our_node = os.uname()[1].lower() def _fixcmd(self, cmd): @@ -127,20 +103,10 @@ def _cmd(self, args): if sysname is None or sysname.lower() in [self._our_node, "localhost"]: ret = command else: - ret = f"{self.command} {sysname} '{self._fixcmd(command)}'" + ret = f"{self._command} {sysname} '{self._fixcmd(command)}'" return ret - def _log(self, args): - """Log a message.""" - if not self._silent: - logging.log(args) - - def _debug(self, args): - """Log a message at the debug level.""" - if not self._silent: - logging.debug(args) - def call_async(self, node, command, delegate=None): """ Run the given command on the given remote system and do not wait for it to complete. @@ -157,7 +123,7 @@ def call_async(self, node, command, delegate=None): aproc.start() return aproc - def __call__(self, node, command, synchronous=True, verbose=2): + def call(self, node, command, verbose=2): """ Run the given command on the given remote system. @@ -167,7 +133,6 @@ def __call__(self, node, command, synchronous=True, verbose=2): Arguments: node -- The remote machine to run on command -- The command to run, as a string - synchronous -- Should we wait for the command to complete? verbose -- If 0, do not log anything. If 1, log the command and its return code but not its output. If 2, additionally log command output. @@ -177,63 +142,50 @@ def __call__(self, node, command, synchronous=True, verbose=2): rc = 0 result = None # pylint: disable=consider-using-with - proc = Popen(self._cmd([node, command]), - stdout=PIPE, stderr=PIPE, close_fds=True, shell=True) - - if not synchronous and proc.pid > 0 and not self._silent: - aproc = AsyncCmd(node, command, proc=proc) - aproc.start() - return (rc, result) + proc = subprocess.Popen(self._cmd([node, command]), stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True, shell=True, + universal_newlines=True) if proc.stdout: result = proc.stdout.readlines() proc.stdout.close() else: - self._log("No stdout stream") + logging.log("No stdout stream") rc = proc.wait() if verbose > 0: - self._debug(f"cmd: target={node}, rc={rc}: {command}") - - result = convert2string(result) + logging.debug(f"cmd: target={node}, rc={rc}: {command}") if proc.stderr: errors = proc.stderr.readlines() proc.stderr.close() for err in errors: - self._debug(f"cmd: stderr: {err}") + logging.debug(f"cmd: stderr: {err}") if verbose == 2: for line in result: - self._debug(f"cmd: stdout: {line}") + logging.debug(f"cmd: stdout: {line}") return (rc, result) - def copy(self, source, target, silent=False): + def copy(self, source, target): """ Perform a copy of the source file to the remote target. - This function uses the cp_command provided when the RemoteExec object - was created. - - Returns the return code of the cp_command. + Returns the return code of the copy process. """ - # @TODO Use subprocess module with argument array instead - # (self.cp_command should be an array too) - cmd = f"{self.cp_command} '{source}' '{target}'" - rc = os.system(cmd) - - if not silent: - self._debug(f"cmd: rc={rc}: {cmd}") - - return rc + # -B: batch mode, -q: no stats (quiet) + p = subprocess.run(["scp", "-B", "-q", "-o", "StrictHostKeyChecking=off", + f"'{source}'", f"'{target}'"], check=False) + logging.debug(f"cmd: rc={p.returncode}: {p.args}") + return p.returncode def exists_on_all(self, filename, hosts): """Return True if specified file exists on all specified hosts.""" for host in hosts: - (rc, _) = self(host, f"test -r {filename}") + (rc, _) = self.call(host, f"test -r {filename}") if rc != 0: return False @@ -242,39 +194,8 @@ def exists_on_all(self, filename, hosts): def exists_on_none(self, filename, hosts): """Return True if specified file does not exist on any specified host.""" for host in hosts: - (rc, _) = self(host, f"test -r {filename}") + (rc, _) = self.call(host, f"test -r {filename}") if rc == 0: return False return True - - -class RemoteFactory: - """A class for constructing a singleton instance of a RemoteExec object.""" - - # Class variables - - # -n: no stdin, -x: no X11, - # -o ServerAliveInterval=5: disconnect after 3*5s if the server - # stops responding - command = ("ssh -l root -n -x -o ServerAliveInterval=5 " - "-o ConnectTimeout=10 -o TCPKeepAlive=yes " - "-o ServerAliveCountMax=3 ") - - # -B: batch mode, -q: no stats (quiet) - cp_command = "scp -B -q" - - instance = None - - # pylint: disable=invalid-name - def getInstance(self): - """ - Return the previously created instance of RemoteExec. - - If no instance exists, create one and then return that. - """ - if not RemoteFactory.instance: - RemoteFactory.instance = RemoteExec(RemoteFactory.command, - RemoteFactory.cp_command, - False) - return RemoteFactory.instance diff --git a/python/pacemaker/_cts/tests/cibsecret.py b/python/pacemaker/_cts/tests/cibsecret.py index f8ceec93aa7..01d85e68f18 100644 --- a/python/pacemaker/_cts/tests/cibsecret.py +++ b/python/pacemaker/_cts/tests/cibsecret.py @@ -16,8 +16,6 @@ # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # This comes from include/config.h as private API, assuming pacemaker is built @@ -87,8 +85,8 @@ def _remove_dummy(self, node): def _check_cib_value(self, node, expected): """Check that the secret has the expected value.""" - (rc, lines) = self._rsh(node, f"crm_resource -r {self._rid} -g {self._secret}", - verbose=1) + (rc, lines) = self._rsh.call(node, f"crm_resource -r {self._rid} -g {self._secret}", + verbose=1) s = " ".join(lines).strip() if rc != 0 or s != expected: @@ -99,8 +97,8 @@ def _check_cib_value(self, node, expected): def _test_check(self, node): """Test the 'cibsecret check' subcommand.""" - (rc, _) = self._rsh(node, f"cibsecret check {self._rid} {self._secret}", - verbose=1) + (rc, _) = self._rsh.call(node, f"cibsecret check {self._rid} {self._secret}", + verbose=1) if rc != 0: return self.failure("Failed to check secret") @@ -109,8 +107,8 @@ def _test_check(self, node): def _test_delete(self, node): """Test the 'cibsecret delete' subcommand.""" - (rc, _) = self._rsh(node, f"cibsecret delete {self._rid} {self._secret}", - verbose=1) + (rc, _) = self._rsh.call(node, f"cibsecret delete {self._rid} {self._secret}", + verbose=1) if rc != 0: return self.failure("Failed to delete secret") @@ -119,8 +117,8 @@ def _test_delete(self, node): def _test_get(self, node, expected): """Test the 'cibsecret get' subcommand.""" - (rc, lines) = self._rsh(node, f"cibsecret get {self._rid} {self._secret}", - verbose=1) + (rc, lines) = self._rsh.call(node, f"cibsecret get {self._rid} {self._secret}", + verbose=1) s = " ".join(lines).strip() if rc != 0 or s != expected: @@ -131,8 +129,8 @@ def _test_get(self, node, expected): def _test_set(self, node): """Test the 'cibsecret set' subcommand.""" - (rc, _) = self._rsh(node, f"cibsecret set {self._rid} {self._secret} {self._secret_val}", - verbose=1) + (rc, _) = self._rsh.call(node, f"cibsecret set {self._rid} {self._secret} {self._secret_val}", + verbose=1) if rc != 0: return self.failure("Failed to set secret") @@ -141,8 +139,8 @@ def _test_set(self, node): def _test_stash(self, node): """Test the 'cibsecret stash' subcommand.""" - (rc, _) = self._rsh(node, f"cibsecret stash {self._rid} {self._secret}", - verbose=1) + (rc, _) = self._rsh.call(node, f"cibsecret stash {self._rid} {self._secret}", + verbose=1) if rc != 0: return self.failure(f"Failed to stash secret {self._secret}") @@ -151,7 +149,7 @@ def _test_stash(self, node): def _test_sync(self, node): """Test the 'cibsecret sync' subcommand.""" - (rc, _) = self._rsh(node, "cibsecret sync", verbose=1) + (rc, _) = self._rsh.call(node, "cibsecret sync", verbose=1) if rc != 0: return self.failure("Failed to sync secrets") @@ -160,8 +158,8 @@ def _test_sync(self, node): def _test_unstash(self, node): """Test the 'cibsecret unstash' subcommand.""" - (rc, _) = self._rsh(node, f"cibsecret unstash {self._rid} {self._secret}", - verbose=1) + (rc, _) = self._rsh.call(node, f"cibsecret unstash {self._rid} {self._secret}", + verbose=1) if rc != 0: return self.failure(f"Failed to unstash secret {self._secret}") @@ -263,8 +261,8 @@ def is_applicable(self): other = self._cm.env["nodes"][1:] for o in other: - (rc, _) = self._cm.rsh(node, f"{self._cm.rsh.command} {o} exit", - verbose=0) + (rc, _) = self._cm.rsh.call(node, f"ssh -o StrictHostKeyChecking=off root@{o} exit", + verbose=0) if rc != ExitStatus.OK: return False diff --git a/python/pacemaker/_cts/tests/componentfail.py b/python/pacemaker/_cts/tests/componentfail.py index 5a81c602333..fb7f62eb9a1 100644 --- a/python/pacemaker/_cts/tests/componentfail.py +++ b/python/pacemaker/_cts/tests/componentfail.py @@ -16,8 +16,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -82,7 +80,7 @@ def __call__(self, node): self._okerrpatterns = [ self._cm.templates["Pat:Resource_active"], ] - (_, lines) = self._rsh(node, "crm_resource -c", verbose=1) + (_, lines) = self._rsh.call(node, "crm_resource -c", verbose=1) for line in lines: if re.search("^Resource", line): diff --git a/python/pacemaker/_cts/tests/ctstest.py b/python/pacemaker/_cts/tests/ctstest.py index 331bfaebbac..5d5cf7a55bd 100644 --- a/python/pacemaker/_cts/tests/ctstest.py +++ b/python/pacemaker/_cts/tests/ctstest.py @@ -8,7 +8,7 @@ from pacemaker._cts import logging from pacemaker._cts.environment import EnvFactory -from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.remote import RemoteExec from pacemaker._cts.timer import Timer from pacemaker._cts.watcher import LogWatcher @@ -49,7 +49,7 @@ def __init__(self, cm): self._cm = cm self._env = EnvFactory().getInstance() - self._rsh = RemoteFactory().getInstance() + self._rsh = RemoteExec() self._timers = {} self.benchmark = True # which tests to benchmark diff --git a/python/pacemaker/_cts/tests/maintenancemode.py b/python/pacemaker/_cts/tests/maintenancemode.py index efc0fa5493f..16ede1b9089 100644 --- a/python/pacemaker/_cts/tests/maintenancemode.py +++ b/python/pacemaker/_cts/tests/maintenancemode.py @@ -13,14 +13,6 @@ from pacemaker._cts.tests.starttest import StartTest from pacemaker._cts.timer import Timer -# Disable various pylint warnings that occur in so many places throughout this -# file it's easiest to just take care of them globally. This does introduce the -# possibility that we'll miss some other cause of the same warning, but we'll -# just have to be careful. - -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable - class MaintenanceMode(CTSTest): """Toggle nodes in and ount of maintenance mode.""" @@ -67,10 +59,10 @@ def _toggle_maintenance_mode(self, node, enabled): watch.set_watch() self.debug(f"Turning maintenance mode {action}") - self._rsh(node, self._cm.templates[f"MaintenanceMode{action}"]) + self._rsh.call(node, self._cm.templates[f"MaintenanceMode{action}"]) if enabled: - self._rsh(node, f"crm_resource -V -F -r {self._rid} -H {node} &>/dev/null") + self._rsh.call(node, f"crm_resource -V -F -r {self._rid} -H {node} &>/dev/null") with Timer(self.name, f"recover{action}"): watch.look_for_all() @@ -123,7 +115,7 @@ def _remove_maintenance_dummy(self, node): def _managed_rscs(self, node): """Return a list of all resources managed by the cluster.""" rscs = [] - (_, lines) = self._rsh(node, "crm_resource -c", verbose=1) + (_, lines) = self._rsh.call(node, "crm_resource -c", verbose=1) for line in lines: if re.search("^Resource", line): @@ -142,7 +134,7 @@ def _verify_resources(self, node, rscs, managed): if not managed: managed_str = "unmanaged" - (_, lines) = self._rsh(node, "crm_resource -c", verbose=1) + (_, lines) = self._rsh.call(node, "crm_resource -c", verbose=1) for line in lines: if re.search("^Resource", line): tmp = AuditResource(self._cm, line) diff --git a/python/pacemaker/_cts/tests/nearquorumpointtest.py b/python/pacemaker/_cts/tests/nearquorumpointtest.py index 7ed6f7e0746..58e46039e4a 100644 --- a/python/pacemaker/_cts/tests/nearquorumpointtest.py +++ b/python/pacemaker/_cts/tests/nearquorumpointtest.py @@ -12,8 +12,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -109,7 +107,7 @@ def __call__(self, dummy): # Make sure they're completely down with no residule for node in stopset: - self._rsh(node, self._cm.templates["StopCmd"]) + self._rsh.call(node, self._cm.templates["StopCmd"]) return self.success() diff --git a/python/pacemaker/_cts/tests/reattach.py b/python/pacemaker/_cts/tests/reattach.py index bf914fca48d..99070bc9557 100644 --- a/python/pacemaker/_cts/tests/reattach.py +++ b/python/pacemaker/_cts/tests/reattach.py @@ -15,14 +15,6 @@ from pacemaker._cts.tests.simulstoplite import SimulStopLite from pacemaker._cts.tests.starttest import StartTest -# Disable various pylint warnings that occur in so many places throughout this -# file it's easiest to just take care of them globally. This does introduce the -# possibility that we'll miss some other cause of the same warning, but we'll -# just have to be careful. - -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable - class Reattach(CTSTest): """Restart the cluster and verify that resources remain running throughout.""" @@ -43,19 +35,19 @@ def __init__(self, cm): def _is_managed(self, node): """Return whether resources are managed by the cluster.""" - (_, is_managed) = self._rsh(node, "crm_attribute -t rsc_defaults -n is-managed -q -G -d true", verbose=1) + (_, is_managed) = self._rsh.call(node, "crm_attribute -t rsc_defaults -n is-managed -q -G -d true", verbose=1) is_managed = is_managed[0].strip() return is_managed == "true" def _set_unmanaged(self, node): """Disable resource management.""" self.debug("Disable resource management") - self._rsh(node, "crm_attribute -t rsc_defaults -n is-managed -v false") + self._rsh.call(node, "crm_attribute -t rsc_defaults -n is-managed -v false") def _set_managed(self, node): """Enable resource management.""" self.debug("Re-enable resource management") - self._rsh(node, "crm_attribute -t rsc_defaults -n is-managed -D") + self._rsh.call(node, "crm_attribute -t rsc_defaults -n is-managed -D") def _disable_incompatible_rscs(self, node): """ @@ -75,13 +67,13 @@ def _disable_incompatible_rscs(self, node): ' --scope rsc_defaults""" - return self._rsh(node, self._cm.templates['CibAddXml'] % xml) + return self._rsh.call(node, self._cm.templates['CibAddXml'] % xml) def _enable_incompatible_rscs(self, node): """Re-enable resources that were incompatible with this test.""" self.debug("Re-enable incompatible resources") xml = """""" - return self._rsh(node, f"""cibadmin --delete --xml-text '{xml}'""") + return self._rsh.call(node, f"cibadmin --delete --xml-text '{xml}'") def _reprobe(self, node): """ @@ -92,7 +84,7 @@ def _reprobe(self, node): rules. An earlier test may have erased the relevant node attribute, so do a reprobe, which should add the attribute back. """ - return self._rsh(node, """crm_resource --refresh""") + return self._rsh.call(node, "crm_resource --refresh") def setup(self, node): """Set up this test.""" @@ -186,7 +178,7 @@ def __call__(self, node): # Ignore actions for STONITH resources ignore = [] - (_, lines) = self._rsh(node, "crm_resource -c", verbose=1) + (_, lines) = self._rsh.call(node, "crm_resource -c", verbose=1) for line in lines: if re.search("^Resource", line): r = AuditResource(self._cm, line) diff --git a/python/pacemaker/_cts/tests/remotedriver.py b/python/pacemaker/_cts/tests/remotedriver.py index 706e41eb9f8..dc5500643d1 100644 --- a/python/pacemaker/_cts/tests/remotedriver.py +++ b/python/pacemaker/_cts/tests/remotedriver.py @@ -17,14 +17,6 @@ from pacemaker._cts.tests.stoptest import StopTest from pacemaker._cts.timer import Timer -# Disable various pylint warnings that occur in so many places throughout this -# file it's easiest to just take care of them globally. This does introduce the -# possibility that we'll miss some other cause of the same warning, but we'll -# just have to be careful. - -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable - class RemoteDriver(CTSTest): """ @@ -100,7 +92,7 @@ def _del_rsc(self, node, rsc): delete command. """ othernode = self._get_other_node(node) - (rc, _) = self._rsh(othernode, f"crm_resource -D -r {rsc} -t primitive") + (rc, _) = self._rsh.call(othernode, f"crm_resource -D -r {rsc} -t primitive") if rc != 0: self.fail(f"Removal of resource '{rsc}' failed") @@ -112,7 +104,7 @@ def _add_rsc(self, node, rsc_xml): add command. """ othernode = self._get_other_node(node) - (rc, _) = self._rsh(othernode, f"cibadmin -C -o resources -X '{rsc_xml}'") + (rc, _) = self._rsh.call(othernode, f"cibadmin -C -o resources -X '{rsc_xml}'") if rc != 0: self.fail("resource creation failed") @@ -188,7 +180,7 @@ def _enable_services(self, node): def _stop_pcmk_remote(self, node): """Stop the Pacemaker Remote service on the given node.""" for _ in range(10): - (rc, _) = self._rsh(node, "service pacemaker_remote stop") + (rc, _) = self._rsh.call(node, "service pacemaker_remote stop") if rc != 0: time.sleep(6) else: @@ -197,7 +189,7 @@ def _stop_pcmk_remote(self, node): def _start_pcmk_remote(self, node): """Start the Pacemaker Remote service on the given node.""" for _ in range(10): - (rc, _) = self._rsh(node, "service pacemaker_remote start") + (rc, _) = self._rsh.call(node, "service pacemaker_remote start") if rc != 0: time.sleep(6) else: @@ -229,8 +221,8 @@ def _start_metal(self, node): self._disable_services(node) # make sure the resource doesn't already exist for some reason - self._rsh(node, f"crm_resource -D -r {self._remote_rsc} -t primitive") - self._rsh(node, f"crm_resource -D -r {self._remote_node} -t primitive") + self._rsh.call(node, f"crm_resource -D -r {self._remote_rsc} -t primitive") + self._rsh.call(node, f"crm_resource -D -r {self._remote_node} -t primitive") if not self._stop(node): self.fail(f"Failed to shutdown cluster node {node}") @@ -274,7 +266,7 @@ def migrate_connection(self, node): watch = self.create_watch(pats, 120) watch.set_watch() - (rc, _) = self._rsh(node, f"crm_resource -M -r {self._remote_node}", verbose=1) + (rc, _) = self._rsh.call(node, f"crm_resource -M -r {self._remote_node}", verbose=1) if rc != 0: self.fail("failed to move remote node connection resource") return @@ -305,7 +297,7 @@ def fail_rsc(self, node): self.debug("causing dummy rsc to fail.") - self._rsh(node, "rm -f /var/run/resource-agents/Dummy*") + self._rsh.call(node, "rm -f /var/run/resource-agents/Dummy*") with Timer(self.name, "remoteRscFail"): watch.look_for_all() @@ -390,7 +382,7 @@ def _add_dummy_rsc(self, node): self._add_primitive_rsc(node) # force that rsc to prefer the remote node. - (rc, _) = self._cm.rsh(node, f"crm_resource -M -r {self._remote_rsc} -N {self._remote_node} -f", verbose=1) + (rc, _) = self._cm.rsh.call(node, f"crm_resource -M -r {self._remote_rsc} -N {self._remote_node} -f", verbose=1) if rc != 0: self.fail("Failed to place remote resource on remote node.") return @@ -408,17 +400,17 @@ def test_attributes(self, node): # This verifies permanent attributes can be set on a remote-node. It also # verifies the remote-node can edit its own cib node section remotely. - (rc, line) = self._cm.rsh(node, f"crm_attribute -l forever -n testattr -v testval -N {self._remote_node}", verbose=1) + (rc, line) = self._cm.rsh.call(node, f"crm_attribute -l forever -n testattr -v testval -N {self._remote_node}", verbose=1) if rc != 0: self.fail(f"Failed to set remote-node attribute. rc:{rc} output:{line}") return - (rc, _) = self._cm.rsh(node, f"crm_attribute -l forever -n testattr -q -N {self._remote_node}", verbose=1) + (rc, _) = self._cm.rsh.call(node, f"crm_attribute -l forever -n testattr -q -N {self._remote_node}", verbose=1) if rc != 0: self.fail("Failed to get remote-node attribute") return - (rc, _) = self._cm.rsh(node, f"crm_attribute -l forever -n testattr -D -N {self._remote_node}", verbose=1) + (rc, _) = self._cm.rsh.call(node, f"crm_attribute -l forever -n testattr -D -N {self._remote_node}", verbose=1) if rc != 0: self.fail("Failed to delete remote-node attribute") @@ -451,13 +443,13 @@ def cleanup_metal(self, node): if self._remote_rsc_added: # Remove dummy resource added for remote node tests self.debug("Cleaning up dummy rsc put on remote node") - self._rsh(self._get_other_node(node), f"crm_resource -U -r {self._remote_rsc}") + self._rsh.call(self._get_other_node(node), f"crm_resource -U -r {self._remote_rsc}") self._del_rsc(node, self._remote_rsc) if self._remote_node_added: # Remove remote node's connection resource self.debug("Cleaning up remote node connection resource") - self._rsh(self._get_other_node(node), f"crm_resource -U -r {self._remote_node}") + self._rsh.call(self._get_other_node(node), f"crm_resource -U -r {self._remote_node}") self._del_rsc(node, self._remote_node) watch.look_for_all() @@ -473,7 +465,7 @@ def cleanup_metal(self, node): if self._remote_node_added: # Remove remote node itself self.debug("Cleaning up node entry for remote node") - self._rsh(self._get_other_node(node), f"crm_node --force --remove {self._remote_node}") + self._rsh.call(self._get_other_node(node), f"crm_node --force --remove {self._remote_node}") def _setup_env(self, node): """ @@ -497,10 +489,10 @@ def _setup_env(self, node): # sync key throughout the cluster for n in self._env["nodes"]: - self._rsh(n, "mkdir -p --mode=0750 /etc/pacemaker") + self._rsh.call(n, "mkdir -p --mode=0750 /etc/pacemaker") self._rsh.copy(keyfile, f"root@{n}:/etc/pacemaker/authkey") - self._rsh(n, "chgrp haclient /etc/pacemaker /etc/pacemaker/authkey") - self._rsh(n, "chmod 0640 /etc/pacemaker/authkey") + self._rsh.call(n, "chgrp haclient /etc/pacemaker /etc/pacemaker/authkey") + self._rsh.call(n, "chmod 0640 /etc/pacemaker/authkey") os.unlink(keyfile) @@ -510,7 +502,7 @@ def is_applicable(self): return False for node in self._env["nodes"]: - (rc, _) = self._rsh(node, "which pacemaker-remoted >/dev/null 2>&1") + (rc, _) = self._rsh.call(node, "which pacemaker-remoted >/dev/null 2>&1") if rc != 0: return False @@ -538,7 +530,7 @@ def __call__(self, node): def errors_to_ignore(self): """Return list of errors which should be ignored.""" return [ - r"""is running on remote.*which isn't allowed""", - r"""Connection terminated""", - r"""Could not send remote""" + r"is running on remote.*which isn't allowed", + r"Connection terminated", + r"Could not send remote" ] diff --git a/python/pacemaker/_cts/tests/resourcerecover.py b/python/pacemaker/_cts/tests/resourcerecover.py index f70147a8d9a..9ac87d63573 100644 --- a/python/pacemaker/_cts/tests/resourcerecover.py +++ b/python/pacemaker/_cts/tests/resourcerecover.py @@ -10,14 +10,6 @@ from pacemaker._cts.tests.starttest import StartTest from pacemaker._cts.timer import Timer -# Disable various pylint warnings that occur in so many places throughout this -# file it's easiest to just take care of them globally. This does introduce the -# possibility that we'll miss some other cause of the same warning, but we'll -# just have to be careful. - -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable - class ResourceRecover(CTSTest): """Fail a random resource.""" @@ -92,7 +84,7 @@ def _choose_resource(self, node, resourcelist): """Choose a random resource to target.""" self._rid = self._env.random_gen.choice(resourcelist) self._rid_alt = self._rid - (_, lines) = self._rsh(node, "crm_resource -c", verbose=1) + (_, lines) = self._rsh.call(node, "crm_resource -c", verbose=1) for line in lines: if line.startswith("Resource: "): @@ -108,8 +100,8 @@ def _choose_resource(self, node, resourcelist): def _get_failcount(self, node): """Check the fail count of targeted resource on given node.""" cmd = "crm_failcount --quiet --query --resource %s --operation %s --interval %d --node %s" - (rc, lines) = self._rsh(node, cmd % (self._rid, self._action, self._interval, node), - verbose=1) + (rc, lines) = self._rsh.call(node, cmd % (self._rid, self._action, self._interval, node), + verbose=1) if rc != 0 or len(lines) != 1: lines = [line.strip() for line in lines] @@ -133,7 +125,7 @@ def _fail_resource(self, rsc, node, pats): watch = self.create_watch(pats, 60) watch.set_watch() - self._rsh(node, f"crm_resource -V -F -r {self._rid} -H {node} &>/dev/null") + self._rsh.call(node, f"crm_resource -V -F -r {self._rid} -H {node} &>/dev/null") with Timer(self.name, "recover"): watch.look_for_all() diff --git a/python/pacemaker/_cts/tests/resynccib.py b/python/pacemaker/_cts/tests/resynccib.py index 581ffdc4b4b..61e75a95cef 100644 --- a/python/pacemaker/_cts/tests/resynccib.py +++ b/python/pacemaker/_cts/tests/resynccib.py @@ -1,7 +1,7 @@ """Start the cluster without a CIB and verify it gets copied from another node.""" __all__ = ["ResyncCIB"] -__copyright__ = "Copyright 2000-2025 the Pacemaker project contributors" +__copyright__ = "Copyright 2000-2026 the Pacemaker project contributors" __license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY" from pacemaker import BuildOptions @@ -10,14 +10,6 @@ from pacemaker._cts.tests.simulstartlite import SimulStartLite from pacemaker._cts.tests.simulstoplite import SimulStopLite -# Disable various pylint warnings that occur in so many places throughout this -# file it's easiest to just take care of them globally. This does introduce the -# possibility that we'll miss some other cause of the same warning, but we'll -# just have to be careful. - -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable - class ResyncCIB(CTSTest): """Start the cluster on a node without a CIB and verify the CIB is copied over later.""" @@ -46,7 +38,7 @@ def __call__(self, node): return self.failure("Could not stop all nodes") # Test config recovery when the other nodes come up - self._rsh(node, f"rm -f {BuildOptions.CIB_DIR}/cib*") + self._rsh.call(node, f"rm -f {BuildOptions.CIB_DIR}/cib*") # Start the selected node if not self._restart1(node): diff --git a/python/pacemaker/_cts/tests/simulstoplite.py b/python/pacemaker/_cts/tests/simulstoplite.py index 840ccaabd51..71364430dac 100644 --- a/python/pacemaker/_cts/tests/simulstoplite.py +++ b/python/pacemaker/_cts/tests/simulstoplite.py @@ -12,8 +12,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -65,7 +63,7 @@ def __call__(self, dummy): if watch.look_for_all(): # Make sure they're completely down with no residule for node in self._env["nodes"]: - self._rsh(node, self._cm.templates["StopCmd"]) + self._rsh.call(node, self._cm.templates["StopCmd"]) return self.success() diff --git a/python/pacemaker/_cts/tests/splitbraintest.py b/python/pacemaker/_cts/tests/splitbraintest.py index e6edb83bd95..008839bab78 100644 --- a/python/pacemaker/_cts/tests/splitbraintest.py +++ b/python/pacemaker/_cts/tests/splitbraintest.py @@ -17,8 +17,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -118,7 +116,7 @@ def __call__(self, node): self.debug(f"Partition[{key}]:\t{val!r}") # Disabling STONITH to reduce test complexity for now - self._rsh(node, "crm_attribute -V -n fencing-enabled -v false") + self._rsh.call(node, "crm_attribute -V -n fencing-enabled -v false") for val in partitions.values(): self._isolate_partition(val) @@ -185,7 +183,7 @@ def __call__(self, node): # Turn fencing back on if self._env["fencing_enabled"]: - self._rsh(node, "crm_attribute -V -D -n fencing-enabled") + self._rsh.call(node, "crm_attribute -V -D -n fencing-enabled") self._cm.cluster_stable() diff --git a/python/pacemaker/_cts/tests/stonithdtest.py b/python/pacemaker/_cts/tests/stonithdtest.py index 415d76494f0..7061260cbe4 100644 --- a/python/pacemaker/_cts/tests/stonithdtest.py +++ b/python/pacemaker/_cts/tests/stonithdtest.py @@ -15,8 +15,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -68,7 +66,7 @@ def __call__(self, node): origin = self._env.random_gen.choice(self._env["nodes"]) - (rc, _) = self._rsh(origin, f"stonith_admin --reboot {node} -VVVVVV") + (rc, _) = self._rsh.call(origin, f"stonith_admin --reboot {node} -VVVVVV") if rc == ExitStatus.TIMEOUT: # Look for the patterns, usually this means the required diff --git a/python/pacemaker/_cts/tests/stoptest.py b/python/pacemaker/_cts/tests/stoptest.py index 74f7929459c..38b26cde82f 100644 --- a/python/pacemaker/_cts/tests/stoptest.py +++ b/python/pacemaker/_cts/tests/stoptest.py @@ -12,8 +12,6 @@ # possibility that we'll miss some other cause of the same warning, but we'll # just have to be careful. -# pylint doesn't understand that self._rsh is callable. -# pylint: disable=not-callable # pylint doesn't understand that self._env is subscriptable. # pylint: disable=unsubscriptable-object @@ -71,11 +69,11 @@ def __call__(self, node): unmatched_str = "||" if watch.unmatched: - (_, output) = self._rsh(node, "/bin/ps axf", verbose=1) + (_, output) = self._rsh.call(node, "/bin/ps axf", verbose=1) for line in output: self.debug(line) - (_, output) = self._rsh(node, "/usr/sbin/dlm_tool dump 2>/dev/null", verbose=1) + (_, output) = self._rsh.call(node, "/usr/sbin/dlm_tool dump 2>/dev/null", verbose=1) for line in output: self.debug(line) diff --git a/python/pacemaker/_cts/watcher.py b/python/pacemaker/_cts/watcher.py index d1f1423b047..779b0f71d34 100644 --- a/python/pacemaker/_cts/watcher.py +++ b/python/pacemaker/_cts/watcher.py @@ -12,7 +12,7 @@ from pacemaker.buildoptions import BuildOptions from pacemaker._cts.errors import OutputNotFoundError from pacemaker._cts import logging -from pacemaker._cts.remote import RemoteFactory +from pacemaker._cts.remote import RemoteExec CTS_SUPPORT_BIN = f"{BuildOptions.DAEMON_DIR}/cts-support" @@ -51,7 +51,7 @@ def __init__(self, filename, host=None, name=None): self.limit = None self.name = name self.offset = "EOF" - self.rsh = RemoteFactory().getInstance() + self.rsh = RemoteExec() if host: self.host = host @@ -188,8 +188,7 @@ def set_end(self): cmd = f"{CTS_SUPPORT_BIN} watch -p CTSwatcher: -l 2 -f {self.filename} -o EOF" - # pylint: disable=not-callable - (_, lines) = self.rsh(self.host, cmd, verbose=0) + (_, lines) = self.rsh.call(self.host, cmd, verbose=0) for line in lines: match = re.search(r"^CTSwatcher:Last read: (\d+)", line) @@ -323,8 +322,7 @@ def set_end(self): return # Seconds and nanoseconds since epoch - # pylint: disable=not-callable - (rc, lines) = self.rsh(self.host, "date +%s.%N", verbose=0) + (rc, lines) = self.rsh.call(self.host, "date +%s.%N", verbose=0) if rc == 0 and len(lines) == 1: self.limit = float(lines[0].strip())