Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package crmsh for openSUSE:Factory checked 
in at 2022-07-28 20:59:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.1533 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Thu Jul 28 20:59:04 2022 rev:250 rq:991514 version:4.4.0+20220728.3f249756

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2022-07-22 
19:21:31.784689089 +0200
+++ /work/SRC/openSUSE:Factory/.crmsh.new.1533/crmsh.changes    2022-07-28 
20:59:30.275714523 +0200
@@ -1,0 +2,12 @@
+Thu Jul 28 08:14:15 UTC 2022 - xli...@suse.com
+
+- Update to version 4.4.0+20220728.3f249756:
+  * Dev: ui_cluster: Change the dest of -N option as node_list
+  * Update crmsh/ui_cluster.py
+  * Dev: unittest: Adjust unit test for previous changes
+  * Dev: behave: adjust functional test based on previous changes
+  * Dev: doc: remove cluster add in doc
+  * Dev: bootstrap: remove cluster add sub-command
+  * Fix: bootstrap: -N option setup the current node and peers all together 
(bsc#1175863)
+
+-------------------------------------------------------------------

Old:
----
  crmsh-4.4.0+20220711.573ebb98.tar.bz2

New:
----
  crmsh-4.4.0+20220728.3f249756.tar.bz2

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

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.xb5now/_old  2022-07-28 20:59:30.795715988 +0200
+++ /var/tmp/diff_new_pack.xb5now/_new  2022-07-28 20:59:30.803716010 +0200
@@ -36,7 +36,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        4.4.0+20220711.573ebb98
+Version:        4.4.0+20220728.3f249756
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.xb5now/_old  2022-07-28 20:59:30.867716191 +0200
+++ /var/tmp/diff_new_pack.xb5now/_new  2022-07-28 20:59:30.871716201 +0200
@@ -9,7 +9,7 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">573ebb9879786925d04ce19017e06179936d833d</param>
+  <param 
name="changesrevision">3f249756dbcd5c5d2548e27b992df76cd3b38cb4</param>
 </service>
 </servicedata>
 (No newline at EOF)

++++++ crmsh-4.4.0+20220711.573ebb98.tar.bz2 -> 
crmsh-4.4.0+20220728.3f249756.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.0+20220711.573ebb98/crmsh/bootstrap.py 
new/crmsh-4.4.0+20220728.3f249756/crmsh/bootstrap.py
--- old/crmsh-4.4.0+20220711.573ebb98/crmsh/bootstrap.py        2022-07-11 
08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/crmsh/bootstrap.py        2022-07-28 
09:57:44.000000000 +0200
@@ -86,7 +86,8 @@
         self.cluster_name = None
         self.watchdog = None
         self.no_overwrite_sshkey = None
-        self.nic_list = None
+        self.nic_list = []
+        self.node_list = []
         self.unicast = None
         self.multicast = None
         self.admin_ip = None
@@ -111,7 +112,7 @@
         self.clusters = None
         self.tickets = None
         self.sbd_manager = None
-        self.sbd_devices = None
+        self.sbd_devices = []
         self.diskless_sbd = None
         self.stage = None
         self.args = None
@@ -167,6 +168,21 @@
             if self.cluster_is_running:
                 utils.check_all_nodes_reachable()
 
+    def _validate_nodes_option(self):
+        """
+        Validate -N/--nodes option
+        """
+        self.node_list = utils.parse_append_action_argument(self.node_list)
+        me = utils.this_node()
+        if me in self.node_list:
+            self.node_list.remove(me)
+        if self.node_list and self.stage:
+            utils.fatal("Can't use -N/--nodes option and stage({}) 
together".format(self.stage))
+        if utils.has_dup_value(self.node_list):
+            utils.fatal("Duplicated input for -N/--nodes option")
+        for node in self.node_list:
+            utils.ping_node(node)
+
     def validate_option(self):
         """
         Validate options
@@ -178,14 +194,15 @@
         if self.nic_list:
             if len(self.nic_list) > 2:
                 utils.fatal("Maximum number of interface is 2")
-            if len(self.nic_list) != len(set(self.nic_list)):
-                utils.fatal("Duplicated input")
+            if utils.has_dup_value(self.nic_list):
+                utils.fatal("Duplicated input for -i/--interface option")
         if self.no_overwrite_sshkey:
             logger.warning("--no-overwrite-sshkey option is deprecated since 
crmsh does not overwrite ssh keys by default anymore and will be removed in 
future versions")
         if self.type == "join" and self.watchdog:
             logger.warning("-w option is deprecated and will be removed in 
future versions")
         if self.ocfs2_devices or self.stage == "ocfs2":
             ocfs2.OCFS2Manager.verify_ocfs2(self)
+        self._validate_nodes_option()
         self._validate_sbd_option()
 
     def init_sbd_manager(self):
@@ -688,19 +705,27 @@
                 dst.write(line)
 
 
-def append(fromfile, tofile):
-    logger_utils.log_only_to_file("+ cat %s >> %s" % (fromfile, tofile))
-    with open(tofile, "a") as tf:
-        with open(fromfile, "r") as ff:
-            tf.write(ff.read())
+def append(fromfile, tofile, remote=None):
+    cmd = "cat {} >> {}".format(fromfile, tofile)
+    utils.get_stdout_or_raise_error(cmd, remote=remote)
 
 
-def append_unique(fromfile, tofile):
+def append_unique(fromfile, tofile, remote=None, from_local=False):
     """
     Append unique content from fromfile to tofile
-    """
-    if not utils.check_file_content_included(fromfile, tofile):
-        append(fromfile, tofile)
+    
+    if from_local and remote:
+        append local fromfile to remote tofile
+    elif remote:
+        append remote fromfile to remote tofile
+    if not remote:
+        append fromfile to tofile, locally
+    """
+    if not utils.check_file_content_included(fromfile, tofile, remote=remote, 
source_local=from_local):
+        if from_local and remote:
+            append_to_remote_file(fromfile, remote, tofile)
+        else:
+            append(fromfile, tofile, remote=remote)
 
 
 def rmfile(path, ignore_errors=False):
@@ -735,7 +760,29 @@
     """
     utils.start_service("sshd.service", enable=True)
     for user in USER_LIST:
-        configure_local_ssh_key(user)
+        configure_ssh_key(user)
+
+    # If not use -N/--nodes option
+    if not _context.node_list:
+        return
+
+    print()
+    node_list = _context.node_list
+    # Swap public ssh key between remote node and local
+    for node in node_list:
+        swap_public_ssh_key(node, add=True)
+        if utils.service_is_active("pacemaker.service", node):
+            utils.fatal("Cluster is currently active on {} - can't 
run".format(node))
+    # Swap public ssh key between one remote node and other remote nodes
+    if len(node_list) > 1:
+        _, _, authorized_file = key_files("root").values()
+        for node in node_list:
+            public_key_file_remote = fetch_public_key_from_remote_node(node)
+            for other_node in node_list:
+                if other_node == node:
+                    continue
+                append_unique(public_key_file_remote, authorized_file, 
remote=other_node, from_local=True)
+    print()
 
 
 def key_files(user):
@@ -772,9 +819,9 @@
         invoke("usermod -s /bin/bash {}".format(user))
 
 
-def configure_local_ssh_key(user="root"):
+def configure_ssh_key(user="root", remote=None):
     """
-    Configure ssh rsa key locally
+    Configure ssh rsa key on local or remote
 
     If <home_dir>/.ssh/id_rsa not exist, generate a new one
     Add <home_dir>/.ssh/id_rsa.pub to <home_dir>/.ssh/authorized_keys anyway, 
make sure itself authorized
@@ -782,17 +829,17 @@
     change_user_shell(user)
 
     private_key, public_key, authorized_file = key_files(user).values()
-    if not os.path.exists(private_key):
-        logger.info("Generating SSH key for {}".format(user))
-        cmd = "ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N 
''".format(private_key, utils.this_node())
+    if not utils.detect_file(private_key, remote=remote):
+        logger.info("SSH key for {} does not exist, hence generate it 
now".format(user))
+        cmd = "ssh-keygen -q -f {} -C 'Cluster Internal on {}' -N 
''".format(private_key, remote if remote else utils.this_node())
         cmd = utils.add_su(cmd, user)
-        rc, _, err = invoke(cmd)
-        if not rc:
-            utils.fatal("Failed to generate ssh key for {}: {}".format(user, 
err))
+        utils.get_stdout_or_raise_error(cmd, remote=remote)
+
+    if not utils.detect_file(authorized_file, remote=remote):
+        cmd = "touch {}".format(authorized_file)
+        utils.get_stdout_or_raise_error(cmd, remote=remote)
 
-    if not os.path.exists(authorized_file):
-        open(authorized_file, 'w').close()
-    append_unique(public_key, authorized_file)
+    append_unique(public_key, authorized_file, remote=remote)
 
 
 def init_ssh_remote():
@@ -813,19 +860,28 @@
             append(fn + ".pub", authorized_keys_file)
 
 
-def append_to_remote_file(fromfile, remote_node, tofile):
+def copy_ssh_key(source_key, user, remote_node):
     """
-    Append content of fromfile to tofile on remote_node
+    Copy ssh key from local to remote's authorized_keys
     """
     err_details_string = """
-    crmsh has no way to help you to setup up passwordless ssh among nodes at 
this time. 
-    As the hint, likely, `PasswordAuthentication` is 'no' in 
/etc/ssh/sshd_config. 
+    crmsh has no way to help you to setup up passwordless ssh among nodes at 
this time.
+    As the hint, likely, `PasswordAuthentication` is 'no' in 
/etc/ssh/sshd_config.
     Given in this case, users must setup passwordless ssh beforehand, or 
change it to 'yes' and manage passwords properly
     """
+    cmd = "ssh-copy-id -i {} {}@{}".format(source_key, user, remote_node)
+    try:
+        utils.get_stdout_or_raise_error(cmd)
+    except ValueError as err:
+        utils.fatal("{}\n{}".format(str(err), err_details_string))
+
+
+def append_to_remote_file(fromfile, remote_node, tofile):
+    """
+    Append content of fromfile to tofile on remote_node
+    """
     cmd = "cat {} | ssh {} root@{} 'cat >> {}'".format(fromfile, SSH_OPTION, 
remote_node, tofile)
-    rc, _, err = invoke(cmd)
-    if not rc:
-        utils.fatal("Failed to append contents of {} to 
{}:\n\"{}\"\n{}".format(fromfile, remote_node, err, err_details_string))
+    utils.get_stdout_or_raise_error(cmd)
 
 
 def init_csync2():
@@ -1321,7 +1377,7 @@
 
     utils.start_service("sshd.service", enable=True)
     for user in USER_LIST:
-        configure_local_ssh_key(user)
+        configure_ssh_key(user)
         swap_public_ssh_key(seed_host, user)
 
     # This makes sure the seed host has its own SSH keys in its own
@@ -1333,7 +1389,7 @@
         utils.fatal("Can't invoke crm cluster init -i {} ssh_remote on {}: 
{}".format(_context.default_nic_list[0], seed_host, err))
 
 
-def swap_public_ssh_key(remote_node, user="root"):
+def swap_public_ssh_key(remote_node, user="root", add=False):
     """
     Swap public ssh key between remote_node and local
     """
@@ -1346,7 +1402,13 @@
         # If no passwordless configured, paste /root/.ssh/id_rsa.pub to 
remote_node's /root/.ssh/authorized_keys
         logger.info("Configuring SSH passwordless with {}@{}".format(user, 
remote_node))
         # After this, login to remote_node is passwordless
-        append_to_remote_file(public_key, remote_node, authorized_file)
+        if user == "root":
+            copy_ssh_key(public_key, user, remote_node)
+        else:
+            append_to_remote_file(public_key, remote_node, authorized_file)
+
+    if add:
+        configure_ssh_key(remote=remote_node)
 
     try:
         # Fetch public key file from remote_node
@@ -1680,7 +1742,6 @@
                 break
             if not rrp_flag:
                 break
-        print("")
         invoke("rm -f /var/lib/heartbeat/crm/* /var/lib/pacemaker/cib/*")
         try:
             corosync.add_node_ucast(ringXaddr_res)
@@ -1911,19 +1972,16 @@
     # vgfs stage requires running cluster, everything else requires inactive 
cluster,
     # except ssh and csync2 (which don't care) and csync2_remote (which 
mustn't care,
     # just in case this breaks ha-cluster-join on another node).
-    corosync_active = utils.service_is_active("corosync.service")
     if stage in ("vgfs", "admin", "qdevice", "ocfs2"):
-        if not corosync_active:
+        if not _context.cluster_is_running:
             utils.fatal("Cluster is inactive - can't run %s stage" % (stage))
     elif stage == "":
-        if corosync_active:
+        if _context.cluster_is_running:
             utils.fatal("Cluster is currently active - can't run")
     elif stage not in ("ssh", "ssh_remote", "csync2", "csync2_remote", "sbd", 
"ocfs2"):
-        if corosync_active:
+        if _context.cluster_is_running:
             utils.fatal("Cluster is currently active - can't run %s stage" % 
(stage))
 
-    _context.initialize_qdevice()
-    _context.validate_option()
     _context.load_profiles()
     _context.init_sbd_manager()
 
@@ -1961,6 +2019,29 @@
     bootstrap_finished()
 
 
+def bootstrap_add(context):
+    """
+    Adds the given node to the cluster.
+    """
+    if not context.node_list:
+        return
+
+    global _context
+    _context = context
+
+    options = ""
+    for nic in _context.nic_list:
+        options += '-i {} '.format(nic)
+    options = " {}".format(options.strip()) if options else ""
+
+    for node in _context.node_list:
+        print()
+        logger.info("Adding node {} to cluster".format(node))
+        cmd = "crm cluster join{} -c {}{}".format(" -y" if _context.yes_to_all 
else "", utils.this_node(), options)
+        logger.info("Running command on {}: {}".format(node, cmd))
+        utils.ext_cmd_nosudo("ssh{} root@{} {} '{}'".format("" if 
_context.yes_to_all else " -t", node, SSH_OPTION, cmd))
+
+
 def bootstrap_join(context):
     """
     Join cluster process
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.0+20220711.573ebb98/crmsh/ui_cluster.py 
new/crmsh-4.4.0+20220728.3f249756/crmsh/ui_cluster.py
--- old/crmsh-4.4.0+20220711.573ebb98/crmsh/ui_cluster.py       2022-07-11 
08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/crmsh/ui_cluster.py       2022-07-28 
09:57:44.000000000 +0200
@@ -336,9 +336,8 @@
                             help='Answer "yes" to all prompts (use with 
caution, this is destructive, especially those storage related configurations 
and stages.)')
         parser.add_argument("-n", "--name", metavar="NAME", 
dest="cluster_name", default="hacluster",
                             help='Set the name of the configured cluster.')
-        parser.add_argument("-N", "--nodes", metavar="NODES", dest="nodes",
-                            help='Additional nodes to add to the created 
cluster. May include the current node, which will always be the initial cluster 
node.')
-        # parser.add_argument("--quick-start", dest="quickstart", 
action="store_true", help="Perform basic system configuration (NTP, watchdog, 
/etc/hosts)")
+        parser.add_argument("-N", "--node", metavar="NODENAME", 
dest="node_list", action="append", default=[],
+                            help='The member node of the cluster. Note: the 
current node is always get initialized during bootstrap in the beginning.')
         parser.add_argument("-S", "--enable-sbd", dest="diskless_sbd", 
action="store_true",
                             help="Enable SBD even if no SBD device is 
configured (diskless mode)")
         parser.add_argument("-w", "--watchdog", dest="watchdog", 
metavar="WATCHDOG",
@@ -347,7 +346,7 @@
                             help='Avoid "/root/.ssh/id_rsa" overwrite if "-y" 
option is used (False by default; Deprecated)')
 
         network_group = parser.add_argument_group("Network configuration", 
"Options for configuring the network and messaging layer.")
-        network_group.add_argument("-i", "--interface", dest="nic_list", 
metavar="IF", action="append", choices=utils.interface_choice(),
+        network_group.add_argument("-i", "--interface", dest="nic_list", 
metavar="IF", action="append", choices=utils.interface_choice(), default=[],
                                    help="Bind to IP address on interface IF. 
Use -i second time for second interface")
         network_group.add_argument("-u", "--unicast", action="store_true", 
dest="unicast",
                                    help="Configure corosync to communicate 
over unicast(udpu). This is the default transport type")
@@ -377,7 +376,7 @@
                                    help="MODE of operation of heuristics 
(on/sync/off, default:sync)")
 
         storage_group = parser.add_argument_group("Storage configuration", 
"Options for configuring shared storage.")
-        storage_group.add_argument("-s", "--sbd-device", dest="sbd_devices", 
metavar="DEVICE", action="append",
+        storage_group.add_argument("-s", "--sbd-device", dest="sbd_devices", 
metavar="DEVICE", action="append", default=[],
                                    help="Block device to use for SBD fencing, 
use \";\" as separator or -s multiple times for multi path (up to 3 devices)")
         storage_group.add_argument("-o", "--ocfs2-device", 
dest="ocfs2_devices", metavar="DEVICE", action="append", default=[],
                 help="Block device to use for OCFS2; When using Cluster LVM2 
to manage the shared storage, user can specify one or multiple raw disks, use 
\";\" as separator or -o multiple times for multi path (must specify -C option) 
NOTE: this is a Technical Preview")
@@ -414,20 +413,11 @@
         boot_context.args = args
         boot_context.cluster_is_running = 
utils.service_is_active("pacemaker.service")
         boot_context.type = "init"
+        boot_context.initialize_qdevice()
+        boot_context.validate_option()
 
         bootstrap.bootstrap_init(boot_context)
-
-        # if options.geo:
-        #    bootstrap.bootstrap_init_geo()
-
-        if options.nodes is not None:
-            nodelist = [n for n in re.split('[ ,;]+', options.nodes)]
-            for node in nodelist:
-                if node == utils.this_node():
-                    continue
-                logger.info("\n\nAdd node {} (may prompt for root 
password):".format(node))
-                if not self._add_node(node, yes_to_all=options.yes_to_all):
-                    return False
+        bootstrap.bootstrap_add(boot_context)
 
         return True
 
@@ -465,7 +455,7 @@
 
         network_group = parser.add_argument_group("Network configuration", 
"Options for configuring the network and messaging layer.")
         network_group.add_argument("-c", "--cluster-node", 
dest="cluster_node", help="IP address or hostname of existing cluster node", 
metavar="HOST")
-        network_group.add_argument("-i", "--interface", dest="nic_list", 
metavar="IF", action="append", choices=utils.interface_choice(),
+        network_group.add_argument("-i", "--interface", dest="nic_list", 
metavar="IF", action="append", choices=utils.interface_choice(), default=[],
                 help="Bind to IP address on interface IF. Use -i second time 
for second interface")
         options, args = parse_options(parser, args)
         if options is None or args is None:
@@ -486,37 +476,6 @@
 
         return True
 
-    def _add_node(self, node, yes_to_all=False):
-        '''
-        Adds the given node to the cluster.
-        '''
-        me = utils.this_node()
-        cmd = "crm cluster join{} -c {}".format(" -y" if yes_to_all else "", 
me)
-        rc = utils.ext_cmd_nosudo("ssh{} root@{} -o StrictHostKeyChecking=no 
'{}'".format("" if yes_to_all else " -t", node, cmd))
-        return rc == 0
-
-    @command.completers_repeating(compl.call(scripts.param_completion_list, 
'add'))
-    @command.skill_level('administrator')
-    def do_add(self, context, *args):
-        '''
-        Add the given node(s) to the cluster.
-        Installs packages, sets up corosync and pacemaker, etc.
-        Must be executed from a node in the existing cluster.
-        '''
-        parser = ArgParser(description="""
-Add a new node to the cluster. The new node will be
-configured as a cluster member.""",
-                usage="add [options] [node ...]", add_help=False, 
formatter_class=RawDescriptionHelpFormatter)
-        parser.add_argument("-h", "--help", action="store_true", dest="help", 
help="Show this help message")
-        parser.add_argument("-y", "--yes", help='Answer "yes" to all prompts 
(use with caution)', action="store_true", dest="yes_to_all")
-        options, args = parse_options(parser, args)
-        if options is None or args is None:
-            return
-
-        for node in args:
-            if not self._add_node(node, yes_to_all=options.yes_to_all):
-                return False
-
     @command.alias("delete")
     @command.completers_repeating(_remove_completer)
     @command.skill_level('administrator')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.0+20220711.573ebb98/crmsh/utils.py 
new/crmsh-4.4.0+20220728.3f249756/crmsh/utils.py
--- old/crmsh-4.4.0+20220711.573ebb98/crmsh/utils.py    2022-07-11 
08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/crmsh/utils.py    2022-07-28 
09:57:44.000000000 +0200
@@ -2486,19 +2486,19 @@
         return False
 
 
-def check_file_content_included(source_file, target_file):
+def check_file_content_included(source_file, target_file, remote=None, 
source_local=False):
     """
     Check whether target_file includes contents of source_file
     """
-    if not os.path.exists(source_file):
+    if not detect_file(source_file, remote=None if source_local else remote):
         raise ValueError("File {} not exist".format(source_file))
-    if not os.path.exists(target_file):
+    if not detect_file(target_file, remote=remote):
         return False
 
-    with open(target_file, 'r') as target_fd:
-        target_data = target_fd.read()
-    with open(source_file, 'r') as source_fd:
-        source_data = source_fd.read()
+    cmd = "cat {}".format(target_file)
+    target_data = get_stdout_or_raise_error(cmd, remote=remote)
+    cmd = "cat {}".format(source_file)
+    source_data = get_stdout_or_raise_error(cmd, remote=None if source_local 
else remote)
     return source_data in target_data
 
 
@@ -3106,4 +3106,22 @@
     with open(infile, 'rt', encoding='utf-8', errors='replace') as f:
         data = f.read()
     return to_ascii(data)
+
+
+def has_dup_value(_list):
+    return _list and len(_list) != len(set(_list))
+
+
+def detect_file(_file, remote=None):
+    """
+    Detect if file exists, support both local and remote
+    """
+    rc = False
+    if not remote:
+        rc = os.path.exists(_file)
+    else:
+        cmd = "ssh {} root@{} 'test -f {}'".format(SSH_OPTION, remote, _file)
+        code, _, _ = get_stdout_stderr(cmd)
+        rc = code == 0
+    return rc
 # vim:ts=4:sw=4:et:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.0+20220711.573ebb98/doc/crm.8.adoc 
new/crmsh-4.4.0+20220728.3f249756/doc/crm.8.adoc
--- old/crmsh-4.4.0+20220711.573ebb98/doc/crm.8.adoc    2022-07-11 
08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/doc/crm.8.adoc    2022-07-28 
09:57:44.000000000 +0200
@@ -946,10 +946,6 @@
 cluster, by providing support for package installation, configuration
 of the cluster messaging layer, file system setup and more.
 
-[[cmdhelp_cluster_add,Add a new node to the cluster,From Code]]
-==== `add`
-See "crm cluster help add" or "crm cluster add --help"
-
 [[cmdhelp_cluster_copy,Copy file to other cluster nodes]]
 ==== `copy`
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.0+20220711.573ebb98/test/features/bootstrap_options.feature 
new/crmsh-4.4.0+20220728.3f249756/test/features/bootstrap_options.feature
--- old/crmsh-4.4.0+20220711.573ebb98/test/features/bootstrap_options.feature   
2022-07-11 08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/test/features/bootstrap_options.feature   
2022-07-28 09:57:44.000000000 +0200
@@ -2,7 +2,7 @@
 Feature: crmsh bootstrap process - options
 
   Test crmsh bootstrap options:
-      "--nodes": Additional nodes to add to the created cluster
+      "--node": Additional nodes to add to the created cluster
       "-i":      Bind to IP address on interface IF
       "-M":      Configure corosync with second heartbeat line
       "-n":      Set the name of the configured cluster
@@ -20,8 +20,6 @@
     Then    Output is the same with expected "crm cluster init" help output
     When    Run "crm cluster join -h" on "hanode1"
     Then    Output is the same with expected "crm cluster join" help output
-    When    Run "crm cluster add -h" on "hanode1"
-    Then    Output is the same with expected "crm cluster add" help output
     When    Run "crm cluster remove -h" on "hanode1"
     Then    Output is the same with expected "crm cluster remove" help output
     When    Run "crm cluster geo_init -h" on "hanode1"
@@ -32,10 +30,10 @@
     Then    Output is the same with expected "crm cluster geo-init-arbitrator" 
help output
 
   @clean
-  Scenario: Init whole cluster service on node "hanode1" using "--nodes" option
+  Scenario: Init whole cluster service on node "hanode1" using "--node" option
     Given   Cluster service is "stopped" on "hanode1"
     And     Cluster service is "stopped" on "hanode2"
-    When    Run "crm cluster init -y --nodes \"hanode1 hanode2\"" on "hanode1"
+    When    Run "crm cluster init -y --node \"hanode1 hanode2\"" on "hanode1"
     Then    Cluster service is "started" on "hanode1"
     And     Cluster service is "started" on "hanode2"
     And     Online nodes are "hanode1 hanode2"
@@ -131,3 +129,11 @@
     Then    Cluster service is "started" on "hanode2"
     And     Show cluster status on "hanode1"
     And     Corosync working on "multicast" mode
+
+  @clean
+  Scenario: Init cluster with -N option (bsc#1175863)
+    Given   Cluster service is "stopped" on "hanode1"
+    Given   Cluster service is "stopped" on "hanode2"
+    When    Run "crm cluster init -N hanode1 -N hanode2 -y" on "hanode1"
+    Then    Cluster service is "started" on "hanode1"
+    And     Cluster service is "started" on "hanode2"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.0+20220711.573ebb98/test/features/steps/const.py 
new/crmsh-4.4.0+20220728.3f249756/test/features/steps/const.py
--- old/crmsh-4.4.0+20220711.573ebb98/test/features/steps/const.py      
2022-07-11 08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/test/features/steps/const.py      
2022-07-28 09:57:44.000000000 +0200
@@ -67,10 +67,10 @@
                         destructive, especially those storage related
                         configurations and stages.)
   -n NAME, --name NAME  Set the name of the configured cluster.
-  -N NODES, --nodes NODES
-                        Additional nodes to add to the created cluster. May
-                        include the current node, which will always be the
-                        initial cluster node.
+  -N NODENAME, --node NODENAME
+                        The member node of the cluster. Note: the current node
+                        is always get initialized during bootstrap in the
+                        beginning.
   -S, --enable-sbd      Enable SBD even if no SBD device is configured
                         (diskless mode)
   -w WATCHDOG, --watchdog WATCHDOG
@@ -234,16 +234,6 @@
   crm cluster join -c <node> -i eth1 -i eth2 -y'''
 
 
-CRM_CLUSTER_ADD_H_OUTPUT = '''usage: add [options] [node ...]
-
-Add a new node to the cluster. The new node will be
-configured as a cluster member.
-
-optional arguments:
-  -h, --help  Show this help message
-  -y, --yes   Answer "yes" to all prompts (use with caution)'''
-
-
 CRM_CLUSTER_REMOVE_H_OUTPUT = '''usage: remove [options] [<node> ...]
 
 Remove one or more nodes from the cluster.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.0+20220711.573ebb98/test/features/steps/step_implementation.py 
new/crmsh-4.4.0+20220728.3f249756/test/features/steps/step_implementation.py
--- 
old/crmsh-4.4.0+20220711.573ebb98/test/features/steps/step_implementation.py    
    2022-07-11 08:41:22.000000000 +0200
+++ 
new/crmsh-4.4.0+20220728.3f249756/test/features/steps/step_implementation.py    
    2022-07-28 09:57:44.000000000 +0200
@@ -276,7 +276,6 @@
     cmd_help["crm"] = const.CRM_H_OUTPUT
     cmd_help["crm_cluster_init"] = const.CRM_CLUSTER_INIT_H_OUTPUT
     cmd_help["crm_cluster_join"] = const.CRM_CLUSTER_JOIN_H_OUTPUT
-    cmd_help["crm_cluster_add"] = const.CRM_CLUSTER_ADD_H_OUTPUT
     cmd_help["crm_cluster_remove"] = const.CRM_CLUSTER_REMOVE_H_OUTPUT
     cmd_help["crm_cluster_geo-init"] = const.CRM_CLUSTER_GEO_INIT_H_OUTPUT
     cmd_help["crm_cluster_geo-join"] = const.CRM_CLUSTER_GEO_JOIN_H_OUTPUT
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.0+20220711.573ebb98/test/unittests/test_bootstrap.py 
new/crmsh-4.4.0+20220728.3f249756/test/unittests/test_bootstrap.py
--- old/crmsh-4.4.0+20220711.573ebb98/test/unittests/test_bootstrap.py  
2022-07-11 08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/test/unittests/test_bootstrap.py  
2022-07-28 09:57:44.000000000 +0200
@@ -120,14 +120,16 @@
             ctx.validate_option()
         mock_error.assert_called_once_with("Maximum number of interface is 2")
 
+    @mock.patch('crmsh.utils.has_dup_value')
     @mock.patch('crmsh.utils.fatal')
-    def test_validate_option_error_nic_dup(self, mock_error):
+    def test_validate_option_error_nic_dup(self, mock_error, mock_dup):
+        mock_dup.return_value = True
         mock_error.side_effect = SystemExit
         options = mock.Mock(nic_list=["eth2", "eth2"])
         ctx = self.ctx_inst.set_context(options)
         with self.assertRaises(SystemExit):
             ctx.validate_option()
-        mock_error.assert_called_once_with("Duplicated input")
+        mock_error.assert_called_once_with("Duplicated input for 
-i/--interface option")
 
     @mock.patch('logging.Logger.warning')
     @mock.patch('crmsh.bootstrap.Validation.valid_admin_ip')
@@ -277,9 +279,10 @@
         mock_long.assert_called_once_with('Starting pacemaker(delaying start 
of sbd for 60s)')
         mock_start.assert_called_once_with('pacemaker.service', enable=False, 
node_list=[])
 
-    @mock.patch('crmsh.bootstrap.configure_local_ssh_key')
+    @mock.patch('crmsh.bootstrap.configure_ssh_key')
     @mock.patch('crmsh.utils.start_service')
     def test_init_ssh(self, mock_start_service, mock_config_ssh):
+        bootstrap._context = mock.Mock(node_list=[])
         bootstrap.init_ssh()
         mock_start_service.assert_called_once_with("sshd.service", enable=True)
         mock_config_ssh.assert_has_calls([
@@ -325,79 +328,74 @@
         mock_nologin.assert_called_once_with("hacluster")
         mock_invoke.assert_called_once_with("usermod -s /bin/bash hacluster")
 
-    @mock.patch('crmsh.utils.this_node')
-    @mock.patch('crmsh.utils.fatal')
-    @mock.patch('crmsh.bootstrap.invoke')
+    @mock.patch('crmsh.bootstrap.append_unique')
+    @mock.patch('crmsh.utils.get_stdout_or_raise_error')
+    @mock.patch('crmsh.utils.add_su')
     @mock.patch('logging.Logger.info')
-    @mock.patch('os.path.exists')
+    @mock.patch('crmsh.utils.detect_file')
     @mock.patch('crmsh.bootstrap.key_files')
     @mock.patch('crmsh.bootstrap.change_user_shell')
-    def test_configure_local_ssh_key_error(self, mock_change_shell, 
mock_key_files, mock_exists, mock_status, mock_invoke, mock_error, 
mock_this_node):
-        mock_key_files.return_value = {"private": "/root/.ssh/id_rsa", 
"public": "/root/.ssh/id_rsa.pub", "authorized": "/root/.ssh/authorized_keys"}
-        mock_exists.return_value = False
-        mock_invoke.return_value = (False, None, "error")
-        mock_this_node.return_value = "node1"
-        mock_error.side_effect = SystemExit
+    def test_configure_ssh_key_remote(self, mock_change_shell, mock_key_files, 
mock_detect, mock_info, mock_su, mock_run, mock_append):
+        mock_key_files.return_value = {"private": "/test/.ssh/id_rsa", 
"public": "/test/.ssh/id_rsa.pub", "authorized": "/test/.ssh/authorized_keys"}
+        mock_detect.side_effect = [False, True]
+        mock_su.return_value = "cmd with su"
 
-        with self.assertRaises(SystemExit) as err:
-            bootstrap.configure_local_ssh_key("root")
+        bootstrap.configure_ssh_key("test", remote="node1")
 
-        mock_change_shell.assert_called_once_with("root")
-        mock_key_files.assert_called_once_with("root")
-        mock_exists.assert_called_once_with("/root/.ssh/id_rsa")
-        mock_status.assert_called_once_with("Generating SSH key for root")
-        mock_invoke.assert_called_once_with("ssh-keygen -q -f 
/root/.ssh/id_rsa -C 'Cluster Internal on node1' -N ''")
-        mock_error.assert_called_once_with("Failed to generate ssh key for 
root: error")
+        mock_change_shell.assert_called_once_with("test")
+        mock_key_files.assert_called_once_with("test")
+        mock_detect.assert_has_calls([
+            mock.call("/test/.ssh/id_rsa", remote="node1"),
+            mock.call("/test/.ssh/authorized_keys", remote="node1")
+            ])
+        mock_info.assert_called_once_with("SSH key for test does not exist, 
hence generate it now")
+        mock_su.assert_called_once_with("ssh-keygen -q -f /test/.ssh/id_rsa -C 
'Cluster Internal on node1' -N ''", "test")
+        mock_run.assert_has_calls([
+            mock.call("cmd with su", remote="node1")
+            ])
+        mock_append.assert_called_once_with("/test/.ssh/id_rsa.pub", 
"/test/.ssh/authorized_keys", remote="node1")
 
     @mock.patch('crmsh.bootstrap.append_unique')
-    @mock.patch('builtins.open', create=True)
-    @mock.patch('crmsh.bootstrap.invoke')
-    @mock.patch('crmsh.utils.add_su')
-    @mock.patch('crmsh.utils.this_node')
-    @mock.patch('logging.Logger.info')
-    @mock.patch('os.path.exists')
+    @mock.patch('crmsh.utils.get_stdout_or_raise_error')
+    @mock.patch('crmsh.utils.detect_file')
     @mock.patch('crmsh.bootstrap.key_files')
     @mock.patch('crmsh.bootstrap.change_user_shell')
-    def test_configure_local_ssh_key(self, mock_change_shell, mock_key_files, 
mock_exists, mock_status, mock_this_node, mock_su, mock_invoke, mock_open_file, 
mock_append):
-        bootstrap._context = mock.Mock(yes_to_all=True)
+    def test_configure_ssh_key(self, mock_change_shell, mock_key_files, 
mock_detect, mock_run, mock_append_unique):
         mock_key_files.return_value = {"private": "/test/.ssh/id_rsa", 
"public": "/test/.ssh/id_rsa.pub", "authorized": "/test/.ssh/authorized_keys"}
-        mock_exists.side_effect = [False, False]
-        mock_this_node.return_value = "node1"
-        mock_invoke.return_value = (True, None, None)
-        mock_su.return_value = "cmd with su"
+        mock_detect.side_effect = [True, False]
 
-        bootstrap.configure_local_ssh_key("test")
+        bootstrap.configure_ssh_key("test")
 
         mock_change_shell.assert_called_once_with("test")
         mock_key_files.assert_called_once_with("test")
-        mock_exists.assert_has_calls([
-            mock.call("/test/.ssh/id_rsa"),
-            mock.call("/test/.ssh/authorized_keys")
+        mock_detect.assert_has_calls([
+            mock.call("/test/.ssh/id_rsa", remote=None),
+            mock.call("/test/.ssh/authorized_keys", remote=None)
             ])
-        mock_status.assert_called_once_with("Generating SSH key for test")
-        mock_invoke.assert_called_once_with("cmd with su")
-        mock_su.assert_called_once_with("ssh-keygen -q -f /test/.ssh/id_rsa -C 
'Cluster Internal on node1' -N ''", "test")
-        mock_this_node.assert_called_once_with()
-        mock_open_file.assert_called_once_with("/test/.ssh/authorized_keys", 
'w')
-        mock_append.assert_called_once_with("/test/.ssh/id_rsa.pub", 
"/test/.ssh/authorized_keys")
+        mock_append_unique.assert_called_once_with("/test/.ssh/id_rsa.pub", 
"/test/.ssh/authorized_keys", remote=None)
+        mock_run.assert_called_once_with('touch /test/.ssh/authorized_keys', 
remote=None)
+
+    @mock.patch('crmsh.bootstrap.append_to_remote_file')
+    @mock.patch('crmsh.utils.check_file_content_included')
+    def test_append_unique_remote(self, mock_check, mock_append):
+        mock_check.return_value = False
+        bootstrap.append_unique("fromfile", "tofile", remote="node1", 
from_local=True)
+        mock_check.assert_called_once_with("fromfile", "tofile", 
remote="node1", source_local=True)
+        mock_append.assert_called_once_with("fromfile", "node1", "tofile")
 
     @mock.patch('crmsh.bootstrap.append')
     @mock.patch('crmsh.utils.check_file_content_included')
     def test_append_unique(self, mock_check, mock_append):
         mock_check.return_value = False
         bootstrap.append_unique("fromfile", "tofile")
-        mock_check.assert_called_once_with("fromfile", "tofile")
-        mock_append.assert_called_once_with("fromfile", "tofile")
+        mock_check.assert_called_once_with("fromfile", "tofile", remote=None, 
source_local=False)
+        mock_append.assert_called_once_with("fromfile", "tofile", remote=None)
 
-    @mock.patch('crmsh.utils.fatal')
-    @mock.patch('crmsh.bootstrap.invoke')
-    def test_append_to_remote_file(self, mock_invoke, mock_error):
-        mock_invoke.return_value = (False, None, "error")
-        error_string = 'Failed to append contents of fromfile to 
node1:\n"error"\n\n    crmsh has no way to help you to setup up passwordless 
ssh among nodes at this time. \n    As the hint, likely, 
`PasswordAuthentication` is \'no\' in /etc/ssh/sshd_config. \n    Given in this 
case, users must setup passwordless ssh beforehand, or change it to \'yes\' and 
manage passwords properly\n    '
+    @mock.patch('crmsh.utils.get_stdout_or_raise_error')
+    def test_append_to_remote_file(self, mock_run):
         bootstrap.append_to_remote_file("fromfile", "node1", "tofile")
         cmd = "cat fromfile | ssh {} root@node1 'cat >> 
tofile'".format(constants.SSH_OPTION)
-        mock_invoke.assert_called_once_with(cmd)
-        mock_error.assert_called_once_with(error_string)
+        mock_run.assert_called_once_with(cmd)
 
     @mock.patch('crmsh.bootstrap.invokerc')
     def test_fetch_public_key_from_remote_node_exception(self, mock_invoke):
@@ -439,7 +437,7 @@
     @mock.patch('crmsh.utils.fatal')
     @mock.patch('crmsh.bootstrap.invoke')
     @mock.patch('crmsh.bootstrap.swap_public_ssh_key')
-    @mock.patch('crmsh.bootstrap.configure_local_ssh_key')
+    @mock.patch('crmsh.bootstrap.configure_ssh_key')
     @mock.patch('crmsh.utils.start_service')
     def test_join_ssh(self, mock_start_service, mock_config_ssh, mock_swap, 
mock_invoke, mock_error):
         bootstrap._context = mock.Mock(default_nic_list=["eth1"])
@@ -489,16 +487,37 @@
         mock_key_files.return_value = {"private": "/root/.ssh/id_rsa", 
"public": "/root/.ssh/id_rsa.pub", "authorized": "/root/.ssh/authorized_keys"}
         mock_check_passwd.return_value = True
         mock_fetch.return_value = "file1"
+        bootstrap._context = mock.Mock(with_other_user=True)
 
-        bootstrap.swap_public_ssh_key("node1")
+        bootstrap.swap_public_ssh_key("node1", user="other")
 
-        mock_key_files.assert_called_once_with("root")
-        mock_check_passwd.assert_called_once_with("node1", "root")
-        mock_status.assert_called_once_with("Configuring SSH passwordless with 
root@node1")
+        mock_key_files.assert_called_once_with("other")
+        mock_check_passwd.assert_called_once_with("node1", "other")
+        mock_status.assert_called_once_with("Configuring SSH passwordless with 
other@node1")
         mock_append_remote.assert_called_once_with("/root/.ssh/id_rsa.pub", 
"node1", "/root/.ssh/authorized_keys")
-        mock_fetch.assert_called_once_with("node1", "root")
+        mock_fetch.assert_called_once_with("node1", "other")
         mock_append_unique.assert_called_once_with("file1", 
"/root/.ssh/authorized_keys")
 
+    @mock.patch('crmsh.utils.this_node')
+    def test_bootstrap_add_return(self, mock_this_node):
+        ctx = mock.Mock(node_list=[])
+        bootstrap.bootstrap_add(ctx)
+        mock_this_node.assert_not_called()
+
+    @mock.patch('crmsh.utils.ext_cmd_nosudo')
+    @mock.patch('logging.Logger.info')
+    @mock.patch('crmsh.utils.this_node')
+    def test_bootstrap_add(self, mock_this_node, mock_info, mock_ext):
+        ctx = mock.Mock(node_list=["node2", "node3"], nic_list=["eth1"])
+        mock_this_node.return_value = "node1"
+        bootstrap.bootstrap_add(ctx)
+        mock_info.assert_has_calls([
+            mock.call("Adding node node2 to cluster"),
+            mock.call("Running command on node2: crm cluster join -y -c node1 
-i eth1"),
+            mock.call("Adding node node3 to cluster"),
+            mock.call("Running command on node3: crm cluster join -y -c node1 
-i eth1")
+            ])
+
     @mock.patch('crmsh.utils.fatal')
     @mock.patch('crmsh.utils.get_stdout_stderr')
     def test_setup_passwordless_with_other_nodes_failed_fetch_nodelist(self, 
mock_run, mock_error):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.0+20220711.573ebb98/test/unittests/test_utils.py 
new/crmsh-4.4.0+20220728.3f249756/test/unittests/test_utils.py
--- old/crmsh-4.4.0+20220711.573ebb98/test/unittests/test_utils.py      
2022-07-11 08:41:22.000000000 +0200
+++ new/crmsh-4.4.0+20220728.3f249756/test/unittests/test_utils.py      
2022-07-28 09:57:44.000000000 +0200
@@ -68,28 +68,34 @@
             "Check whether crmsh is installed on other_node")
 
 
-@mock.patch('os.path.exists')
-def test_check_file_content_included_target_not_exist(mock_exists):
-    mock_exists.side_effect = [True, False]
+@mock.patch('crmsh.utils.detect_file')
+def test_check_file_content_included_target_not_exist(mock_detect):
+    mock_detect.side_effect = [True, False]
     res = utils.check_file_content_included("file1", "file2")
     assert res is False
-    mock_exists.assert_has_calls([mock.call("file1"), mock.call("file2")])
+    mock_detect.assert_has_calls([
+        mock.call("file1", remote=None),
+        mock.call("file2", remote=None)
+        ])
 
 
-@mock.patch("builtins.open")
-@mock.patch('os.path.exists')
-def test_check_file_content_included(mock_exists, mock_open_file):
-    mock_exists.side_effect = [True, True]
-    mock_open_file.side_effect = [
-            mock.mock_open(read_data="data1").return_value,
-            mock.mock_open(read_data="data2").return_value
-        ]
+@mock.patch('crmsh.utils.get_stdout_or_raise_error')
+@mock.patch('crmsh.utils.detect_file')
+def test_check_file_content_included(mock_detect, mock_run):
+    mock_detect.side_effect = [True, True]
+    mock_run.side_effect = ["data data", "data"]
 
     res = utils.check_file_content_included("file1", "file2")
-    assert res is False
+    assert res is True
 
-    mock_exists.assert_has_calls([mock.call("file1"), mock.call("file2")])
-    mock_open_file.assert_has_calls([mock.call("file2", 'r'), 
mock.call("file1", 'r')])
+    mock_detect.assert_has_calls([
+        mock.call("file1", remote=None),
+        mock.call("file2", remote=None)
+        ])
+    mock_run.assert_has_calls([
+        mock.call("cat file2", remote=None),
+        mock.call("cat file1", remote=None)
+        ])
 
 
 @mock.patch("crmsh.utils.get_stdout_stderr")

Reply via email to