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
 

Reply via email to