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