Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package crmsh for openSUSE:Factory checked in at 2025-10-10 17:11:31 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/crmsh (Old) and /work/SRC/openSUSE:Factory/.crmsh.new.5300 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "crmsh" Fri Oct 10 17:11:31 2025 rev:386 rq:1310518 version:5.0.0+20250925.91a04ca5 Changes: -------- --- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes 2025-09-16 18:19:25.121672325 +0200 +++ /work/SRC/openSUSE:Factory/.crmsh.new.5300/crmsh.changes 2025-10-10 17:13:12.590991003 +0200 @@ -1,0 +2,16 @@ +Thu Sep 25 02:29:49 UTC 2025 - [email protected] + +- Update to version 5.0.0+20250925.91a04ca5: + * Fix: bootstrap: public keys from ssh-agent are not added to local authorized_keys in `crm cluster join` (#1916) + * Fix: log: divided-by-zero when terminal width is 0 in progress bar + * Dev: behave: add functional test cases for bootstrapping with ssh password + * Dev: behave: allow to use a pypi mirror with a environment variable + * Dev: test_container: add expect and remove automake + +------------------------------------------------------------------- +Fri Sep 19 03:24:31 UTC 2025 - [email protected] + +- Update to version 5.0.0+20250919.fb15985a: + * Fix: cibconfig: Add utils.auto_convert_role=True flag in method CibObjectSetCli.save + +------------------------------------------------------------------- Old: ---- crmsh-5.0.0+20250916.c8f0c88a.tar.bz2 New: ---- crmsh-5.0.0+20250925.91a04ca5.tar.bz2 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ crmsh.spec ++++++ --- /var/tmp/diff_new_pack.TFWT29/_old 2025-10-10 17:13:13.319021631 +0200 +++ /var/tmp/diff_new_pack.TFWT29/_new 2025-10-10 17:13:13.323021800 +0200 @@ -41,7 +41,7 @@ Summary: High Availability cluster command-line interface License: GPL-2.0-or-later Group: %{pkg_group} -Version: 5.0.0+20250916.c8f0c88a +Version: 5.0.0+20250925.91a04ca5 Release: 0 URL: http://crmsh.github.io Source0: %{name}-%{version}.tar.bz2 ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.TFWT29/_old 2025-10-10 17:13:13.371023819 +0200 +++ /var/tmp/diff_new_pack.TFWT29/_new 2025-10-10 17:13:13.375023987 +0200 @@ -9,7 +9,7 @@ </service> <service name="tar_scm"> <param name="url">https://github.com/ClusterLabs/crmsh.git</param> - <param name="changesrevision">ff10557ca93cfc30cb7d5f57bd755e3b0ffc288f</param> + <param name="changesrevision">91a04ca58ff3a8256f598aaceaf31a426e8e0bbd</param> </service> </servicedata> (No newline at EOF) ++++++ crmsh-5.0.0+20250916.c8f0c88a.tar.bz2 -> crmsh-5.0.0+20250925.91a04ca5.tar.bz2 ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/.github/workflows/crmsh-ci.yml new/crmsh-5.0.0+20250925.91a04ca5/.github/workflows/crmsh-ci.yml --- old/crmsh-5.0.0+20250916.c8f0c88a/.github/workflows/crmsh-ci.yml 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/.github/workflows/crmsh-ci.yml 2025-09-25 04:02:32.000000000 +0200 @@ -168,6 +168,21 @@ token: ${{ secrets.CODECOV_TOKEN }} flags: integration + functional_test_bootstrap_password: + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - name: functional test for bootstrap using password + run: | + index=`$GET_INDEX_OF bootstrap_password` + $CONTAINER_SCRIPT $index + - uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: integration + + functional_test_corosync_ui: runs-on: ubuntu-24.04 timeout-minutes: 40 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/codecov.yml new/crmsh-5.0.0+20250925.91a04ca5/codecov.yml --- old/crmsh-5.0.0+20250916.c8f0c88a/codecov.yml 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/codecov.yml 2025-09-25 04:02:32.000000000 +0200 @@ -8,7 +8,7 @@ threshold: 0.35% codecov: notify: - after_n_builds: 32 + after_n_builds: 33 comment: - after_n_builds: 32 + after_n_builds: 33 layout: "condensed_header, flags, files, condensed_footer" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/bootstrap.py new/crmsh-5.0.0+20250925.91a04ca5/crmsh/bootstrap.py --- old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/bootstrap.py 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/crmsh/bootstrap.py 2025-09-25 04:02:32.000000000 +0200 @@ -1648,36 +1648,50 @@ ServiceManager(sh.ClusterShellAdaptorForLocalShell(sh.LocalShell())).start_service("sshd.service", enable=True) if ssh_public_keys: local_shell = sh.LocalShell(additional_environ={'SSH_AUTH_SOCK': os.environ.get('SSH_AUTH_SOCK')}) + ssh_shell = sh.SSHShell(local_shell, local_user) + authorized_key_manager = ssh_key.AuthorizedKeyManager(ssh_shell) + authorized_key_manager.add(seed_host, seed_user, ssh_public_keys[0]) + logger.info( + 'A public key is added to authorized_keys for user %s@%s: %s', + local_user, seed_host, ssh_public_keys[0].fingerprint(), + ) + authorized_key_manager.add(None, seed_user, ssh_public_keys[0]) + logger.info( + 'A public key is added to authorized_keys for user %s: %s', + local_user, ssh_public_keys[0].fingerprint(), + ) + # From here, login to remote_node is passwordless + ssh_shell = sh.SSHShell(local_shell, local_user) else: local_shell = sh.LocalShell(additional_environ={'SSH_AUTH_SOCK': ''}) - result = ssh_copy_id_no_raise(local_user, seed_user, seed_host, local_shell) - if 0 != result.returncode: - msg = f"Failed to login to {seed_user}@{seed_host}. Please check the credentials." - sudoer = userdir.get_sudoer() - if sudoer and seed_user != sudoer: - args = ['sudo crm'] - args += [x for x in sys.argv[1:]] - for i, arg in enumerate(args): - if arg == '-c' or arg == '--cluster-node' and i + 1 < len(args): - if '@' not in args[i+1]: - args[i + 1] = f'{sudoer}@{seed_host}' - msg += '\nOr, run "{}".'.format(' '.join(args)) - raise ValueError(msg) - # From here, login to remote_node is passwordless - ssh_shell = sh.SSHShell(local_shell, local_user) - authorized_key_manager = ssh_key.AuthorizedKeyManager(ssh_shell) - if not result.public_keys: - pass - elif isinstance(result.public_keys[0], ssh_key.KeyFile): - public_key = ssh_key.InMemoryPublicKey( - generate_ssh_key_pair_on_remote(local_shell, local_user, seed_host, seed_user, seed_user), - ) - authorized_key_manager.add( None, local_user, public_key) - logger.info('A public key is added to authorized_keys for user %s: %s', local_user, public_key.fingerprint()) - elif isinstance(result.public_keys[0], ssh_key.InMemoryPublicKey): - authorized_key_manager.add(None, local_user, result.public_keys[0]) - logger.info('A public key is added to authorized_keys for user %s: %s', local_user, result.public_keys[0].fingerprint()) - # else is not None do nothing + result = ssh_copy_id_no_raise(local_user, seed_user, seed_host, local_shell) + if 0 != result.returncode: + msg = f"Failed to login to {seed_user}@{seed_host}. Please check the credentials." + sudoer = userdir.get_sudoer() + if sudoer and seed_user != sudoer: + args = ['sudo crm'] + args += [x for x in sys.argv[1:]] + for i, arg in enumerate(args): + if arg == '-c' or arg == '--cluster-node' and i + 1 < len(args): + if '@' not in args[i+1]: + args[i + 1] = f'{sudoer}@{seed_host}' + msg += '\nOr, run "{}".'.format(' '.join(args)) + raise ValueError(msg) + # From here, login to remote_node is passwordless + ssh_shell = sh.SSHShell(local_shell, local_user) + authorized_key_manager = ssh_key.AuthorizedKeyManager(ssh_shell) + if not result.public_keys: + pass + elif isinstance(result.public_keys[0], ssh_key.KeyFile): + public_key = ssh_key.InMemoryPublicKey( + generate_ssh_key_pair_on_remote(local_shell, local_user, seed_host, seed_user, seed_user), + ) + authorized_key_manager.add( None, local_user, public_key) + logger.info('A public key is added to authorized_keys for user %s: %s', local_user, public_key.fingerprint()) + elif isinstance(result.public_keys[0], ssh_key.InMemoryPublicKey): + authorized_key_manager.add(None, local_user, result.public_keys[0]) + logger.info('A public key is added to authorized_keys for user %s: %s', local_user, result.public_keys[0].fingerprint()) + # else is not None do nothing if seed_user != 'root' and 0 != ssh_shell.subprocess_run_without_input( seed_host, seed_user, 'sudo true', stdout=subprocess.DEVNULL, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/cibconfig.py new/crmsh-5.0.0+20250925.91a04ca5/crmsh/cibconfig.py --- old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/cibconfig.py 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/crmsh/cibconfig.py 2025-09-25 04:02:32.000000000 +0200 @@ -575,6 +575,7 @@ diff = CibDiff(self) rc = True comments = [] + utils.auto_convert_role = True with logger_utils.line_number(): for cli_text in lines2cli(s): logger_utils.incr_lineno() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/log.py new/crmsh-5.0.0+20250925.91a04ca5/crmsh/log.py --- old/crmsh-5.0.0+20250916.c8f0c88a/crmsh/log.py 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/crmsh/log.py 2025-09-25 04:02:32.000000000 +0200 @@ -512,6 +512,8 @@ except OSError: # not a terminal return + if width == 0: + return self._i = (self._i + 1) % width line = '\r{}{}'.format('.' * self._i, ' ' * (width - self._i)) sys.stdout.write(line) @@ -523,6 +525,8 @@ except OSError: # not a terminal return + if width == 0: + return if self._i == 0: pass elif self._i < width: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/data-manifest new/crmsh-5.0.0+20250925.91a04ca5/data-manifest --- old/crmsh-5.0.0+20250916.c8f0c88a/data-manifest 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/data-manifest 2025-09-25 04:02:32.000000000 +0200 @@ -64,6 +64,7 @@ test/features/bootstrap_firewalld.feature test/features/bootstrap_init_join_remove.feature test/features/bootstrap_options.feature +test/features/bootstrap_password.feature test/features/bootstrap_sbd_delay.feature test/features/bootstrap_sbd_normal.feature test/features/cluster_api.feature @@ -91,6 +92,7 @@ test/features/sbd_ui.feature test/features/ssh_agent.feature test/features/steps/behave_agent.py +test/features/steps/bootstrap_steps.py test/features/steps/const.py test/features/steps/__init__.py test/features/steps/step_implementation.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/test/features/bootstrap_password.feature new/crmsh-5.0.0+20250925.91a04ca5/test/features/bootstrap_password.feature --- old/crmsh-5.0.0+20250916.c8f0c88a/test/features/bootstrap_password.feature 1970-01-01 01:00:00.000000000 +0100 +++ new/crmsh-5.0.0+20250925.91a04ca5/test/features/bootstrap_password.feature 2025-09-25 04:02:32.000000000 +0200 @@ -0,0 +1,381 @@ +# vim: sw=2 sts=2 +Feature: crmsh bootstrap process - with password authentication + + Need nodes: hanode1 hanode2 hanode3 qnetd-node + + Scenario: Disable key-based authentication + Given Permit root ssh login with password on "hanode1" + Given Permit root ssh login with password on "hanode2" + Given Permit root ssh login with password on "hanode3" + Given Permit root ssh login with password on "qnetd-node" + Given The password of user "root" set to "root123" on "hanode1" + Given The password of user "root" set to "root123" on "hanode2" + Given The password of user "root" set to "root123" on "hanode3" + Given The password of user "root" set to "root123" on "qnetd-node" + Given The password of user "alice" set to "alice123" on "hanode1" + Given The password of user "alice" set to "alice123" on "hanode2" + Given The password of user "alice" set to "alice123" on "hanode3" + Given The password of user "alice" set to "alice123" on "qnetd-node" + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "hanode3" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "hanode3" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "hanode3" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + + Scenario: Init cluster service on node "hanode1", and join on node "hanode2" + When Run "crm cluster init -y" on "hanode1" + Then Cluster service is "started" on "hanode1" + Then This expect program exits with 0 on "root"@"hanode2" + """ + set timeout 120 + spawn crm cluster join -c hanode1 -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode2" + Then Online nodes are "hanode1 hanode2" + Then two_node in corosync.conf is "1" + Then Cluster is using "knet" transport mode + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 1"" OK on "hanode1" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 1"" OK on "hanode2" + + Scenario: Join on a 3rd node "hanode3" + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 120 + spawn crm cluster join -c hanode1 -y + for {set i 0} {$i < 2} {incr i} { + expect "Password: " { + send "root123\n" + } + } + expect eof + """ + Then Cluster service is "started" on "hanode3" + Then Online nodes are "hanode1 hanode2 hanode3" + Then two_node in corosync.conf is "0" + Then Cluster is using "knet" transport mode + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode1" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode2" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode3" + + @clean + Scenario: Bootstrap using `init -N` + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "hanode3" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "hanode3" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "hanode3" + Then This expect program exits with 0 on "root"@"hanode1" + """ + set timeout 120 + spawn crm cluster init -N hanode2 -N hanode3 -y + for {set i 0} {$i < 2} {incr i} { + expect "Password: " { + send "root123\n" + } + } + expect eof + """ + Then Online nodes are "hanode1 hanode2 hanode3" + Then two_node in corosync.conf is "0" + Then Cluster is using "knet" transport mode + + @clean + Scenario: Init cluster service on node "hanode1", and join on node "hanode2" (non-root) + When Run "crm cluster init -y" on "hanode1" + Then Cluster service is "started" on "hanode1" + Then This expect program exits with 0 on "alice"@"hanode2" + """ + set timeout 120 + spawn sudo crm cluster join -c alice@hanode1 -y + expect "Password: " { + send "alice123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode2" + Then Online nodes are "hanode1 hanode2" + Then two_node in corosync.conf is "1" + Then Cluster is using "knet" transport mode + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 1"" OK on "hanode1" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 1"" OK on "hanode2" + + Scenario: Join on a 3rd node "hanode3" (non-root) + Then This expect program exits with 0 on "alice"@"hanode3" + """ + set timeout 120 + spawn sudo crm cluster join -c alice@hanode1 -y + for {set i 0} {$i < 2} {incr i} { + expect "Password: " { + send "alice123\n" + } + } + expect eof + """ + Then Cluster service is "started" on "hanode3" + Then Online nodes are "hanode1 hanode2 hanode3" + Then two_node in corosync.conf is "0" + Then Cluster is using "knet" transport mode + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode1" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode2" + Then Run "corosync-cmapctl|grep "votequorum.two_node .* = 0"" OK on "hanode3" + + @clean + Scenario: Bootstrap using `init -N` (non-root) + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "hanode3" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "hanode3" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "hanode3" + Then This expect program exits with 0 on "alice"@"hanode1" + """ + set timeout 120 + spawn sudo crm cluster init -N alice@hanode2 -N alice@hanode3 -y + for {set i 0} {$i < 2} {incr i} { + expect "Password: " { + send "alice123\n" + } + } + expect eof + """ + Then Online nodes are "hanode1 hanode2 hanode3" + Then two_node in corosync.conf is "0" + Then Cluster is using "knet" transport mode + + @clean + Scenario: Setup qdevice/qnetd during init/join process + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + Then This expect program exits with 0 on "root"@"hanode1" + """ + set timeout 120 + spawn crm cluster init --qnetd-hostname=qnetd-node -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode1" + Then Service "corosync-qdevice" is "started" on "hanode1" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + Then This expect program exits with 0 on "root"@"hanode2" + """ + set timeout 120 + spawn crm cluster join -c hanode1 -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode2" + Then Online nodes are "hanode1 hanode2" + Then Service "corosync-qdevice" is "started" on "hanode2" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + + @clean + Scenario: Setup qdevice/qnetd on running cluster + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + Then This expect program exits with 0 on "root"@"hanode1" + """ + set timeout 120 + spawn crm cluster init -N hanode1 -N hanode2 -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then Online nodes are "hanode1 hanode2" + Then This expect program exits with 0 on "root"@"hanode1" + """ + set timeout 120 + spawn crm cluster init qdevice --qnetd-hostname=qnetd-node -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then Service "corosync-qdevice" is "started" on "hanode1" + Then Service "corosync-qdevice" is "started" on "hanode2" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + + @clean + Scenario: Setup qdevice/qnetd during init/join process (non-root) + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + Then This expect program exits with 0 on "alice"@"hanode1" + """ + set timeout 120 + spawn sudo crm cluster init --qnetd-hostname=alice@qnetd-node -y + expect "Password: " { + send "alice123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode1" + Then Service "corosync-qdevice" is "started" on "hanode1" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + Then This expect program exits with 0 on "alice"@"hanode2" + """ + set timeout 120 + spawn sudo crm cluster join -c alice@hanode1 -y + expect "Password: " { + send "alice123\n" + } + expect eof + """ + Then Cluster service is "started" on "hanode2" + Then Online nodes are "hanode1 hanode2" + Then Service "corosync-qdevice" is "started" on "hanode2" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + + @clean + Scenario: Setup qdevice/qnetd on running cluster + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + Then This expect program exits with 0 on "alice"@"hanode1" + """ + set timeout 120 + spawn sudo crm cluster init -N alice@hanode2 -y + expect "Password: " { + send "alice123\n" + } + expect eof + """ + Then Online nodes are "hanode1 hanode2" + Then This expect program exits with 0 on "alice"@"hanode1" + """ + set timeout 120 + spawn sudo crm cluster init qdevice --qnetd-hostname=alice@qnetd-node -y + expect "Password: " { + send "alice123\n" + } + expect eof + """ + Then Service "corosync-qdevice" is "started" on "hanode1" + Then Service "corosync-qdevice" is "started" on "hanode2" + Then Service "corosync-qnetd" is "started" on "qnetd-node" + + @clean + Scenario: Skip creating ssh key pairs when keys are available from ssh-agent fowarding + Given Directory ~root/.ssh is empty on "hanode1" + Given Directory ~root/.ssh is empty on "hanode2" + Given Directory ~root/.ssh is empty on "hanode3" + Given Directory ~root/.ssh is empty on "qnetd-node" + Given Directory ~alice/.ssh is empty on "hanode1" + Given Directory ~alice/.ssh is empty on "hanode2" + Given Directory ~alice/.ssh is empty on "hanode3" + Given Directory ~alice/.ssh is empty on "qnetd-node" + Given Directory ~hacluster/.ssh is empty on "hanode1" + Given Directory ~hacluster/.ssh is empty on "hanode2" + Given Directory ~hacluster/.ssh is empty on "hanode3" + Given Directory ~hacluster/.ssh is empty on "qnetd-node" + Given crm.conf poisoned on nodes ["hanode1", "hanode2", "hanode3"] + Given ssh-agent is started at "/tmp/ssh-auth-sock" on nodes ["hanode3"] + Given Run "ssh-keygen -q -N '' -t ed25519 -f /root/.ssh/id_ed25519" OK on "hanode3" + Given Run "SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh-add ~/.ssh/id_ed25519" OK on "hanode3" + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 120 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -At root@hanode1 crm cluster init -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 15 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -A root@hanode1 echo 'NO-NEED-FOR-PASSWORD' + expect "NO-NEED-FOR-PASSWORD" + expect eof + """ + Then Cluster service is "started" on "hanode1" + Then Run "test x1 == x$(awk 'END {print NR}' ~/.ssh/authorized_keys)" OK + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 120 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -At root@hanode2 crm cluster join -c hanode1 -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 15 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -A root@hanode2 echo 'NO-NEED-FOR-PASSWORD' + expect "NO-NEED-FOR-PASSWORD" + expect eof + """ + Then Online nodes are "hanode1 hanode2" + Then Run "test x1 == x$(awk 'END {print NR}' ~/.ssh/authorized_keys)" OK + Then Run "test x2 == x$(sudo awk 'END {print NR}' ~hacluster/.ssh/authorized_keys)" OK + Then Run "grep -E 'hosts = (root|alice)@hanode1' /root/.config/crm/crm.conf" OK on "hanode1,hanode2" + Then Run "SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -A root@hanode1 crm report /tmp/report1" OK on "hanode3" + Then Directory "hanode2" in "/tmp/report1.tar.bz2" + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 120 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -At root@hanode1 crm cluster init qdevice --qnetd-hostname qnetd-node -y + expect "Password: " { + send "root123\n" + } + expect eof + """ + Then This expect program exits with 0 on "root"@"hanode3" + """ + set timeout 15 + spawn env SSH_AUTH_SOCK=/tmp/ssh-auth-sock ssh -A root@qnetd-node echo 'NO-NEED-FOR-PASSWORD' + expect "NO-NEED-FOR-PASSWORD" + expect eof + """ + Then Service "corosync-qdevice" is "started" on "hanode1" + Then Service "corosync-qdevice" is "started" on "hanode2" + Then Service "corosync-qnetd" is "started" on "qnetd-node" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/test/features/steps/bootstrap_steps.py new/crmsh-5.0.0+20250925.91a04ca5/test/features/steps/bootstrap_steps.py --- old/crmsh-5.0.0+20250916.c8f0c88a/test/features/steps/bootstrap_steps.py 1970-01-01 01:00:00.000000000 +0100 +++ new/crmsh-5.0.0+20250925.91a04ca5/test/features/steps/bootstrap_steps.py 2025-09-25 04:02:32.000000000 +0200 @@ -0,0 +1,68 @@ +import random +from behave import given, when, then + +import behave_agent + + +_rng = random.SystemRandom() + + +def _gen_random_string(): + return 'random-' + _rng.randbytes(8).hex() + + +@given('Permit root ssh login with password on "{node}"') +def step_impl(context, node): + eof = _gen_random_string() + script = '''echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/permit-root-login.conf +systemctl restart sshd.service +''' + rc, stdout, stderr = behave_agent.call(node, 1122, script, user='root') + if 0 == rc: + return + else: + print(stderr.decode('utf-8', errors='backslashreplace')) + assert 0 == rc + + +@given('The password of user "{user}" set to "{password}" on "{node}"') +def step_impl(context, user, password, node): + eof = _gen_random_string() + script = f'''chpasswd << '{eof}' +{user}:{password} +{eof} +''' + rc, stdout, stderr = behave_agent.call(node, 1122, script, user='root') + if 0 == rc: + return + else: + print(stderr.decode('utf-8', errors='backslashreplace')) + assert 0 == rc + + + +@given('Directory ~{user}/.ssh is empty on "{node}"') +def step_impl(context, user, node): + rc, stdout, stderr = behave_agent.call(node, 1122, f'rm -rf ~{user}/.ssh', user='root') + if 0 == rc: + return + else: + print(stderr.decode('utf-8', errors='backslashreplace')) + assert 0 == rc + + +@then('This expect program exits with 0 on "{user}"@"{node}"') +def step_impl(context, user, node): + eof = _gen_random_string() + script = f'''expect <(cat << '{eof}' +{context.text} +{eof} +) +''' + rc, stdout, stderr = behave_agent.call(node, 1122, script, user) + if 0 == rc: + print(stdout.decode('utf-8', errors='backslashreplace')) + return + else: + print(stderr.decode('utf-8', errors='backslashreplace')) + assert 0 == rc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/test/run-functional-tests new/crmsh-5.0.0+20250925.91a04ca5/test/run-functional-tests --- old/crmsh-5.0.0+20250916.c8f0c88a/test/run-functional-tests 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/test/run-functional-tests 2025-09-25 04:02:32.000000000 +0200 @@ -1,5 +1,6 @@ #!/bin/bash CONTAINER_IMAGE=${CONTAINER_IMAGE:-"docker.io/nyang23/haleap:master"} +PYPI_MIRROR=${PYPI_MIRROR:-""} PROJECT_PATH=$(dirname $(dirname `realpath $0`)) PROJECT_INSIDE="/opt/crmsh" COROSYNC_CONF="/etc/corosync/corosync.conf" @@ -207,6 +208,7 @@ if [[ "$node_name" != "qnetd-node" && ! "$node_name" =~ ^pcmk-remote-node[0-9]$ ]];then podman cp $PROJECT_PATH $node_name:/opt/crmsh info "Building crmsh on \"$node_name\"..." + [ -n "${PYPI_MIRROR}" ] && podman_exec $node_name "pip config set global.index-url ${PYPI_MIRROR}" podman_exec $node_name "$make_cmd" || \ fatal "Building failed on $node_name!" podman_exec $node_name "chown hacluster:haclient -R /var/log/crmsh" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/test/unittests/test_bootstrap.py new/crmsh-5.0.0+20250925.91a04ca5/test/unittests/test_bootstrap.py --- old/crmsh-5.0.0+20250916.c8f0c88a/test/unittests/test_bootstrap.py 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/test/unittests/test_bootstrap.py 2025-09-25 04:02:32.000000000 +0200 @@ -683,21 +683,25 @@ mock_swap_public_ssh_key_for_secondary_user, mock_setup_passwordless_with_other_nodes, ): + bootstrap._context = mock.Mock(stage=None) ssh_key = mock.Mock(crmsh.ssh_key.InMemoryPublicKey) ssh_key.fingerprint.return_value = 'foo' mock_environ.get.return_value = '/nonexist' - mock_ssh_copy_id_no_raise.return_value = crmsh.bootstrap.SshCopyIdResult( - 0, [ssh_key], - ) mock_ssh_shell.return_value.subprocess_run_without_input.return_value = mock.Mock(returncode=0) mock_get_node_canonical_hostname.return_value = 'host1' crmsh.bootstrap.join_ssh_impl('alice', 'node1', 'bob', [ssh_key]) mock_environ.get.assert_called_with('SSH_AUTH_SOCK') mock_local_shell.assert_called_with(additional_environ={'SSH_AUTH_SOCK': '/nonexist'}) - mock_ssh_copy_id_no_raise.assert_called_once_with('alice', 'bob', 'node1', mock_local_shell.return_value) - mock_ssh_shell.assert_called_once_with(mock_local_shell.return_value, 'alice') + mock_ssh_copy_id_no_raise.assert_not_called() + mock_ssh_shell.assert_has_calls([ + mock.call(mock_local_shell.return_value, 'alice'), + mock.call(mock_local_shell.return_value, 'alice'), + ]) mock_authorized_key_manager.assert_called_once_with(mock_ssh_shell.return_value) - mock_authorized_key_manager.return_value.add.assert_called_once_with(None, 'alice', ssh_key) + mock_authorized_key_manager.return_value.add.assert_has_calls([ + mock.call('node1', 'bob', ssh_key), + mock.call(None, 'bob', ssh_key), + ]) mock_ssh_shell.return_value.subprocess_run_without_input.assert_called_once_with( 'node1', 'bob', 'sudo true', stdout=subprocess.DEVNULL, @@ -710,7 +714,27 @@ mock_cluster_shell_fn.return_value, 'node1', 'hacluster', ) + @mock.patch('crmsh.bootstrap.ssh_copy_id_no_raise') + @mock.patch('crmsh.sh.LocalShell') + @mock.patch('crmsh.service_manager.ServiceManager') + @mock.patch('crmsh.userdir.get_sudoer', return_value=None) + def test_join_ssh_impl_no_key_fails( + self, + mock_get_sudoer, + mock_service_manager, + mock_local_shell, + mock_ssh_copy_id_no_raise, + ): + bootstrap._context = mock.Mock(stage=None) + mock_ssh_copy_id_no_raise.return_value = mock.Mock(returncode=1) + + with self.assertRaisesRegex(ValueError, "Failed to login to bob@node1"): + crmsh.bootstrap.join_ssh_impl('alice', 'node1', 'bob', []) + mock_local_shell.assert_called_with(additional_environ={'SSH_AUTH_SOCK': ''}) + mock_ssh_copy_id_no_raise.assert_called_once_with('alice', 'bob', 'node1', mock_local_shell.return_value) + + @mock.patch('crmsh.bootstrap.generate_ssh_key_pair_on_remote') @mock.patch('crmsh.bootstrap.setup_passwordless_with_other_nodes') @mock.patch('crmsh.bootstrap.swap_public_ssh_key_for_secondary_user') @mock.patch('crmsh.sh.cluster_shell') @@ -723,12 +747,10 @@ @mock.patch('crmsh.sh.SSHShell') @mock.patch('crmsh.bootstrap.ssh_copy_id_no_raise') @mock.patch('crmsh.sh.LocalShell') - @mock.patch('os.environ') @mock.patch('crmsh.service_manager.ServiceManager') - def test_join_ssh_bad_credential( + def test_join_ssh_impl_no_key( self, mock_service_manager, - mock_environ, mock_local_shell, mock_ssh_copy_id_no_raise, mock_ssh_shell, @@ -741,25 +763,17 @@ mock_cluster_shell_fn, mock_swap_public_ssh_key_for_secondary_user, mock_setup_passwordless_with_other_nodes, + mock_generate_ssh_key_pair_on_remote, ): - ssh_key = mock.Mock(crmsh.ssh_key.InMemoryPublicKey) - ssh_key.fingerprint.return_value = 'foo' - mock_environ.get.side_effect = ['/nonexist', 'alice'] - mock_ssh_copy_id_no_raise.return_value = crmsh.bootstrap.SshCopyIdResult( - 255, list(), - ) - with self.assertRaises(ValueError): - crmsh.bootstrap.join_ssh_impl('alice', 'node1', 'bob', [ssh_key]) - mock_environ.get.assert_called_with('SUDO_USER') - mock_local_shell.assert_called_with(additional_environ={'SSH_AUTH_SOCK': '/nonexist'}) - mock_ssh_copy_id_no_raise.assert_called_once_with('alice', 'bob', 'node1', mock_local_shell.return_value) - mock_ssh_shell.assert_not_called() - mock_authorized_key_manager.assert_not_called() - mock_host_user_config.return_value.add.assert_not_called() - mock_configure_ssh_key.assert_not_called() - mock_change_user_shell.assert_not_called() - mock_swap_public_ssh_key_for_secondary_user.assert_not_called() - + bootstrap._context = mock.Mock(stage=None) + in_memory_key = mock.Mock(spec=crmsh.ssh_key.InMemoryPublicKey) + mock_ssh_copy_id_no_raise.return_value = mock.Mock(returncode=0, public_keys=[in_memory_key]) + mock_ssh_shell.return_value.subprocess_run_without_input.return_value = mock.Mock(returncode=0) + mock_get_node_canonical_hostname.return_value = 'host1' + crmsh.bootstrap.join_ssh_impl('alice', 'node1', 'bob', []) + mock_ssh_copy_id_no_raise.assert_called_once() + mock_generate_ssh_key_pair_on_remote.assert_not_called() + mock_authorized_key_manager.return_value.add.assert_called_once_with(None, 'alice', in_memory_key) @mock.patch('crmsh.ssh_key.AuthorizedKeyManager.add') @mock.patch('crmsh.ssh_key.KeyFile.public_key') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/crmsh-5.0.0+20250916.c8f0c88a/test_container/Dockerfile new/crmsh-5.0.0+20250925.91a04ca5/test_container/Dockerfile --- old/crmsh-5.0.0+20250916.c8f0c88a/test_container/Dockerfile 2025-09-16 05:04:23.000000000 +0200 +++ new/crmsh-5.0.0+20250925.91a04ca5/test_container/Dockerfile 2025-09-25 04:02:32.000000000 +0200 @@ -3,9 +3,9 @@ CMD ["/usr/lib/systemd/systemd", "--system"] -RUN zypper -n install systemd openssh \ +RUN zypper -n dup && zypper -n install systemd openssh \ firewalld iptables iptables-backend-nft \ - make autoconf automake vim which libxslt-tools asciidoc mailx iproute2 iputils bzip2 tar file glibc-locale-base dos2unix cpio gawk sudo \ + make autoconf vim which libxslt-tools asciidoc mailx iproute2 iputils bzip2 tar file glibc-locale-base dos2unix cpio gawk sudo expect\ python313 python313-pip python313-lxml python313-python-dateutil python313-build python313-PyYAML python313-curses python313-behave python313-coverage python313-packaging \ csync2 corosync corosync-qdevice pacemaker pacemaker-remote booth corosync-qnetd
