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-11-23 09:48:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/crmsh (Old)
 and      /work/SRC/openSUSE:Factory/.crmsh.new.1597 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "crmsh"

Wed Nov 23 09:48:02 2022 rev:267 rq:1037344 version:4.4.1+20221122.102a8e11

Changes:
--------
--- /work/SRC/openSUSE:Factory/crmsh/crmsh.changes      2022-11-16 
15:43:54.979958006 +0100
+++ /work/SRC/openSUSE:Factory/.crmsh.new.1597/crmsh.changes    2022-11-23 
09:48:27.999154228 +0100
@@ -1,0 +2,21 @@
+Tue Nov 22 14:36:03 UTC 2022 - xli...@suse.com
+
+- Update to version 4.4.1+20221122.102a8e11:
+  * Dev: workflows: add behave test `healthcheck`
+  * Dev: behave: add functional test for previous changes
+  * Dev: upgradeutil: change the format of seq from int to major.minor
+  * Dev: unittest: move tests to test_healthcheck
+  * Dev: bootstrap: fix passwordless ssh authentication for hacluster 
automatically when a new node is joining the cluster (bsc#1201785)
+  * Dev: refactor: extract healthcheck module from upgradeutil
+  * Fix: testcases: fix shadow cib tests for previous changes.
+  * Fix: testcases: add no_reg option for utils.list_cluster_nodes
+  * Dev: unittest: add new tests for upgradeutil
+  * Dev: upgradeutil: automated init ssh passwordless auth for hacluster after 
upgrading (bsc#1201785)
+
+-------------------------------------------------------------------
+Tue Nov 22 10:19:09 UTC 2022 - nicholas.y...@suse.com
+
+- Update to version 4.4.1+20221122.20aa6e8e:
+  * Dev: workflows: update actions version
+
+-------------------------------------------------------------------

Old:
----
  crmsh-4.4.1+20221116.4faefec3.tar.bz2

New:
----
  crmsh-4.4.1+20221122.102a8e11.tar.bz2

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

Other differences:
------------------
++++++ crmsh.spec ++++++
--- /var/tmp/diff_new_pack.BKQzyS/_old  2022-11-23 09:48:28.779158298 +0100
+++ /var/tmp/diff_new_pack.BKQzyS/_new  2022-11-23 09:48:28.783158319 +0100
@@ -36,7 +36,7 @@
 Summary:        High Availability cluster command-line interface
 License:        GPL-2.0-or-later
 Group:          %{pkg_group}
-Version:        4.4.1+20221116.4faefec3
+Version:        4.4.1+20221122.102a8e11
 Release:        0
 URL:            http://crmsh.github.io
 Source0:        %{name}-%{version}.tar.bz2

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.BKQzyS/_old  2022-11-23 09:48:28.847158653 +0100
+++ /var/tmp/diff_new_pack.BKQzyS/_new  2022-11-23 09:48:28.851158674 +0100
@@ -9,7 +9,7 @@
 </service>
 <service name="tar_scm">
   <param name="url">https://github.com/ClusterLabs/crmsh.git</param>
-  <param 
name="changesrevision">4faefec3264baba80b0f2bd59aa718280062f7c2</param>
+  <param 
name="changesrevision">5baaacb0a20a8ed89adaa403a50dacde998688f6</param>
 </service>
 </servicedata>
 (No newline at EOF)

++++++ crmsh-4.4.1+20221116.4faefec3.tar.bz2 -> 
crmsh-4.4.1+20221122.102a8e11.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/.github/workflows/crmsh-ci.yml 
new/crmsh-4.4.1+20221122.102a8e11/.github/workflows/crmsh-ci.yml
--- old/crmsh-4.4.1+20221116.4faefec3/.github/workflows/crmsh-ci.yml    
2022-11-16 04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/.github/workflows/crmsh-ci.yml    
2022-11-22 15:17:41.000000000 +0100
@@ -18,9 +18,9 @@
 
 jobs:
   general_check:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: check data-manifest
       run: |
         ./update-data-manifest.sh
@@ -33,16 +33,16 @@
         }
 
   unit_test:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     strategy:
       matrix:
         python-version: ['3.6', '3.8', '3.10']
       fail-fast: false
     timeout-minutes: 5
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
@@ -54,10 +54,10 @@
         tox -v -e${{ matrix.python-version }}
 
   functional_test_crm_report_bugs:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for crm_report
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -65,10 +65,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF crm_report_bugs`
 
   functional_test_bootstrap_bugs:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for bootstrap bugs
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -76,10 +76,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF bootstrap_bugs`
 
   functional_test_bootstrap_common:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for bootstrap common
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -87,10 +87,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF bootstrap_init_join_remove`
 
   functional_test_bootstrap_options:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for bootstrap options
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -98,10 +98,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF bootstrap_options`
 
   functional_test_qdevice_setup_remove:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for qdevice setup and remove
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -109,10 +109,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF qdevice_setup_remove`
 
   functional_test_qdevice_options:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for qdevice options
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -120,10 +120,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF qdevice_options`
 
   functional_test_qdevice_validate:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for qdevice validate
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -131,10 +131,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF qdevice_validate`
 
   functional_test_qdevice_user_case:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for qdevice user case
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -142,10 +142,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF qdevice_usercase`
 
   functional_test_resource_subcommand:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for resource subcommand
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -153,10 +153,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF resource_failcount resource_set`
 
   functional_test_configure_sublevel:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for configure sublevel bugs
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -164,10 +164,10 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF configure_bugs`
 
   functional_test_constraints_bugs:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for constraints bugs
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
@@ -175,21 +175,32 @@
         $DOCKER_SCRIPT `$GET_INDEX_OF constraints_bugs`
 
   functional_test_geo_cluster:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: functional test for geo cluster
       run:  |
         echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
         sudo systemctl restart docker.service
         $DOCKER_SCRIPT `$GET_INDEX_OF geo_setup`
 
+  functional_test_healthcheck:
+    runs-on: ubuntu-20.04
+    timeout-minutes: 40
+    steps:
+    - uses: actions/checkout@v3
+    - name: functional test for healthcheck
+      run:  |
+        echo '{ "exec-opts": ["native.cgroupdriver=systemd"] }' | sudo tee 
/etc/docker/daemon.json
+        sudo systemctl restart docker.service
+        $DOCKER_SCRIPT `$GET_INDEX_OF healthcheck`
+
   original_regression_test:
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 40
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: original regression test
       run:  |
         $DOCKER_SCRIPT `$GET_INDEX_OF "regression test"`
@@ -209,11 +220,12 @@
       functional_test_configure_sublevel,
       functional_test_constraints_bugs,
       functional_test_geo_cluster,
+      functional_test_healthcheck,
       original_regression_test]
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 10
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: delivery process
       if: github.repository == 'ClusterLabs/crmsh' && github.ref == 
'refs/heads/master' && github.event_name == 'push'
       run: |
@@ -229,10 +241,10 @@
 
   submit:
     needs: delivery
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-20.04
     timeout-minutes: 10
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: submit process
       if: github.repository == 'ClusterLabs/crmsh' && github.ref == 
'refs/heads/master' && github.event_name == 'push'
       run: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/crmsh/bootstrap.py 
new/crmsh-4.4.1+20221122.102a8e11/crmsh/bootstrap.py
--- old/crmsh-4.4.1+20221116.4faefec3/crmsh/bootstrap.py        2022-11-16 
04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/crmsh/bootstrap.py        2022-11-22 
15:17:41.000000000 +0100
@@ -25,6 +25,7 @@
 from pathlib import Path
 from contextlib import contextmanager
 from . import config
+from . import upgradeutil
 from . import utils
 from . import xmlutil
 from .cibconfig import mkset_obj, cib_factory
@@ -1251,6 +1252,10 @@
     _context.sbd_manager.sbd_init()
 
 
+def init_upgradeutil():
+    upgradeutil.force_set_local_upgrade_seq()
+
+
 def init_ocfs2():
     """
     OCFS2 configure process
@@ -1426,7 +1431,14 @@
         if user == "root":
             copy_ssh_key(public_key, user, remote_node)
         else:
-            append_to_remote_file(public_key, remote_node, authorized_file)
+            try:
+                append_to_remote_file(public_key, remote_node, authorized_file)
+            except ValueError:
+                utils.get_stdout_or_raise_error(
+                    '/usr/bin/env python3 -m crmsh.healthcheck fix-cluster 
PasswordlessHaclusterAuthenticationFeature',
+                    remote_node,
+                )
+                append_to_remote_file(public_key, remote_node, authorized_file)
 
     if add:
         configure_ssh_key(remote=remote_node)
@@ -2049,6 +2061,7 @@
         init_corosync()
         init_remote_auth()
         init_sbd()
+        init_upgradeutil()
 
         lock_inst = lock.Lock()
         try:
@@ -2120,6 +2133,7 @@
             cluster_node = prompt_for_string("IP address or hostname of 
existing node (e.g.: 192.168.1.1)", ".+")
             _context.cluster_node = cluster_node
 
+        init_upgradeutil()
         utils.ping_node(cluster_node)
 
         join_ssh(cluster_node)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/crmsh/healthcheck.py 
new/crmsh-4.4.1+20221122.102a8e11/crmsh/healthcheck.py
--- old/crmsh-4.4.1+20221116.4faefec3/crmsh/healthcheck.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/crmsh/healthcheck.py      2022-11-22 
15:17:41.000000000 +0100
@@ -0,0 +1,271 @@
+import logging
+import argparse
+import os
+import os.path
+import parallax
+import subprocess
+import sys
+import typing
+
+import crmsh.parallax
+import crmsh.utils
+
+
+logger = logging.getLogger(__name__)
+
+
+class Feature:
+    _feature_registry = dict()
+
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+        Feature._feature_registry[cls.__name__.rsplit('.', 1)[-1]] = cls
+
+    @staticmethod
+    def get_feature_by_name(name: str):
+        return Feature._feature_registry[name]
+
+    def check_quick(self) -> bool:
+        raise NotImplementedError
+
+    def check_local(self, nodes: typing.Iterable[str]) -> bool:
+        """Check whether the feature is functional on local node."""
+        raise NotImplementedError
+
+    def check_cluster(self, nodes: typing.Iterable[str]) -> bool:
+        """Check whether the feature is functional on the cluster."""
+        raise NotImplementedError
+
+    def fix_local(self, nodes: typing.Iterable[str], ask: 
typing.Callable[[str], None]) -> None:
+        """Fix the feature on local node.
+
+        At least one of fix_local and fix_cluster should be implemented. If 
fix_local is not implemented, this method
+        will be run on each node.
+        """
+        raise NotImplementedError
+
+    def fix_cluster(self, nodes: typing.Iterable[str], ask: 
typing.Callable[[str], None]) -> None:
+        """Fix the feature on the cluster.
+
+        At least one of fix_local and fix_cluster should be implemented. If 
this method is not implemented, fix_local
+        will be run on each node.
+        """
+        raise NotImplementedError
+
+
+class FixFailure(Exception):
+    pass
+
+
+class AskDeniedByUser(Exception):
+    pass
+
+
+def feature_quick_check(feature: Feature):
+    return feature.check_quick()
+
+
+def feature_local_check(feature: Feature, nodes: typing.Iterable[str]):
+    try:
+        if not feature.check_quick():
+            return False
+    except NotImplementedError:
+        pass
+    return feature.check_local(nodes)
+
+
+def feature_full_check(feature: Feature, nodes: typing.Iterable[str]) -> bool:
+    try:
+        if not feature.check_quick():
+            return False
+    except NotImplementedError:
+        pass
+    try:
+        if not feature.check_local(nodes):
+            return False
+    except NotImplementedError:
+        pass
+    try:
+        return feature.check_cluster(nodes)
+    except NotImplementedError:
+        results = _parallax_run(
+            nodes,
+            '/usr/bin/env python3 -m crmsh.healthcheck check-local {}'.format(
+                feature.__class__.__name__.rsplit('.', 1)[-1],
+            )
+        )
+        return all(rc == 0 for rc, _, _ in results.values())
+
+
+def feature_fix(feature: Feature, nodes: typing.Iterable[str], ask: 
typing.Callable[[str], None]) -> None:
+    try:
+        return feature.fix_cluster(nodes, ask)
+    except NotImplementedError:
+        results = _parallax_run(
+            nodes,
+            '/usr/bin/env python3 -m crmsh.healthcheck fix-local {}'.format(
+                feature.__class__.__name__.rsplit('.', 1)[-1],
+            )
+        )
+        if any(rc != 0 for rc, _, _ in results.values()):
+            raise FixFailure
+
+
+class PasswordlessHaclusterAuthenticationFeature(Feature):
+    SSH_DIR = os.path.expanduser('~hacluster/.ssh')
+    KEY_TYPES = ['ed25519', 'ecdsa', 'rsa']
+
+    def check_quick(self) -> bool:
+        for key_type in self.KEY_TYPES:
+            try:
+                os.stat('{}/{}'.format(self.SSH_DIR, key_type))
+                os.stat('{}/{}.pub'.format(self.SSH_DIR, key_type))
+                return True
+            except FileNotFoundError:
+                pass
+        return False
+
+    def check_local(self, nodes: typing.Iterable[str]) -> bool:
+        try:
+            for node in nodes:
+                subprocess.check_call(
+                    ['sudo', 'su', '-', 'hacluster', '-c', 'ssh hacluster@{} 
true'.format(node)],
+                    stdin=subprocess.DEVNULL,
+                    stdout=subprocess.DEVNULL,
+                    stderr=subprocess.DEVNULL,
+                )
+            return True
+        except subprocess.CalledProcessError:
+            return False
+
+    def fix_cluster(self, nodes: typing.Iterable[str], ask: 
typing.Callable[[str], None]) -> None:
+        logger.debug("setup passwordless ssh authentication for user 
hacluster")
+        try:
+            nodes_without_keys = [
+                node for node, result in
+                _parallax_run(
+                    nodes,
+                    '[ -f ~hacluster/.ssh/id_rsa ] || [ -f 
~hacluster/.ssh/id_ecdsa ] || [ -f ~hacluster/.ssh/id_ed25519 ]'
+                ).items()
+                if result[0] != 0
+            ]
+        except parallax.Error:
+            raise FixFailure()
+        if nodes_without_keys:
+            ask("Setup passwordless ssh authentication for user hacluster?")
+            if len(nodes_without_keys) == len(nodes):
+                # pick one node to run init ssh on it
+                init_node = nodes_without_keys[0]
+                # and run join ssh on other nodes
+                join_nodes = list()
+                join_nodes.extend(nodes)
+                join_nodes.remove(init_node)
+                join_target_node = init_node
+            else:
+                nodes_with_keys = set(nodes) - set(nodes_without_keys)
+                # no need to init ssh
+                init_node = None
+                join_nodes = nodes_without_keys
+                # pick one node as join target
+                join_target_node = next(iter(nodes_with_keys))
+            if init_node is not None:
+                try:
+                    crmsh.parallax.parallax_call([init_node], 'crm cluster 
init ssh -y')
+                except ValueError as e:
+                    logger.error('Failed to initialize passwordless ssh 
authentication on node %s.', init_node, exc_info=e)
+                    raise FixFailure from None
+            try:
+                for node in join_nodes:
+                    crmsh.parallax.parallax_call([node], 'crm cluster join ssh 
-c {} -y'.format(join_target_node))
+            except ValueError as e:
+                logger.error('Failed to initialize passwordless ssh 
authentication.', exc_info=e)
+                raise FixFailure from None
+
+
+def _parallax_run(nodes: str, cmd: str) -> typing.Dict[str, typing.Tuple[int, 
bytes, bytes]]:
+    parallax_options = parallax.Options()
+    parallax_options.ssh_options = ['StrictHostKeyChecking=no', 
'ConnectTimeout=10']
+    ret = dict()
+    for node, result in parallax.run(nodes, cmd, parallax_options).items():
+        if isinstance(result, parallax.Error):
+            logger.warning("SSH connection to remote node %s failed.", node, 
exc_info=result)
+            raise result
+        ret[node] = result
+    return ret
+
+
+def main_check_local(args) -> int:
+    try:
+        feature = Feature.get_feature_by_name(args.feature)()
+        nodes = crmsh.utils.list_cluster_nodes(no_reg=True)
+        if nodes:
+            if feature_local_check(feature, nodes):
+                return 0
+            else:
+                return 1
+    except KeyError:
+        logger.error('No such feature: %s.', args.feature)
+    return 2
+
+
+def main_fix_local(args) -> int:
+    try:
+        feature = Feature.get_feature_by_name(args.feature)()
+        nodes = crmsh.utils.list_cluster_nodes(no_reg=True)
+        if nodes:
+            if args.yes:
+                def ask(msg): return True
+            else:
+                def ask(msg): return crmsh.utils.ask('Healthcheck: fix: ' + 
msg)
+            if args.without_check or not feature_local_check(feature, nodes):
+                feature.fix_local(nodes, ask)
+            return 0
+    except KeyError:
+        logger.error('No such feature: %s.', args.feature)
+    return 2
+
+
+def main_fix_cluster(args) -> int:
+    try:
+        feature = Feature.get_feature_by_name(args.feature)()
+        nodes = crmsh.utils.list_cluster_nodes(no_reg=True)
+        if nodes:
+            if args.yes:
+                def ask(msg): return True
+            else:
+                def ask(msg): return crmsh.utils.ask('Healthcheck: fix: ' + 
msg)
+            if args.without_check or not feature_full_check(feature, nodes):
+                feature_fix(feature, nodes, ask)
+            return 0
+    except KeyError:
+        logger.error('No such feature: %s.', args.feature)
+    return 2
+
+
+def main() -> int:
+    # This entrance is for internal programmatic use only.
+    parser = argparse.ArgumentParser()
+    subparsers = parser.add_subparsers()
+
+    check_local_parser = subparsers.add_parser('check-local')
+    check_local_parser.add_argument('feature')
+    check_local_parser.set_defaults(func=main_check_local)
+
+    fix_cluster_parser = subparsers.add_parser('fix-local')
+    fix_cluster_parser.add_argument('--yes', action='store_true')
+    fix_cluster_parser.add_argument('--without-check', action='store_true')
+    fix_cluster_parser.add_argument('feature')
+    fix_cluster_parser.set_defaults(func=main_fix_local)
+
+    fix_cluster_parser = subparsers.add_parser('fix-cluster')
+    fix_cluster_parser.add_argument('--yes', action='store_true')
+    fix_cluster_parser.add_argument('--without-check', action='store_true')
+    fix_cluster_parser.add_argument('feature')
+    fix_cluster_parser.set_defaults(func=main_fix_cluster)
+
+    args = parser.parse_args()
+    return args.func(args)
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/crmsh/main.py 
new/crmsh-4.4.1+20221122.102a8e11/crmsh/main.py
--- old/crmsh-4.4.1+20221116.4faefec3/crmsh/main.py     2022-11-16 
04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/crmsh/main.py     2022-11-22 
15:17:41.000000000 +0100
@@ -11,6 +11,7 @@
 from . import constants
 from . import clidisplay
 from . import term
+from . import upgradeutil
 from . import utils
 from . import userdir
 
@@ -371,6 +372,7 @@
         if options.profile:
             return profile_run(context, user_args)
         else:
+            upgradeutil.upgrade_if_needed()
             return main_input_loop(context, user_args)
     except KeyboardInterrupt:
         print("Ctrl-C, leaving")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/crmsh/upgradeutil.py 
new/crmsh-4.4.1+20221122.102a8e11/crmsh/upgradeutil.py
--- old/crmsh-4.4.1+20221116.4faefec3/crmsh/upgradeutil.py      1970-01-01 
01:00:00.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/crmsh/upgradeutil.py      2022-11-22 
15:17:41.000000000 +0100
@@ -0,0 +1,172 @@
+import logging
+import os.path
+import typing
+
+import parallax
+import sys
+
+import crmsh.healthcheck
+import crmsh.parallax
+import crmsh.utils
+
+
+# pump this seq when upgrade check need to be run
+CURRENT_UPGRADE_SEQ = (1, 0)
+DATA_DIR = os.path.expanduser('~hacluster/crmsh')
+SEQ_FILE_PATH = DATA_DIR + '/upgrade_seq'
+# touch this file to force a upgrade process
+FORCE_UPGRADE_FILE_PATH = DATA_DIR + '/upgrade_forced'
+
+
+VERSION_FEATURES = {
+    (1, 0): [crmsh.healthcheck.PasswordlessHaclusterAuthenticationFeature]
+}
+
+
+logger = logging.getLogger(__name__)
+
+
+class _SkipUpgrade(Exception):
+    pass
+
+
+def _parse_upgrade_seq(s: bytes) -> typing.Tuple[int, int]:
+    parts = s.split(b'.', 1)
+    if len(parts) != 2:
+        raise ValueError('Invalid upgrade seq {}'.format(s))
+    major = int(parts[0])
+    minor = int(parts[1])
+    return major, minor
+
+
+def _format_upgrade_seq(s: typing.Tuple[int, int]) -> str:
+    return '.'.join((str(x) for x in s))
+
+
+def _get_file_content(path, default=None):
+    try:
+        with open(path, 'rb') as f:
+            return f.read()
+    except FileNotFoundError:
+        return default
+
+
+def _parallax_run(nodes: str, cmd: str) -> typing.Dict[str, typing.Tuple[int, 
bytes, bytes]]:
+    parallax_options = parallax.Options()
+    parallax_options.ssh_options = ['StrictHostKeyChecking=no', 
'ConnectTimeout=10']
+    ret = dict()
+    for node, result in parallax.run(nodes, cmd, parallax_options).items():
+        if isinstance(result, parallax.Error):
+            logger.warning("SSH connection to remote node %s failed.", node, 
exc_info=result)
+            raise result
+        ret[node] = result
+    return ret
+
+
+def _is_upgrade_needed(nodes):
+    """decide whether upgrading is needed by checking local sequence file"""
+    needed = False
+    try:
+        os.stat(FORCE_UPGRADE_FILE_PATH)
+        needed = True
+    except FileNotFoundError:
+        pass
+    if not needed:
+        try:
+            local_seq = _parse_upgrade_seq(_get_file_content(SEQ_FILE_PATH, 
b'').strip())
+        except ValueError:
+            local_seq = (0, 0)
+        needed = CURRENT_UPGRADE_SEQ > local_seq
+    return needed
+
+
+def _is_cluster_target_seq_consistent(nodes):
+    cmd = '/usr/bin/env python3 -m crmsh.upgradeutil get-seq'
+    try:
+        results = list(_parallax_run(nodes, cmd).values())
+    except parallax.Error as e:
+        raise _SkipUpgrade() from None
+    try:
+        return all(CURRENT_UPGRADE_SEQ == _parse_upgrade_seq(stdout.strip()) 
if rc == 0 else False for rc, stdout, stderr in results)
+    except ValueError as e:
+        logger.warning("Remote command '%s' returns unexpected output: %s", 
cmd, results, exc_info=e)
+        return False
+
+
+def _get_minimal_seq_in_cluster(nodes) -> typing.Tuple[int, int]:
+    try:
+        return min(
+            _parse_upgrade_seq(stdout.strip()) if rc == 0 else (0, 0)
+            for rc, stdout, stderr in _parallax_run(nodes, 'cat 
{}'.format(SEQ_FILE_PATH)).values()
+        )
+    except ValueError:
+        return 0, 0
+
+
+def _upgrade(nodes, seq):
+    def ask(msg: str):
+        if not crmsh.utils.ask('Upgrade of crmsh configuration: ' + msg):
+            raise crmsh.healthcheck.AskDeniedByUser()
+    try:
+        for key in VERSION_FEATURES.keys():
+            if seq < key <= CURRENT_UPGRADE_SEQ:
+                for feature_class in VERSION_FEATURES[key]:
+                    feature = feature_class()
+                    if crmsh.healthcheck.feature_full_check(feature, nodes):
+                        logger.info("Upgrade: feature %s is already 
functional.")
+                    else:
+                        logger.debug("Upgrade: fixing feature %s...")
+                        crmsh.healthcheck.feature_fix(feature, nodes, ask)
+        logger.info("Upgrade of crmsh configuration succeeded.")
+    except crmsh.healthcheck.AskDeniedByUser:
+        raise _SkipUpgrade() from None
+
+
+def upgrade_if_needed():
+    nodes = crmsh.utils.list_cluster_nodes(no_reg=True)
+    if nodes and _is_upgrade_needed(nodes):
+        logger.info("crmsh version is newer than its configuration. 
Configuration upgrade is needed.")
+        try:
+            if not _is_cluster_target_seq_consistent(nodes):
+                logger.warning("crmsh version is inconsistent in cluster.")
+                raise _SkipUpgrade()
+            seq = _get_minimal_seq_in_cluster(nodes)
+            logger.debug(
+                "Upgrading crmsh configuration from seq %s to %s.",
+                seq, _format_upgrade_seq(CURRENT_UPGRADE_SEQ),
+            )
+            _upgrade(nodes, seq)
+        except _SkipUpgrade:
+            logger.warning("Upgrade of crmsh configuration skipped.")
+            return
+        crmsh.parallax.parallax_call(
+            nodes,
+            "mkdir -p '{}' && echo '{}' > '{}'".format(
+                DATA_DIR,
+                _format_upgrade_seq(CURRENT_UPGRADE_SEQ),
+                SEQ_FILE_PATH,
+            ),
+        )
+        crmsh.parallax.parallax_call(nodes, 'rm -f 
{}'.format(FORCE_UPGRADE_FILE_PATH))
+        logger.debug("Upgrade of crmsh configuration finished.", seq)
+
+
+def force_set_local_upgrade_seq():
+    """Create the upgrade sequence file and set it to CURRENT_UPGRADE_SEQ.
+
+    It should only be used when initializing new cluster nodes."""
+    try:
+        os.mkdir(DATA_DIR)
+    except FileExistsError:
+        pass
+    with open(SEQ_FILE_PATH, 'w', encoding='ascii') as f:
+        print(_format_upgrade_seq(CURRENT_UPGRADE_SEQ), file=f)
+
+
+def main():
+    if sys.argv[1] == 'get-seq':
+        print(_format_upgrade_seq(CURRENT_UPGRADE_SEQ))
+
+
+if __name__ == '__main__':
+    main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/crmsh/utils.py 
new/crmsh-4.4.1+20221122.102a8e11/crmsh/utils.py
--- old/crmsh-4.4.1+20221116.4faefec3/crmsh/utils.py    2022-11-16 
04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/crmsh/utils.py    2022-11-22 
15:17:41.000000000 +0100
@@ -1703,13 +1703,13 @@
         print("{}\n".format(out))
 
 
-def list_cluster_nodes():
+def list_cluster_nodes(no_reg=False):
     '''
     Returns a list of nodes in the cluster.
     '''
     from . import xmlutil
     cib = None
-    rc, out, err = get_stdout_stderr(constants.CIB_QUERY)
+    rc, out, err = get_stdout_stderr(constants.CIB_QUERY, no_reg=no_reg)
     # When cluster service running
     if rc == 0:
         cib = etree.fromstring(out)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/data-manifest 
new/crmsh-4.4.1+20221122.102a8e11/data-manifest
--- old/crmsh-4.4.1+20221116.4faefec3/data-manifest     2022-11-16 
04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/data-manifest     2022-11-22 
15:17:41.000000000 +0100
@@ -75,6 +75,7 @@
 test/features/crm_report_bugs.feature
 test/features/environment.py
 test/features/geo_setup.feature
+test/features/healthcheck.feature
 test/features/ocfs2.feature
 test/features/qdevice_options.feature
 test/features/qdevice_setup_remove.feature
@@ -186,6 +187,7 @@
 test/unittests/test_crashtest_utils.py
 test/unittests/test_gv.py
 test/unittests/test_handles.py
+test/unittests/test_healthcheck.py
 test/unittests/test_lock.py
 test/unittests/test_objset.py
 test/unittests/test_ocfs2.py
@@ -197,6 +199,7 @@
 test/unittests/test_scripts.py
 test/unittests/test_time.py
 test/unittests/test_ui_cluster.py
+test/unittests/test_upgradeuitl.py
 test/unittests/test_utils.py
 test/unittests/test_watchdog.py
 test/unittests/test_xmlutil.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/crmsh-4.4.1+20221116.4faefec3/test/crm-interface 
new/crmsh-4.4.1+20221122.102a8e11/test/crm-interface
--- old/crmsh-4.4.1+20221116.4faefec3/test/crm-interface        2022-11-16 
04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/test/crm-interface        2022-11-22 
15:17:41.000000000 +0100
@@ -67,6 +67,10 @@
 }
 crm_filesession() {
        local _file=`mktemp`
+        $CRM_NO_REG -c $CIB<<EOF
+configure
+delete node1
+EOF
        $CRM -c $CIB configure save xml $_file
        CIB_file=$_file $CRM <<EOF
 `cat`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/test/features/healthcheck.feature 
new/crmsh-4.4.1+20221122.102a8e11/test/features/healthcheck.feature
--- old/crmsh-4.4.1+20221116.4faefec3/test/features/healthcheck.feature 
1970-01-01 01:00:00.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/test/features/healthcheck.feature 
2022-11-22 15:17:41.000000000 +0100
@@ -0,0 +1,27 @@
+@healthcheck
+Feature: healthcheck detect and fix problems in a crmsh deployment
+
+  Tag @clean means need to stop cluster service if the service is available
+  Need nodes: hanode1 hanode2 hanode3
+
+  Background: Setup a two nodes cluster
+    Given   Cluster service is "stopped" on "hanode1"
+    And     Cluster service is "stopped" on "hanode2"
+    And     Cluster service is "stopped" on "hanode3"
+    When    Run "crm cluster init -y" on "hanode1"
+    Then    Cluster service is "started" on "hanode1"
+    And     Show cluster status on "hanode1"
+    When    Run "crm cluster join -c hanode1 -y" on "hanode2"
+    Then    Cluster service is "started" on "hanode2"
+    And     Online nodes are "hanode1 hanode2"
+    And     Show cluster status on "hanode1"
+
+  @clean
+  Scenario: a new node joins when directory ~hacluster/.ssh is removed from 
cluster
+    When    Run "rm -rf ~hacluster/.ssh" on "hanode1"
+    And     Run "rm -rf ~hacluster/.ssh" on "hanode2"
+    And     Run "crm cluster join -c hanode1 -y" on "hanode3"
+    Then    Cluster service is "started" on "hanode3"
+    And     File "~hacluster/.ssh/id_rsa" exists on "hanode1"
+    And     File "~hacluster/.ssh/id_rsa" exists on "hanode2"
+    And     File "~hacluster/.ssh/id_rsa" exists on "hanode3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/test/features/steps/step_implementation.py 
new/crmsh-4.4.1+20221122.102a8e11/test/features/steps/step_implementation.py
--- 
old/crmsh-4.4.1+20221116.4faefec3/test/features/steps/step_implementation.py    
    2022-11-16 04:36:27.000000000 +0100
+++ 
new/crmsh-4.4.1+20221122.102a8e11/test/features/steps/step_implementation.py    
    2022-11-22 15:17:41.000000000 +0100
@@ -404,3 +404,8 @@
     resource.setrlimit(resource.RLIMIT_NOFILE, (50, 50))
     for i in range(51):
         parallax.parallax_call([node], "true")
+
+
+@then('File "{path}" exists on "{node}"')
+def step_impl(context, path, node):
+    parallax.parallax_call([node], '[ -f {} ]'.format(path))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_healthcheck.py 
new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_healthcheck.py
--- old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_healthcheck.py        
1970-01-01 01:00:00.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_healthcheck.py        
2022-11-22 15:17:41.000000000 +0100
@@ -0,0 +1,75 @@
+import unittest
+from unittest import mock
+import sys
+
+from crmsh import healthcheck
+
+
+class _Py37MockCallShim:
+    def __init__(self, mock_call):
+        self._mock_call = mock_call
+
+    def __getattr__(self, item):
+        f = getattr(self._mock_call, item)
+
+        def g(*args, **kwargs):
+            return f(*((self._mock_call, ) + args[1:]), **kwargs)
+        return g
+
+    @property
+    def args(self):
+        if sys.version_info.major == 3 and sys.version_info.major < 8:
+            return self._mock_call[0]
+        else:
+            return self._mock_call.args
+
+    @property
+    def kwargs(self):
+        def args(self):
+            if sys.version_info.major == 3 and sys.version_info.major < 8:
+                return self._mock_call[1]
+            else:
+                return self._mock_call.kwargs
+
+
+class TestPasswordlessHaclusterAuthenticationFeature(unittest.TestCase):
+    @mock.patch('crmsh.parallax.parallax_call')
+    @mock.patch('crmsh.utils.ask')
+    @mock.patch('crmsh.healthcheck._parallax_run')
+    def test_upgrade_partially_initialized(self, mock_parallax_run, mock_ask, 
mock_parallax_call: mock.MagicMock):
+        nodes = ['node-{}'.format(i) for i in range(1, 6)]
+        return_value = {'node-{}'.format(i): (0, b'', b'') for i in range(1, 
4)}
+        return_value.update({'node-{}'.format(i): (1, b'', b'') for i in 
range(4, 6)})
+        mock_parallax_run.return_value = return_value
+        mock_ask.return_value = True
+        
healthcheck.feature_fix(healthcheck.PasswordlessHaclusterAuthenticationFeature(),
 nodes, mock_ask)
+        
self.assertFalse(any(_Py37MockCallShim(call_args).args[1].startswith('crm 
cluster init ssh') for call_args in mock_parallax_call.call_args_list))
+        self.assertEqual(
+            {'node-{}'.format(i) for i in range(4, 6)},
+            set(
+                _Py37MockCallShim(call_args).args[0][0] for call_args in 
mock_parallax_call.call_args_list
+                if _Py37MockCallShim(call_args).args[1].startswith('crm 
cluster join ssh')
+            ),
+        )
+
+    @mock.patch('crmsh.parallax.parallax_call')
+    @mock.patch('crmsh.utils.ask')
+    @mock.patch('crmsh.healthcheck._parallax_run')
+    def test_upgrade_clean(self, mock_parallax_run, mock_ask, 
mock_parallax_call: mock.MagicMock):
+        nodes = ['node-{}'.format(i) for i in range(1, 6)]
+        mock_parallax_run.return_value = {node: (1, b'', b'') for node in 
nodes}
+        mock_ask.return_value = True
+        
healthcheck.feature_fix(healthcheck.PasswordlessHaclusterAuthenticationFeature(),
 nodes, mock_ask)
+        self.assertEqual(
+            1, len([
+                True for call_args in mock_parallax_call.call_args_list
+                if _Py37MockCallShim(call_args).args[1].startswith('crm 
cluster init ssh')
+            ])
+        )
+        self.assertEqual(
+            len(nodes) - 1,
+            len([
+                True for call_args in mock_parallax_call.call_args_list
+                if _Py37MockCallShim(call_args).args[1].startswith('crm 
cluster join ssh')
+            ]),
+            )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_upgradeuitl.py 
new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_upgradeuitl.py
--- old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_upgradeuitl.py        
1970-01-01 01:00:00.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_upgradeuitl.py        
2022-11-22 15:17:41.000000000 +0100
@@ -0,0 +1,54 @@
+import os
+import sys
+import unittest
+from unittest import mock
+
+from crmsh import upgradeutil
+
+
+class TestUpgradeCondition(unittest.TestCase):
+    @mock.patch('crmsh.upgradeutil._get_file_content')
+    @mock.patch('os.stat')
+    def test_is_upgrade_needed_by_force_upgrade(self, mock_stat: 
mock.MagicMock, mock_get_file_content):
+        mock_stat.return_value = mock.Mock(os.stat_result)
+        mock_get_file_content.return_value = b''
+        self.assertTrue(upgradeutil._is_upgrade_needed(['node-1', 'node-2']))
+
+    @mock.patch('crmsh.upgradeutil._get_file_content')
+    @mock.patch('os.stat')
+    def test_is_upgrade_needed_by_non_existent_seq(
+            self,
+            mock_stat: mock.MagicMock,
+            mock_get_file_content: mock.MagicMock,
+    ):
+        mock_stat.side_effect = FileNotFoundError()
+        mock_get_file_content.return_value = b''
+        self.assertTrue(upgradeutil._is_upgrade_needed(['node-1', 'node-2']))
+
+    @mock.patch('crmsh.upgradeutil.CURRENT_UPGRADE_SEQ')
+    @mock.patch('crmsh.upgradeutil._get_file_content')
+    @mock.patch('os.stat')
+    def test_is_upgrade_needed_by_seq_less_than_expected(
+            self,
+            mock_stat,
+            mock_get_file_content,
+            mock_current_upgrade_seq: mock.MagicMock,
+    ):
+        mock_stat.side_effect = FileNotFoundError()
+        mock_get_file_content.return_value = b'0.1\n'
+        mock_current_upgrade_seq.__gt__.return_value = True
+        self.assertTrue(upgradeutil._is_upgrade_needed(['node-1', 'node-2']))
+
+    @mock.patch('crmsh.upgradeutil.CURRENT_UPGRADE_SEQ')
+    @mock.patch('crmsh.upgradeutil._get_file_content')
+    @mock.patch('os.stat')
+    def test_is_upgrade_needed_by_seq_not_less_than_expected(
+            self,
+            mock_stat,
+            mock_get_file_content,
+            mock_current_upgrade_seq: mock.MagicMock,
+    ):
+        mock_stat.side_effect = FileNotFoundError()
+        mock_get_file_content.return_value = b'1.0\n'
+        mock_current_upgrade_seq.__gt__.return_value = False
+        self.assertFalse(upgradeutil._is_upgrade_needed(['node-1', 'node-2']))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_utils.py 
new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_utils.py
--- old/crmsh-4.4.1+20221116.4faefec3/test/unittests/test_utils.py      
2022-11-16 04:36:27.000000000 +0100
+++ new/crmsh-4.4.1+20221122.102a8e11/test/unittests/test_utils.py      
2022-11-22 15:17:41.000000000 +0100
@@ -1560,7 +1560,18 @@
     mock_etree.return_value = None
     res = utils.list_cluster_nodes()
     assert res is None
-    mock_run.assert_called_once_with(constants.CIB_QUERY)
+    mock_run.assert_called_once_with(constants.CIB_QUERY, no_reg=False)
+    mock_etree.assert_called_once_with("data")
+
+
+@mock.patch('crmsh.utils.etree.fromstring')
+@mock.patch('crmsh.utils.get_stdout_stderr')
+def test_list_cluster_nodes_none_no_reg(mock_run, mock_etree):
+    mock_run.return_value = (0, "data", None)
+    mock_etree.return_value = None
+    res = utils.list_cluster_nodes(no_reg=True)
+    assert res is None
+    mock_run.assert_called_once_with(constants.CIB_QUERY, no_reg=True)
     mock_etree.assert_called_once_with("data")
 
 
@@ -1573,7 +1584,7 @@
     mock_isfile.return_value = False
     res = utils.list_cluster_nodes()
     assert res is None
-    mock_run.assert_called_once_with(constants.CIB_QUERY)
+    mock_run.assert_called_once_with(constants.CIB_QUERY, no_reg=False)
     mock_env.assert_called_once_with("CIB_file", constants.CIB_RAW_FILE)
     mock_isfile.assert_called_once_with(constants.CIB_RAW_FILE)
 
@@ -1597,7 +1608,7 @@
     res = utils.list_cluster_nodes()
     assert res == ["node2"]
 
-    mock_run.assert_called_once_with(constants.CIB_QUERY)
+    mock_run.assert_called_once_with(constants.CIB_QUERY, no_reg=False)
     mock_env.assert_called_once_with("CIB_file", constants.CIB_RAW_FILE)
     mock_isfile.assert_called_once_with(constants.CIB_RAW_FILE)
     mock_file2elem.assert_called_once_with(constants.CIB_RAW_FILE)

Reply via email to