Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package crmsh for openSUSE:Factory checked 
in at 2023-03-02 23:04:05
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.31432 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Thu Mar  2 23:04:05 2023 rev:282 rq:1068615 version:4.4.1+20230302.2b5310b9

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2023-02-24 
18:08:27.793532494 +0100
+++ /work/SRC/openSUSE:Factory/.crmsh.new.31432/crmsh.changes   2023-03-02 
23:04:29.864151935 +0100
@@ -1,0 +2,34 @@
+Thu Mar 02 06:37:38 UTC 2023 - xli...@suse.com
+
+- Update to version 4.4.1+20230302.2b5310b9:
+  * Dev: unittest: Adjust unit test for previous change
+  * Dev: bootstrap: Add sudo before crm_node under non-root user on remote node
+
+-------------------------------------------------------------------
+Thu Mar 02 02:23:26 UTC 2023 - xli...@suse.com
+
+- Update to version 4.4.1+20230302.fc282490:
+  * Dev: behave: Create user alice on qnetd node
+  * Dev: behave: don't build crmsh code on qnetd node
+
+-------------------------------------------------------------------
+Thu Mar 02 01:36:34 UTC 2023 - xli...@suse.com
+
+- Update to version 4.4.1+20230302.2ed0ab14:
+  * Dev: unittest: Adjust unit test for previous changes
+  * Fix: qdevice: Unable to setup qdevice under non-root user (bsc#1208770)
+
+-------------------------------------------------------------------
+Tue Feb 28 10:07:22 UTC 2023 - xli...@suse.com
+
+- Update to version 4.4.1+20230228.2f852310:
+  * Dev: utils: Suppress the output of ssh-copy-id for non-root user case
+
+-------------------------------------------------------------------
+Mon Feb 27 01:58:59 UTC 2023 - xli...@suse.com
+
+- Update to version 4.4.1+20230227.b420cbf5:
+  * Dev: unittest: Adjust unit test for previous change
+  * Dev: utils: Avoid using magic number
+
+-------------------------------------------------------------------

Old:
----
  crmsh-4.4.1+20230224.498677ab.tar.bz2

New:
----
  crmsh-4.4.1+20230302.2b5310b9.tar.bz2

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

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.WYsAMV/_old  2023-03-02 23:04:30.856156890 +0100
+++ /var/tmp/diff_new_pack.WYsAMV/_new  2023-03-02 23:04:30.860156911 +0100
@@ -36,7 +36,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        4.4.1+20230224.498677ab
+Version:        4.4.1+20230302.2b5310b9
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.WYsAMV/_old  2023-03-02 23:04:30.916157190 +0100
+++ /var/tmp/diff_new_pack.WYsAMV/_new  2023-03-02 23:04:30.920157210 +0100
@@ -9,7 +9,7 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">498677abb1bf88b1490339d307e925742854080b</param>
+  <param 
name="changesrevision">e2961b896d4155fb6205fe9f6c628a572ae61f93</param>
 </service>
 </servicedata>
 (No newline at EOF)

++++++ crmsh-4.4.1+20230224.498677ab.tar.bz2 -> 
crmsh-4.4.1+20230302.2b5310b9.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20230224.498677ab/crmsh/bootstrap.py 
new/crmsh-4.4.1+20230302.2b5310b9/crmsh/bootstrap.py
--- old/crmsh-4.4.1+20230224.498677ab/crmsh/bootstrap.py        2023-02-24 
10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/crmsh/bootstrap.py        2023-03-02 
07:05:25.000000000 +0100
@@ -157,12 +157,20 @@
         """
         if not self.qnetd_addr:
             return
+        parts = self.qnetd_addr.split('@', 2)
+        if len(parts) == 2:
+            ssh_user = parts[0]
+            qnetd_host = parts[1]
+        else:
+            ssh_user = None
+            qnetd_host = self.qnetd_addr
         self.qdevice_inst = qdevice.QDevice(
-                self.qnetd_addr,
+                qnetd_host,
                 port=self.qdevice_port,
                 algo=self.qdevice_algo,
                 tie_breaker=self.qdevice_tie_breaker,
                 tls=self.qdevice_tls,
+                ssh_user=ssh_user,
                 cmds=self.qdevice_heuristics,
                 mode=self.qdevice_heuristics_mode,
                 is_stage=self.stage == "qdevice")
@@ -867,7 +875,9 @@
 
     # If not use -N/--nodes option
     if not node_list:
-        _save_core_hosts([local_user], [utils.this_node()], 
sync_to_remote=False)
+        user_by_host = utils.HostUserConfig()
+        user_by_host.add(local_user, utils.this_node())
+        user_by_host.save_local()
         return
 
     print()
@@ -903,14 +913,11 @@
             )
             if result.returncode != 0:
                 utils.fatal('Failed to add public keys to {}@{}: 
{}'.format(remote_user, node, result.stdout))
-    if utils.this_node() not in node_list:
-        _user_list = [local_user]
-        _user_list.extend(user_list)
-        _node_list = [utils.this_node()]
-        _node_list.extend(node_list)
-        _save_core_hosts(_user_list, _node_list, sync_to_remote=True)
-    else:
-        _save_core_hosts(user_list, node_list, sync_to_remote=True)
+    user_by_host = utils.HostUserConfig()
+    for user, node in zip(user_list, node_list):
+        user_by_host.add(user, node)
+    user_by_host.add(local_user, utils.this_node())
+    user_by_host.save_remote(node_list)
 
 
 def _merge_authorized_keys(keys: typing.List[str]) -> bytes:
@@ -925,33 +932,7 @@
     return buf
 
 
-def _save_core_hosts(user_list: typing.List[str], host_list: typing.List[str], 
sync_to_remote: bool):
-    value = [f'{user}@{host}' for user, host in zip(user_list, host_list)]
-    config.set_option('core', 'hosts', value)
-    # TODO: it is saved in ~root/.config/crm/crm.conf, is it as suitable path?
-    config.save()
-    if sync_to_remote:
-        assert "'" not in value
-        crmsh.parallax.parallax_call(host_list, "crm options set core.hosts 
'{}'".format(', '.join(value)))
-
-
-def _load_core_hosts() -> typing.Optional[typing.Tuple[typing.List[str], 
typing.List[str]]]:
-    users = list()
-    hosts = list()
-    li = config.get_option('core', 'hosts')
-    if li == ['']:
-        return users, hosts
-    for s in li:
-        parts = s.split('@', 2)
-        if len(parts) != 2:
-            utils.fatal('Malformed config core.hosts: {}'.format(s))
-        users.append(parts[0])
-        hosts.append(parts[1])
-    return users, hosts
-
-
 def _fetch_core_hosts(local_user, remote_user, remote_host) -> 
typing.Tuple[typing.List[str], typing.List[str]]:
-    # FIXME: when the remote_host is initialized with -d, this cmd will print 
extra debug log to stdout
     cmd = 'crm options show core.hosts'
     result = utils.su_subprocess_run(
         local_user,
@@ -1604,12 +1585,21 @@
     qdevice_heuristics_mode = prompt_for_string("MODE of operation of 
heuristics (on/sync/off)", default="sync",
             valid_func=qdevice.QDevice.check_qdevice_heuristics_mode) if 
qdevice_heuristics else None
 
+    parts = qnetd_addr.split('@', 2)
+    if len(parts) == 2:
+        ssh_user = parts[0]
+        qnetd_host = parts[1]
+    else:
+        ssh_user = None
+        qnetd_host = qnetd_addr
+
     _context.qdevice_inst = qdevice.QDevice(
-            qnetd_addr,
+            qnetd_host,
             port=qdevice_port,
             algo=qdevice_algo,
             tie_breaker=qdevice_tie_breaker,
             tls=qdevice_tls,
+            ssh_user=ssh_user,
             cmds=qdevice_heuristics,
             mode=qdevice_heuristics_mode,
             is_stage=_context.stage == "qdevice")
@@ -1626,24 +1616,20 @@
         utils.disable_service("corosync-qdevice.service")
         return
     logger.info("""Configure Qdevice/Qnetd:""")
-    for node in utils.list_cluster_nodes():
+    cluster_node_list = utils.list_cluster_nodes()
+    for node in cluster_node_list:
         if not utils.service_is_available("corosync-qdevice.service", node):
             utils.fatal("corosync-qdevice.service is not available on 
{}".format(node))
     qdevice_inst = _context.qdevice_inst
     qnetd_addr = qdevice_inst.qnetd_addr
+    ssh_user = qdevice_inst.ssh_user if qdevice_inst.ssh_user is not None else 
_context.current_user
     # Configure ssh passwordless to qnetd if detect password is needed
     local_user = utils.user_of(utils.this_node())
-    # TODO: ssh to qnetd with a non-root user
-    assert '@' not in qnetd_addr
-    remote_user = 'root'
-    remote_host = qnetd_addr
-    if utils.check_ssh_passwd_need(local_user, remote_user, remote_host):
-        logger.info("Copy ssh key to qnetd node({}@{})".format(remote_user, 
remote_host))
-        utils.ssh_copy_id(local_user, remote_user, remote_host)
-    user_list, host_list = _load_core_hosts()
-    user_list.append(remote_user)
-    host_list.append(remote_host)
-    _save_core_hosts(user_list, host_list, sync_to_remote=True)
+    if utils.check_ssh_passwd_need(local_user, ssh_user, qnetd_addr):
+        utils.ssh_copy_id(local_user, ssh_user, qnetd_addr)
+    user_by_host = utils.HostUserConfig()
+    user_by_host.add(ssh_user, qnetd_addr)
+    user_by_host.save_remote(cluster_node_list)
     # Start qdevice service if qdevice already configured
     if utils.is_qdevice_configured() and not confirm("Qdevice is already 
configured - overwrite?"):
         qdevice_inst.start_qdevice_service()
@@ -1698,11 +1684,11 @@
         ),
         local_user,
     )
-    user_list = [_context.current_user]
-    user_list.extend(_context.user_list)
-    node_list = [utils.this_node()]
-    node_list.extend(_context.node_list)
-    _save_core_hosts(user_list, node_list, sync_to_remote=False)
+    user_by_host = utils.HostUserConfig()
+    for user, node in zip(_context.user_list, _context.node_list):
+        user_by_host.add(user, node)
+    user_by_host.add(local_user, utils.this_node())
+    user_by_host.save_local()
 
 
 def swap_public_ssh_key(
@@ -1941,7 +1927,7 @@
     # Fetch cluster nodes list
     remote_user = _context.user_list[0]
     local_user = _context.current_user
-    cmd = 'ssh {} {}@{} PATH=\\"\\$PATH\\":/usr/sbin:/sbin crm_node 
-l'.format(SSH_OPTION, remote_user, init_node)
+    cmd = f'ssh {SSH_OPTION} {remote_user}@{init_node} sudo crm_node -l'
     rc, out, err = utils.su_get_stdout_stderr(local_user, cmd)
     if rc != 0:
         utils.fatal("Can't fetch cluster nodes list from {}: 
{}".format(init_node, err))
@@ -1962,31 +1948,29 @@
                      tokens[1], tokens[2]))
         else:
             cluster_nodes_list.append(tokens[1])
+    user_by_host = utils.HostUserConfig()
+    user_by_host.add(local_user, utils.this_node())
     try:
         user_list, host_list = _fetch_core_hosts(local_user, remote_user, 
init_node)
+        for user, host in zip(user_list, host_list):
+            user_by_host.add(user, host)
     except ValueError:
         # No core.hosts on the seed host, may be a cluster upgraded from 
previous version
-        user_list = list()
-        host_list = list()
-    user_list.append(local_user)
-    host_list.append(utils.this_node())
-    _save_core_hosts(user_list, host_list, sync_to_remote=False)
+        pass
+    user_by_host.save_local()
 
     # Filter out init node from cluster_nodes_list
     cmd = "ssh {} {}@{} hostname".format(SSH_OPTION, remote_user , init_node)
     rc, out, err = utils.su_get_stdout_stderr(local_user, cmd)
     if rc != 0:
         utils.fatal("Can't fetch hostname of {}: {}".format(init_node, err))
-    if out in cluster_nodes_list:
-        cluster_nodes_list.remove(out)
-
     # Swap ssh public key between join node and other cluster nodes
-    for node in cluster_nodes_list:
+    for node in (node for node in cluster_nodes_list if node != out):
         remote_user_to_swap = utils.user_of(node)
         remote_privileged_user = remote_user_to_swap
         utils.ssh_copy_id(local_user, remote_privileged_user, node)
         swap_public_ssh_key(node, local_user, remote_user_to_swap, local_user, 
remote_privileged_user)
-    _save_core_hosts(user_list, host_list, sync_to_remote=True)
+    user_by_host.save_remote(cluster_nodes_list)
 
 
 def sync_files_to_disk():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20230224.498677ab/crmsh/corosync.py 
new/crmsh-4.4.1+20230302.2b5310b9/crmsh/corosync.py
--- old/crmsh-4.4.1+20230224.498677ab/crmsh/corosync.py 2023-02-24 
10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/crmsh/corosync.py 2023-03-02 
07:05:25.000000000 +0100
@@ -113,7 +113,6 @@
     except ValueError:
         remote_user = 'root'
     if utils.check_ssh_passwd_need(local_user, remote_user, qnetd_addr):
-        print("Copy ssh key to qnetd node({})".format(qnetd_addr))
         utils.ssh_copy_id(local_user, remote_user, qnetd_addr)
 
     cmd = "corosync-qnetd-tool -lv -c {}".format(cluster_name)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20230224.498677ab/crmsh/qdevice.py 
new/crmsh-4.4.1+20230302.2b5310b9/crmsh/qdevice.py
--- old/crmsh-4.4.1+20230224.498677ab/crmsh/qdevice.py  2023-02-24 
10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/crmsh/qdevice.py  2023-03-02 
07:05:25.000000000 +0100
@@ -2,6 +2,9 @@
 import re
 import socket
 import functools
+import subprocess
+import tempfile
+import typing
 from enum import Enum
 from . import constants
 from . import utils
@@ -127,7 +130,7 @@
     qdevice_db_path = "/etc/corosync/qdevice/net/nssdb"
 
     def __init__(self, qnetd_addr, port=5403, algo="ffsplit", 
tie_breaker="lowest",
-            tls="on", cluster_node=None, cmds=None, mode=None, 
cluster_name=None, is_stage=False):
+            tls="on", ssh_user=None, cluster_node=None, cmds=None, mode=None, 
cluster_name=None, is_stage=False):
         """
         Init function
         """
@@ -136,6 +139,7 @@
         self.algo = algo
         self.tie_breaker = tie_breaker
         self.tls = tls
+        self.ssh_user = ssh_user
         self.cluster_node = cluster_node
         self.cmds = cmds
         self.mode = mode
@@ -368,7 +372,7 @@
 
         desc = "Step 2: Fetch {} from {}".format(self.qnetd_cacert_filename, 
self.qnetd_addr)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_slurp([self.qnetd_addr], self.qdevice_path, 
self.qnetd_cacert_on_qnetd)
+        self._fetch_file_to_local(self.qnetd_addr, self.qnetd_cacert_on_qnetd, 
'{}/{}'.format(self.qdevice_path, self.qnetd_addr))
 
     def copy_qnetd_crt_to_cluster(self):
         """
@@ -382,10 +386,42 @@
 
         desc = "Step 3: Copy exported {} to 
{}".format(self.qnetd_cacert_filename, node_list)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_copy(
-                node_list,
-                os.path.dirname(self.qnetd_cacert_on_local),
-                self.qdevice_path)
+        
self._copy_file_to_remote_hosts(os.path.dirname(self.qnetd_cacert_on_local), 
node_list, self.qdevice_path)
+
+    @classmethod
+    def _copy_file_to_remote_host(cls, local_file, remote_host: str, 
remote_path):
+        remote_user = utils.user_of(remote_host)
+        with tempfile.NamedTemporaryFile('w', encoding='utf-8') as tmp:
+            tmp.write("put -pr '{}' '{}'\n".format(local_file, remote_path))
+            tmp.flush()
+            # we can not su to a non-root user, since reading the source file 
may need privilege.
+            cmd = "sftp {} -o IdentityFile=~{}/.ssh/id_rsa -o BatchMode=yes -s 
'sudo PATH=/usr/lib/ssh:/usr/libexec/ssh /bin/sh -c \"exec sftp-server\"' -b {} 
{}@{}".format(
+                constants.SSH_OPTION,
+                utils.user_of(utils.this_node()),
+                # FIXME: sftp-server is not always in /usr/lib/ssh
+                tmp.name,
+                remote_user, cls._enclose_inet6_addr(remote_host),
+            )
+            result = subprocess.run(
+                ['/bin/sh', '-c', cmd],
+                stdin=subprocess.DEVNULL,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+            )
+        if result.returncode != 0:
+            utils.fatal('Failed to copy file from {} to {}:{}: {}: 
{}'.format(local_file, remote_host, remote_path, cmd, 
utils.to_ascii(result.stdout)))
+
+    @staticmethod
+    def _enclose_inet6_addr(addr: str):
+        if ':' in addr:
+            return f'[{addr}]'
+        else:
+            return addr
+
+    @classmethod
+    def _copy_file_to_remote_hosts(cls, local_file, remote_hosts: 
typing.Iterable[str], remote_path):
+        for host in remote_hosts:
+            cls._copy_file_to_remote_host(local_file, host, remote_path)
 
     def init_db_on_cluster(self):
         """
@@ -420,10 +456,7 @@
         """
         desc = "Step 6: Copy {} to {}".format(self.qdevice_crq_filename, 
self.qnetd_addr)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_copy(
-                [self.qnetd_addr],
-                self.qdevice_crq_on_local,
-                self.qdevice_crq_on_qnetd)
+        self._copy_file_to_remote_host(self.qdevice_crq_on_local, 
self.qnetd_addr, self.qdevice_crq_on_qnetd)
 
     def sign_crq_on_qnetd(self):
         """
@@ -446,10 +479,21 @@
         """
         desc = "Step 8: Fetch {} from 
{}".format(os.path.basename(self.qnetd_cluster_crt_on_qnetd), self.qnetd_addr)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_slurp(
-                [self.qnetd_addr],
-                self.qdevice_path,
-                self.qnetd_cluster_crt_on_qnetd)
+        self._fetch_file_to_local(self.qnetd_addr, 
self.qnetd_cluster_crt_on_qnetd, '{}/{}'.format(self.qdevice_path, 
self.qnetd_addr))
+
+    @classmethod
+    def _fetch_file_to_local(cls, remote_host: str, remote_path: str, 
local_dir: str):
+        basename = os.path.basename(remote_path)
+        rc, stdout, stderr = utils.get_stdout_stderr_auto_ssh_no_input(
+            remote_host,
+            "cat '{}'".format(remote_path),
+            raw=True,
+        )
+        if rc != 0:
+            utils.fatal("Failed to fetch file {} from {}: 
{}".format(remote_path, remote_host, stderr))
+        os.makedirs(local_dir, exist_ok=True)
+        with open('{}/{}'.format(local_dir, basename), 'wb') as f:
+            f.write(stdout)
 
     def import_cluster_crt(self):
         """
@@ -474,10 +518,7 @@
 
         desc = "Step 10: Copy {} to {}".format(self.qdevice_p12_filename, 
node_list)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_copy(
-                node_list,
-                self.qdevice_p12_on_local,
-                os.path.dirname(self.qdevice_p12_on_local))
+        self._copy_file_to_remote_hosts(self.qdevice_p12_on_local, node_list, 
self.qdevice_p12_on_local)
 
     def import_p12_on_cluster(self):
         """
@@ -522,10 +563,7 @@
 
         desc = "Step 1: Fetch {} from {}".format(self.qnetd_cacert_filename, 
self.cluster_node)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_slurp(
-                [self.cluster_node],
-                self.qdevice_path,
-                self.qnetd_cacert_on_local)
+        self._fetch_file_to_local(self.cluster_node, 
self.qnetd_cacert_on_local, '{}/{}'.format(self.qdevice_path, 
self.cluster_node))
 
     def init_db_on_local(self):
         """
@@ -552,10 +590,7 @@
 
         desc = "Step 3: Fetch {} from {}".format(self.qdevice_p12_filename, 
self.cluster_node)
         logger_utils.log_only_to_file(desc)
-        parallax.parallax_slurp(
-                [self.cluster_node],
-                self.qdevice_path,
-                self.qdevice_p12_on_local)
+        self._fetch_file_to_local(self.cluster_node, 
self.qdevice_p12_on_local, '{}/{}'.format(self.qdevice_path, self.cluster_node))
 
     def import_p12_on_local(self):
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20230224.498677ab/crmsh/utils.py 
new/crmsh-4.4.1+20230302.2b5310b9/crmsh/utils.py
--- old/crmsh-4.4.1+20230224.498677ab/crmsh/utils.py    2023-02-24 
10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/crmsh/utils.py    2023-03-02 
07:05:25.000000000 +0100
@@ -21,6 +21,7 @@
 import argparse
 import random
 import string
+import grp
 from pathlib import Path
 from contextlib import contextmanager, closing
 from stat import S_ISBLK
@@ -116,7 +117,11 @@
         if cached is None:
             ret = self._user_of_impl(host)
             if ret is None:
-                return userdir.getuser()
+                user = userdir.get_sudoer()
+                if user:
+                    return user
+                else:
+                    return userdir.getuser()
             else:
                 self._cache[host] = ret
                 return ret
@@ -125,10 +130,13 @@
 
     @staticmethod
     def _user_of_impl(host):
-        canonical, aliases, _ = socket.gethostbyaddr(host)
-        aliases = set(aliases)
-        aliases.add(canonical)
-        aliases.add(host)
+        try:
+            canonical, aliases, _ = socket.gethostbyaddr(host)
+            aliases = set(aliases)
+            aliases.add(canonical)
+            aliases.add(host)
+        except socket.herror:
+            aliases = {host}
         hosts = config.get_option('core', 'hosts')
         if hosts == ['']:
             return None
@@ -140,7 +148,7 @@
                 node = item
             if node in aliases:
                 return user
-        logger.warning('Failed to get the user of host %s (aliases: %s). Known 
hosts are %s', host, aliases, hosts)
+        logger.debug('Failed to get the user of host %s (aliases: %s). Known 
hosts are %s', host, aliases, hosts)
         return None
 
 
@@ -154,13 +162,8 @@
 def ssh_copy_id(local_user, remote_user, remote_node):
     if check_ssh_passwd_need(local_user, remote_user, remote_node):
         logger.info("Configuring SSH passwordless with 
{}@{}".format(remote_user, remote_node))
-        cmd = "ssh-copy-id -i ~/.ssh/id_rsa.pub '{}@{}'".format(remote_user, 
remote_node)
-        result = su_subprocess_run(
-                local_user, 
-                cmd, 
-                tty=True, 
-                stdout=subprocess.PIPE, 
-                stderr=subprocess.PIPE)
+        cmd = "ssh-copy-id -i ~/.ssh/id_rsa.pub '{}@{}' &> 
/dev/null".format(remote_user, remote_node)
+        result = su_subprocess_run(local_user, cmd, tty=True)
         if result.returncode != 0:
             fatal("Failed to login to remote host {}@{}".format(remote_user, 
remote_node))
 
@@ -2815,10 +2818,7 @@
     cmd = "rpm -q --quiet {}".format(pkg)
     if remote_addr:
         # check on remote
-        print("Check whether {} is installed on {}".format(pkg, remote_addr))
-        # FIXME
-        cmd = "ssh {} {}@{} \"{}\"".format(SSH_OPTION, user_of(remote_addr), 
remote_addr, cmd)
-        rc, _, _ = get_stdout_stderr(cmd, no_reg=True)
+        rc, _, _ = get_stdout_stderr_auto_ssh_no_input(remote_addr, cmd)
     else:
         # check on local
         rc, _ = get_stdout(cmd)
@@ -3427,7 +3427,7 @@
     """
     Check if current user is in haclient group
     """
-    return 90 in os.getgroups()
+    return constants.HA_GROUP in [grp.getgrgid(g).gr_name for g in 
os.getgroups()]
 
 
 def check_user_access(level_name):
@@ -3456,4 +3456,47 @@
     else:
         logger.error("Please run this command starting with \"sudo\"")
     raise TerminateSubCommand
+
+
+class HostUserConfig:
+    """Keep the username used for ssh connection corresponding to each host.
+
+    The data is saved in configuration option `core.hosts`.
+    """
+    def __init__(self):
+        self._hosts_users = dict()
+        self.load()
+
+    def load(self):
+        users = list()
+        hosts = list()
+        li = config.get_option('core', 'hosts')
+        if li == ['']:
+            self._hosts_users = dict()
+            return
+        for s in li:
+            parts = s.split('@', 2)
+            if len(parts) != 2:
+                raise ValueError('Malformed config core.hosts: {}'.format(s))
+            users.append(parts[0])
+            hosts.append(parts[1])
+        self._hosts_users = {host: user for user, host in zip(users, hosts)}
+
+    def save_local(self):
+        value = [f'{user}@{host}' for host, user in 
sorted(self._hosts_users.items(), key=lambda x: x[0])]
+        config.set_option('core', 'hosts', value)
+        # TODO: it is saved in ~root/.config/crm/crm.conf, is it as suitable 
path?
+        config.save()
+
+    def save_remote(self, remote_hosts: typing.Iterable[str]):
+        self.save_local()
+        value = [f'{user}@{host}' for host, user in 
sorted(self._hosts_users.items(), key=lambda x: x[0])]
+        crmsh.parallax.parallax_call(remote_hosts, "crm options set core.hosts 
'{}'".format(', '.join(value)))
+
+    def get(self, host):
+        return self._hosts_users[host]
+
+    def add(self, user, host):
+        self._hosts_users[host] = user
+
 # vim:ts=4:sw=4:et:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20230224.498677ab/test/run-functional-tests 
new/crmsh-4.4.1+20230302.2b5310b9/test/run-functional-tests
--- old/crmsh-4.4.1+20230224.498677ab/test/run-functional-tests 2023-02-24 
10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/test/run-functional-tests 2023-03-02 
07:05:25.000000000 +0100
@@ -222,12 +222,19 @@
        fi
        docker_exec $node_name "rm -rf /run/nologin"
        docker_exec $node_name "echo 'StrictHostKeyChecking no' >> 
/etc/ssh/ssh_config"
-       docker cp $PROJECT_PATH $node_name:/opt/crmsh
-       info "Building crmsh on \"$node_name\"..."
-       docker_exec $node_name "$make_cmd" 1> /dev/null || \
+
+       if [ "$node_name" != "qnetd-node" ];then
+         docker cp $PROJECT_PATH $node_name:/opt/crmsh
+         info "Building crmsh on \"$node_name\"..."
+         docker_exec $node_name "$make_cmd" 1> /dev/null || \
                fatal "Building failed on $node_name!"
 
-       create_alice_bob_carol
+         create_alice_bob_carol
+       else
+         docker_exec $node_name "useradd -m -s /bin/bash alice 2>/dev/null"
+         docker_exec $node_name "echo \"alice ALL=(ALL) NOPASSWD:ALL\" > 
/etc/sudoers.d/alice"
+         docker_exec $node_name "cp -r /root/.ssh ~alice/ && chown alice:users 
-R ~alice/.ssh"
+       fi
 }
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_bootstrap.py 
new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_bootstrap.py
--- old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_bootstrap.py  
2023-02-24 10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_bootstrap.py  
2023-03-02 07:05:25.000000000 +0100
@@ -16,6 +16,7 @@
 import yaml
 import socket
 
+import crmsh.utils
 from crmsh.ui_node import NodeMgmt
 
 try:
@@ -73,7 +74,14 @@
         options = mock.Mock(qnetd_addr="node3", qdevice_port=123, stage="")
         ctx = self.ctx_inst.set_context(options)
         ctx.initialize_qdevice()
-        mock_qdevice.assert_called_once_with('node3', port=123, algo=None, 
tie_breaker=None, tls=None, cmds=None, mode=None, is_stage=False)
+        mock_qdevice.assert_called_once_with('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')
+    def test_initialize_qdevice_with_user(self, mock_qdevice):
+        options = mock.Mock(qnetd_addr="alice@node3", qdevice_port=123, 
stage="")
+        ctx = self.ctx_inst.set_context(options)
+        ctx.initialize_qdevice()
+        mock_qdevice.assert_called_once_with('node3', port=123, 
ssh_user='alice', algo=None, tie_breaker=None, tls=None, cmds=None, mode=None, 
is_stage=False)
 
     @mock.patch('crmsh.utils.fatal')
     def test_validate_sbd_option_error_together(self, mock_error):
@@ -553,18 +561,18 @@
         with self.assertRaises(SystemExit):
             bootstrap.setup_passwordless_with_other_nodes("node1")
 
-        mock_run.assert_called_once_with('carol', 'ssh {} alice@node1 
PATH=\\"\\$PATH\\":/usr/sbin:/sbin crm_node -l'.format(constants.SSH_OPTION))
+        mock_run.assert_called_once_with('carol', 'ssh {} alice@node1 sudo 
crm_node -l'.format(constants.SSH_OPTION))
         mock_error.assert_called_once_with("Can't fetch cluster nodes list 
from node1: None")
 
     @mock.patch('crmsh.utils.fatal')
-    @mock.patch('crmsh.bootstrap._save_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.bootstrap._fetch_core_hosts')
     @mock.patch('crmsh.utils.su_get_stdout_stderr')
     def test_setup_passwordless_with_other_nodes_failed_fetch_hostname(
             self,
             mock_run,
             mock_fetch_core_hosts,
-            mock_save_core_hosts,
+            mock_host_user_config_class,
             mock_error,
     ):
         bootstrap._context = mock.Mock(user_list=["alice"], 
current_user="carol")
@@ -581,12 +589,12 @@
             bootstrap.setup_passwordless_with_other_nodes("node1")
 
         mock_run.assert_has_calls([
-            mock.call('carol', 'ssh {} alice@node1 
PATH=\\"\\$PATH\\":/usr/sbin:/sbin crm_node -l'.format(constants.SSH_OPTION)),
+            mock.call('carol', 'ssh {} alice@node1 sudo crm_node 
-l'.format(constants.SSH_OPTION)),
             mock.call('carol', 'ssh {} alice@node1 
hostname'.format(constants.SSH_OPTION))
         ])
         mock_error.assert_called_once_with("Can't fetch hostname of node1: 
None")
 
-    @mock.patch('crmsh.bootstrap._save_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.bootstrap._fetch_core_hosts')
     @mock.patch('crmsh.utils.ssh_copy_id')
     @mock.patch('crmsh.utils.user_of')
@@ -599,7 +607,7 @@
             mock_userof,
             mock_ssh_copy_id: mock.MagicMock,
             mock_fetch_core_hosts,
-            mock_save_core_hosts,
+            mock_host_user_config_class,
 
     ):
         bootstrap._context = mock.Mock(current_user="carol", 
user_list=["alice", "bob"])
@@ -615,7 +623,7 @@
         bootstrap.setup_passwordless_with_other_nodes("node1")
 
         mock_run.assert_has_calls([
-            mock.call('carol', 'ssh {} alice@node1 
PATH=\\"\\$PATH\\":/usr/sbin:/sbin crm_node -l'.format(constants.SSH_OPTION)),
+            mock.call('carol', 'ssh {} alice@node1 sudo crm_node 
-l'.format(constants.SSH_OPTION)),
             mock.call('carol', 'ssh {} alice@node1 
hostname'.format(constants.SSH_OPTION))
             ])
         mock_userof.assert_called_once_with("node2")
@@ -812,8 +820,7 @@
         mock_status.assert_not_called()
         mock_disable.assert_called_once_with("corosync-qdevice.service")
 
-    @mock.patch('crmsh.bootstrap._save_core_hosts')
-    @mock.patch('crmsh.bootstrap._load_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.utils.user_of')
     @mock.patch('crmsh.utils.list_cluster_nodes')
     @mock.patch('crmsh.utils.ssh_copy_id')
@@ -823,11 +830,10 @@
             self,
             mock_status, mock_check_ssh_passwd_need,
             mock_ssh_copy_id, mock_list_nodes, mock_userof,
-            mock_load_core_hosts, mock_save_core_hosts,
+            mock_host_user_config_class,
     ):
         mock_list_nodes.return_value = []
         bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
current_user="bob", user_list=["alice"])
-        mock_load_core_hosts.return_value = ([], [])
         mock_check_ssh_passwd_need.return_value = True
         mock_ssh_copy_id.side_effect = ValueError('foo')
         mock_userof.return_value = "bob"
@@ -837,13 +843,11 @@
 
         mock_status.assert_has_calls([
             mock.call("Configure Qdevice/Qnetd:"),
-            mock.call("Copy ssh key to qnetd node(root@10.10.10.123)")
-            ])
-        mock_check_ssh_passwd_need.assert_called_once_with("bob", "root", 
"10.10.10.123")
-        mock_ssh_copy_id.assert_called_once_with('bob', 'root', '10.10.10.123')
+        ])
+        mock_check_ssh_passwd_need.assert_called_once_with("bob", "bob", 
"10.10.10.123")
+        mock_ssh_copy_id.assert_called_once_with('bob', 'bob', '10.10.10.123')
 
-    @mock.patch('crmsh.bootstrap._save_core_hosts')
-    @mock.patch('crmsh.bootstrap._load_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.utils.user_of')
     @mock.patch('crmsh.utils.list_cluster_nodes')
     @mock.patch('crmsh.bootstrap.confirm')
@@ -854,11 +858,10 @@
             self,
             mock_status, mock_ssh,
             mock_qdevice_configured, mock_confirm, mock_list_nodes, 
mock_userof,
-            mock_load_core_hosts, mock_save_core_hosts,
+            mock_host_user_config_class,
     ):
         mock_list_nodes.return_value = []
         bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
current_user="bob", user_list=["alice"])
-        mock_load_core_hosts.return_value = ([], [])
         mock_ssh.return_value = False
         mock_userof.return_value = "bob"
         mock_qdevice_configured.return_value = True
@@ -868,14 +871,13 @@
         bootstrap.init_qdevice()
 
         mock_status.assert_called_once_with("Configure Qdevice/Qnetd:")
-        mock_ssh.assert_called_once_with("bob", "root", "10.10.10.123")
-        mock_save_core_hosts.assert_called_once_with(['root'], 
['10.10.10.123'], sync_to_remote=True)
+        mock_ssh.assert_called_once_with("bob", "bob", "10.10.10.123")
+        
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.patch('crmsh.bootstrap._save_core_hosts')
-    @mock.patch('crmsh.bootstrap._load_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.utils.user_of')
     @mock.patch('crmsh.bootstrap.adjust_priority_fencing_delay')
     @mock.patch('crmsh.bootstrap.adjust_priority_in_rsc_defaults')
@@ -884,9 +886,8 @@
     @mock.patch('crmsh.utils.check_ssh_passwd_need')
     @mock.patch('logging.Logger.info')
     def test_init_qdevice(self, mock_info, mock_ssh, mock_qdevice_configured, 
mock_list_nodes,
-            mock_adjust_priority, mock_adjust_fence_delay, mock_userof, 
mock_load_core_hosts, mock_save_core_hosts):
+            mock_adjust_priority, mock_adjust_fence_delay, mock_userof, 
mock_host_user_config_class):
         bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip, 
current_user="bob", user_list=["alice"])
-        mock_load_core_hosts.return_value = ([], [])
         mock_list_nodes.return_value = []
         mock_ssh.return_value = False
         mock_userof.return_value = "bob"
@@ -898,27 +899,26 @@
         bootstrap.init_qdevice()
 
         mock_info.assert_called_once_with("Configure Qdevice/Qnetd:")
-        mock_ssh.assert_called_once_with("bob", "root", "10.10.10.123")
-        mock_save_core_hosts.assert_called_once_with(['root'], 
['10.10.10.123'], sync_to_remote=True)
+        mock_ssh.assert_called_once_with("bob", "bob", "10.10.10.123")
+        
mock_host_user_config_class.return_value.add.assert_called_once_with('bob', 
'10.10.10.123')
+        
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.patch('crmsh.utils.fatal')
-    @mock.patch('crmsh.bootstrap._save_core_hosts')
-    @mock.patch('crmsh.bootstrap._load_core_hosts')
+    @mock.patch('crmsh.utils.HostUserConfig')
     @mock.patch('crmsh.utils.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_load_core_hosts, mock_save_core_hosts,
+            mock_host_user_config_class,
             mock_fatal,
     ):
         bootstrap._context = mock.Mock(qdevice_inst=self.qdevice_with_ip)
-        mock_load_core_hosts.return_value = ([], [])
         mock_list_nodes.return_value = ["node1"]
         mock_available.return_value = False
         mock_fatal.side_effect = SystemExit
@@ -926,7 +926,8 @@
         with self.assertRaises(SystemExit):
             bootstrap.init_qdevice()
 
-        mock_save_core_hosts.assert_not_called()
+        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:")
@@ -967,7 +968,7 @@
     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_prompt.side_effect = ["qnetd-node", 5403, "ffsplit", "lowest", 
"on", None]
+        mock_prompt.side_effect = ["alice@qnetd-node", 5403, "ffsplit", 
"lowest", "on", None]
         mock_qdevice_inst = mock.Mock()
         mock_qdevice.return_value = mock_qdevice_inst
 
@@ -988,7 +989,7 @@
                 valid_func=qdevice.QDevice.check_qdevice_heuristics,
                 allow_empty=True)
             ])
-        mock_qdevice.assert_called_once_with('qnetd-node', port=5403, 
algo='ffsplit', tie_breaker='lowest', tls='on', cmds=None, mode=None, 
is_stage=False)
+        mock_qdevice.assert_called_once_with('qnetd-node', port=5403, 
ssh_user='alice', algo='ffsplit', tie_breaker='lowest', tls='on', cmds=None, 
mode=None, is_stage=False)
 
     @mock.patch('crmsh.utils.fatal')
     @mock.patch('crmsh.utils.is_qdevice_configured')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_qdevice.py 
new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_qdevice.py
--- old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_qdevice.py    
2023-02-24 10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_qdevice.py    
2023-03-02 07:05:25.000000000 +0100
@@ -422,10 +422,10 @@
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("os.path.exists")
-    @mock.patch("crmsh.parallax.parallax_slurp")
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_local", 
new_callable=mock.PropertyMock)
     def test_fetch_qnetd_crt_from_qnetd_exist(self, mock_qnetd_cacert_local,
-                                              mock_slurp, mock_exists, 
mock_log):
+                                              mock_fetch, mock_exists, 
mock_log):
         mock_qnetd_cacert_local.return_value = 
"/etc/corosync/qdevice/net/10.10.10.123/qnetd-cacert.crt"
         mock_exists.return_value = True
 
@@ -433,31 +433,29 @@
 
         
mock_exists.assert_called_once_with(mock_qnetd_cacert_local.return_value)
         mock_qnetd_cacert_local.assert_called_once_with()
-        mock_slurp.assert_not_called()
+        mock_fetch.assert_not_called()
         mock_log.assert_not_called()
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("os.path.exists")
-    @mock.patch("crmsh.parallax.parallax_slurp")
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_local", 
new_callable=mock.PropertyMock)
     def test_fetch_qnetd_crt_from_qnetd(self, mock_qnetd_cacert_local,
-                                        mock_slurp, mock_exists, mock_log):
+                                        mock_fetch, mock_exists, mock_log):
         mock_qnetd_cacert_local.return_value = 
"/etc/corosync/qdevice/net/10.10.10.123/qnetd-cacert.crt"
         mock_exists.return_value = False
-        mock_slurp.return_value = [("10.10.10.123", (0, None, None, "test"))]
 
         self.qdevice_with_ip.fetch_qnetd_crt_from_qnetd()
 
         
mock_exists.assert_called_once_with(mock_qnetd_cacert_local.return_value)
         mock_qnetd_cacert_local.assert_called_once_with()
         mock_log.assert_called_once_with("Step 2: Fetch qnetd-cacert.crt from 
10.10.10.123")
-        mock_slurp.assert_called_once_with(["10.10.10.123"], 
"/etc/corosync/qdevice/net",
-                                           
"/etc/corosync/qnetd/nssdb/qnetd-cacert.crt")
+        mock_fetch.assert_called_once_with("10.10.10.123", 
"/etc/corosync/qnetd/nssdb/qnetd-cacert.crt", 
"/etc/corosync/qdevice/net/10.10.10.123")
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.list_cluster_nodes")
     @mock.patch("crmsh.utils.this_node")
-    @mock.patch("crmsh.parallax.parallax_copy")
+    @mock.patch("crmsh.qdevice.QDevice._copy_file_to_remote_host")
     def test_copy_qnetd_crt_to_cluster_one_node(self, mock_copy, 
mock_this_node, mock_list_nodes, mock_log):
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com"]
@@ -472,7 +470,7 @@
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.list_cluster_nodes")
     @mock.patch("crmsh.utils.this_node")
-    @mock.patch("crmsh.parallax.parallax_copy")
+    @mock.patch("crmsh.qdevice.QDevice._copy_file_to_remote_host")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_local", 
new_callable=mock.PropertyMock)
     @mock.patch("os.path.dirname")
     def test_copy_qnetd_crt_to_cluster(self, mock_dirname, 
mock_qnetd_cacert_local,
@@ -481,14 +479,13 @@
         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"]
-        mock_copy.return_value = [("node1.com", (0, None, None)), 
("node2.com", (0, None, None))]
 
         self.qdevice_with_ip.copy_qnetd_crt_to_cluster()
 
         mock_this_node.assert_called_once_with()
         mock_list_nodes.assert_called_once_with()
         mock_log.assert_called_once_with("Step 3: Copy exported 
qnetd-cacert.crt to ['node2.com']")
-        mock_copy.assert_called_once_with(["node2.com"], 
mock_dirname.return_value,
+        mock_copy.assert_called_once_with(mock_dirname.return_value, 
"node2.com",
                                           "/etc/corosync/qdevice/net")
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
@@ -521,17 +518,16 @@
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.qdevice.QDevice.qdevice_crq_on_qnetd", 
new_callable=mock.PropertyMock)
     @mock.patch("crmsh.qdevice.QDevice.qdevice_crq_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_copy")
+    @mock.patch("crmsh.qdevice.QDevice._copy_file_to_remote_host")
     def test_copy_crq_to_qnetd(self, mock_copy, mock_qdevice_crq_local,
                                mock_qdevice_crq_qnetd, mock_log):
-        mock_copy.return_value = [("10.10.10.123", (0, None, None))]
         mock_qdevice_crq_local.return_value = 
"/etc/corosync/qdevice/net/nssdb/qdevice-net-node.crq"
         mock_qdevice_crq_qnetd.return_value = 
"/etc/corosync/qnetd/nssdb/qdevice-net-node.crq"
 
         self.qdevice_with_ip.copy_crq_to_qnetd()
 
         mock_log.assert_called_once_with("Step 6: Copy qdevice-net-node.crq to 
10.10.10.123")
-        mock_copy.assert_called_once_with(["10.10.10.123"], 
mock_qdevice_crq_local.return_value,
+        mock_copy.assert_called_once_with(mock_qdevice_crq_local.return_value, 
"10.10.10.123",
                                           mock_qdevice_crq_qnetd.return_value)
         mock_qdevice_crq_local.assert_called_once_with()
         mock_qdevice_crq_qnetd.assert_called_once_with()
@@ -553,18 +549,16 @@
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cluster_crt_on_qnetd", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_slurp")
-    def test_fetch_cluster_crt_from_qnetd(self, mock_slurp, mock_crt_on_qnetd, 
mock_log):
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
+    def test_fetch_cluster_crt_from_qnetd(self, mock_fetch, mock_crt_on_qnetd, 
mock_log):
         mock_crt_on_qnetd.return_value = 
"/etc/corosync/qnetd/nssdb/cluster-hacluster.crt"
-        mock_slurp.return_value = [("10.10.10.123", (0, None, None, "test"))]
 
         self.qdevice_with_ip.cluster_name = "hacluster"
         self.qdevice_with_ip.fetch_cluster_crt_from_qnetd()
 
         mock_log.assert_called_once_with("Step 8: Fetch cluster-hacluster.crt 
from 10.10.10.123")
         mock_crt_on_qnetd.assert_has_calls([mock.call(), mock.call()])
-        mock_slurp.assert_called_once_with(["10.10.10.123"], 
"/etc/corosync/qdevice/net",
-                                           mock_crt_on_qnetd.return_value)
+        mock_fetch.assert_called_once_with("10.10.10.123", 
mock_crt_on_qnetd.return_value, "/etc/corosync/qdevice/net/10.10.10.123",)
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.get_stdout_or_raise_error")
@@ -581,7 +575,7 @@
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.list_cluster_nodes")
     @mock.patch("crmsh.utils.this_node")
-    @mock.patch("crmsh.parallax.parallax_copy")
+    @mock.patch("crmsh.qdevice.QDevice._copy_file_to_remote_host")
     def test_copy_p12_to_cluster_one_node(self, mock_copy, mock_this_node, 
mock_list_nodes, mock_log):
         mock_this_node.return_value = "node1.com"
         mock_list_nodes.return_value = ["node1.com"]
@@ -596,25 +590,21 @@
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.list_cluster_nodes")
     @mock.patch("crmsh.utils.this_node")
-    @mock.patch("crmsh.parallax.parallax_copy")
+    @mock.patch("crmsh.qdevice.QDevice._copy_file_to_remote_hosts")
     @mock.patch("crmsh.qdevice.QDevice.qdevice_p12_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("os.path.dirname")
-    def test_copy_p12_to_cluster(self, mock_dirname, mock_p12_on_local,
+    def test_copy_p12_to_cluster(self, mock_p12_on_local,
                                        mock_copy, mock_this_node, 
mock_list_nodes, mock_log):
         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"
-        mock_dirname.return_value = "/etc/corosync/qdevice/net/nssdb"
-        mock_copy.return_value = [("node1.com", (0, None, None)), 
("node2.com", (0, None, None))]
 
         self.qdevice_with_ip.copy_p12_to_cluster()
 
         mock_log.assert_called_once_with("Step 10: Copy qdevice-net-node.p12 
to ['node2.com']")
         mock_this_node.assert_called_once_with()
         mock_list_nodes.assert_called_once_with()
-        mock_copy.assert_called_once_with(["node2.com"], 
mock_p12_on_local.return_value,
-                                          mock_dirname.return_value)
-        mock_dirname.assert_called_once_with(mock_p12_on_local.return_value)
+        mock_copy.assert_called_once_with(mock_p12_on_local.return_value, 
["node2.com"],
+                                          mock_p12_on_local.return_value)
         mock_p12_on_local.assert_has_calls([mock.call(), mock.call()])
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
@@ -680,8 +670,8 @@
     @mock.patch("os.path.exists")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_cluster", 
new_callable=mock.PropertyMock)
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_slurp")
-    def test_fetch_qnetd_crt_from_cluster_exist(self, mock_slurp, 
mock_qnetd_cacert_local,
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
+    def test_fetch_qnetd_crt_from_cluster_exist(self, mock_fetch, 
mock_qnetd_cacert_local,
                                                 mock_qnetd_cacert_cluster, 
mock_exists, mock_log):
         mock_exists.return_value = True
         mock_qnetd_cacert_cluster.return_value = 
"/etc/corosync/qdevice/net/node1.com/qnetd-cacert.crt"
@@ -692,19 +682,18 @@
         
mock_exists.assert_called_once_with(mock_qnetd_cacert_cluster.return_value)
         mock_qnetd_cacert_cluster.assert_called_once_with()
         mock_qnetd_cacert_local.assert_not_called()
-        mock_slurp.assert_not_called()
+        mock_fetch.assert_not_called()
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("os.path.exists")
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_cluster", 
new_callable=mock.PropertyMock)
     @mock.patch("crmsh.qdevice.QDevice.qnetd_cacert_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_slurp")
-    def test_fetch_qnetd_crt_from_cluster(self, mock_slurp, 
mock_qnetd_cacert_local,
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
+    def test_fetch_qnetd_crt_from_cluster(self, mock_fetch, 
mock_qnetd_cacert_local,
                                           mock_qnetd_cacert_cluster, 
mock_exists, mock_log):
         mock_exists.return_value = False
         mock_qnetd_cacert_cluster.return_value = 
"/etc/corosync/qdevice/net/node1.com/qnetd-cacert.crt"
         mock_qnetd_cacert_local.return_value = 
"/etc/corosync/qdevice/net/10.10.10.123/qnetd-cacert.crt"
-        mock_slurp.return_value = [("node1.com", (0, None, None, "test"))]
 
         self.qdevice_with_ip_cluster_node.fetch_qnetd_crt_from_cluster()
 
@@ -712,8 +701,7 @@
         
mock_exists.assert_called_once_with(mock_qnetd_cacert_cluster.return_value)
         mock_qnetd_cacert_cluster.assert_called_once_with()
         mock_qnetd_cacert_local.assert_called_once_with()
-        mock_slurp.assert_called_once_with(["node1.com"], 
"/etc/corosync/qdevice/net",
-                                           
mock_qnetd_cacert_local.return_value)
+        mock_fetch.assert_called_once_with("node1.com", 
mock_qnetd_cacert_local.return_value, '/etc/corosync/qdevice/net/node1.com')
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.get_stdout_or_raise_error")
@@ -732,8 +720,8 @@
     @mock.patch("os.path.exists")
     @mock.patch("crmsh.qdevice.QDevice.qdevice_p12_on_cluster", 
new_callable=mock.PropertyMock)
     @mock.patch("crmsh.qdevice.QDevice.qdevice_p12_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_slurp")
-    def test_fetch_p12_from_cluster_exist(self, mock_slurp, mock_p12_on_local,
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
+    def test_fetch_p12_from_cluster_exist(self, mock_fetch, mock_p12_on_local,
                                           mock_p12_on_cluster, mock_exists, 
mock_log):
         mock_exists.return_value = True
         mock_p12_on_cluster.return_value = 
"/etc/corosync/qdevice/net/node1.com/qdevice-net-node.p12"
@@ -744,19 +732,18 @@
         mock_exists.assert_called_once_with(mock_p12_on_cluster.return_value)
         mock_p12_on_cluster.assert_called_once_with()
         mock_p12_on_local.assert_not_called()
-        mock_slurp.assert_not_called()
+        mock_fetch.assert_not_called()
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("os.path.exists")
     @mock.patch("crmsh.qdevice.QDevice.qdevice_p12_on_cluster", 
new_callable=mock.PropertyMock)
     @mock.patch("crmsh.qdevice.QDevice.qdevice_p12_on_local", 
new_callable=mock.PropertyMock)
-    @mock.patch("crmsh.parallax.parallax_slurp")
-    def test_fetch_p12_from_cluster(self, mock_slurp, mock_p12_on_local,
+    @mock.patch("crmsh.qdevice.QDevice._fetch_file_to_local")
+    def test_fetch_p12_from_cluster(self, mock_fetch, mock_p12_on_local,
                                     mock_p12_on_cluster, mock_exists, 
mock_log):
         mock_exists.return_value = False
         mock_p12_on_cluster.return_value = 
"/etc/corosync/qdevice/net/node1.com/qdevice-net-node.p12"
         mock_p12_on_local.return_value = 
"/etc/corosync/qdevice/net/nssdb/qdevice-net-node.p12"
-        mock_slurp.return_value = [("node1.com", (0, None, None, "test"))]
 
         self.qdevice_with_ip_cluster_node.fetch_p12_from_cluster()
 
@@ -764,8 +751,7 @@
         mock_exists.assert_called_once_with(mock_p12_on_cluster.return_value)
         mock_p12_on_cluster.assert_called_once_with()
         mock_p12_on_local.assert_called_once_with()
-        mock_slurp.assert_called_once_with(["node1.com"], 
"/etc/corosync/qdevice/net",
-                                           mock_p12_on_local.return_value)
+        mock_fetch.assert_called_once_with("node1.com", 
mock_p12_on_local.return_value, "/etc/corosync/qdevice/net/node1.com")
 
     @mock.patch("crmsh.log.LoggerUtils.log_only_to_file")
     @mock.patch("crmsh.utils.get_stdout_or_raise_error")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_utils.py 
new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_utils.py
--- old/crmsh-4.4.1+20230224.498677ab/test/unittests/test_utils.py      
2023-02-24 10:04:38.000000000 +0100
+++ new/crmsh-4.4.1+20230302.2b5310b9/test/unittests/test_utils.py      
2023-03-02 07:05:25.000000000 +0100
@@ -1843,9 +1843,13 @@
     mock_run.assert_called_once_with("sudo -S -k -n id -u")
 
 
+@mock.patch('grp.getgrgid')
 @mock.patch('os.getgroups')
-def test_in_haclient(mock_group):
+def test_in_haclient(mock_group, mock_getgrgid):
     mock_group.return_value = [90, 100]
+    mock_getgrgid_inst1 = mock.Mock(gr_name=constants.HA_GROUP)
+    mock_getgrgid_inst2 = mock.Mock(gr_name="other")
+    mock_getgrgid.side_effect = [mock_getgrgid_inst1, mock_getgrgid_inst2]
     assert utils.in_haclient() is True
     mock_group.assert_called_once_with()
 

Reply via email to