Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-senlinclient for 
openSUSE:Factory checked in at 2022-05-25 20:34:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-senlinclient (Old)
 and      /work/SRC/openSUSE:Factory/.python-senlinclient.new.2254 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-senlinclient"

Wed May 25 20:34:42 2022 rev:13 rq:979086 version:2.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-senlinclient/python-senlinclient.changes  
2021-05-10 15:39:58.489418198 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-senlinclient.new.2254/python-senlinclient.changes
        2022-05-25 20:35:10.544279009 +0200
@@ -1,0 +2,20 @@
+Tue May 24 21:37:24 UTC 2022 - [email protected]
+
+- update to version 2.4.0
+  - Use py3 as the default runtime for tox
+  - Update master for stable/wallaby
+  - Add Python3 xena unit tests
+  - Add Python3 yoga unit tests
+  - Update master for stable/xena
+
+-------------------------------------------------------------------
+Tue Oct 26 22:00:44 UTC 2021 - [email protected]
+
+- update to version 2.3.0
+  - Adds --wait argument for cluster CLI interactions
+  - Fix lower-constraints
+  - Fix config and metadata in cluster update
+  - trivial: Drop references to os-testr
+  - Update TOX_CONSTRAINTS_FILE
+
+-------------------------------------------------------------------

Old:
----
  python-senlinclient-2.2.1.tar.gz

New:
----
  python-senlinclient-2.4.0.tar.gz

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

Other differences:
------------------
++++++ python-senlinclient.spec ++++++
--- /var/tmp/diff_new_pack.qRvaex/_old  2022-05-25 20:35:11.520280371 +0200
+++ /var/tmp/diff_new_pack.qRvaex/_new  2022-05-25 20:35:11.524280377 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-senlinclient
 #
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,21 +17,19 @@
 
 
 Name:           python-senlinclient
-Version:        2.2.1
+Version:        2.4.0
 Release:        0
 Summary:        Python API and CLI for OpenStack Senlin
 License:        Apache-2.0
 Group:          Development/Languages/Python
 URL:            https://docs.openstack.org/python-senlinclient
-Source0:        
https://files.pythonhosted.org/packages/source/p/python-senlinclient/python-senlinclient-2.2.1.tar.gz
+Source0:        
https://files.pythonhosted.org/packages/source/p/python-senlinclient/python-senlinclient-2.4.0.tar.gz
 BuildRequires:  openstack-macros
-BuildRequires:  python3-Babel
 BuildRequires:  python3-PrettyTable >= 0.7.2
-BuildRequires:  python3-PyYAML >= 3.13
+BuildRequires:  python3-PyYAML >= 5.3.1
 BuildRequires:  python3-devel
 BuildRequires:  python3-fixtures
 BuildRequires:  python3-heatclient >= 1.10.0
-BuildRequires:  python3-mock
 BuildRequires:  python3-openstackclient
 BuildRequires:  python3-openstacksdk >= 0.24.0
 BuildRequires:  python3-osc-lib >= 1.11.0
@@ -53,11 +51,10 @@
 
 %package -n python3-senlinclient
 Summary:        Python API and CLI for OpenStack Senlin
-Requires:       python3-Babel
 Requires:       python3-PrettyTable >= 0.7.2
-Requires:       python3-PyYAML >= 3.13
+Requires:       python3-PyYAML >= 5.3.1
 Requires:       python3-heatclient >= 1.10.0
-Requires:       python3-keystoneauth1 >= 3.4.0
+Requires:       python3-keystoneauth1 >= 3.11.0
 Requires:       python3-openstackclient
 Requires:       python3-openstacksdk >= 0.24.0
 Requires:       python3-osc-lib >= 1.11.0
@@ -66,7 +63,6 @@
 Requires:       python3-oslo.utils >= 3.33.0
 Requires:       python3-pbr >= 2.0.0
 Requires:       python3-requests >= 2.14.2
-Requires:       python3-six
 
 %description -n python3-senlinclient
 OpenStack Clustering service Provisioning API Client Library
@@ -89,7 +85,7 @@
 auto-generated documentation.
 
 %prep
-%autosetup -p1 -n python-senlinclient-2.2.1
+%autosetup -p1 -n python-senlinclient-2.4.0
 %py_req_cleanup
 
 %build

++++++ _service ++++++
--- /var/tmp/diff_new_pack.qRvaex/_old  2022-05-25 20:35:11.548280410 +0200
+++ /var/tmp/diff_new_pack.qRvaex/_new  2022-05-25 20:35:11.552280416 +0200
@@ -1,8 +1,8 @@
 <services>
   <service mode="disabled" name="renderspec">
-    <param 
name="input-template">https://opendev.org/openstack/rpm-packaging/raw/branch/stable/wallaby/openstack/python-senlinclient/python-senlinclient.spec.j2</param>
+    <param 
name="input-template">https://opendev.org/openstack/rpm-packaging/raw/master/openstack/python-senlinclient/python-senlinclient.spec.j2</param>
     <param name="output-name">python-senlinclient.spec</param>
-    <param 
name="requirements">https://opendev.org/openstack/python-senlinclient/raw/branch/stable/wallaby/requirements.txt</param>
+    <param 
name="requirements">https://opendev.org/openstack/python-senlinclient/raw/branch/master/requirements.txt</param>
     <param name="changelog-email">[email protected]</param>
     <param name="changelog-provider">gh,openstack,python-senlinclient</param>
   </service>

++++++ python-senlinclient-2.2.1.tar.gz -> python-senlinclient-2.4.0.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/.zuul.yaml 
new/python-senlinclient-2.4.0/.zuul.yaml
--- old/python-senlinclient-2.2.1/.zuul.yaml    2021-03-11 12:03:35.000000000 
+0100
+++ new/python-senlinclient-2.4.0/.zuul.yaml    2022-02-25 17:20:21.000000000 
+0100
@@ -32,7 +32,7 @@
     templates:
       - check-requirements
       - openstack-lower-constraints-jobs
-      - openstack-python3-wallaby-jobs
+      - openstack-python3-yoga-jobs
       - openstackclient-plugin-jobs
       - publish-openstack-docs-pti
       - release-notes-jobs-python3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/AUTHORS 
new/python-senlinclient-2.4.0/AUTHORS
--- old/python-senlinclient-2.2.1/AUTHORS       2021-03-11 12:04:24.000000000 
+0100
+++ new/python-senlinclient-2.4.0/AUTHORS       2022-02-25 17:21:06.000000000 
+0100
@@ -44,6 +44,7 @@
 Saju <[email protected]>
 Sean McGinnis <[email protected]>
 Sharat Sharma <[email protected]>
+Stephen Finucane <[email protected]>
 Tang Chen <[email protected]>
 Thomas Bechtold <[email protected]>
 Thomas Herve <[email protected]>
@@ -67,6 +68,7 @@
 dixiaoli <[email protected]>
 gecong1973 <[email protected]>
 gengchc2 <[email protected]>
+gugug <[email protected]>
 howardlee <[email protected]>
 huangtianhua <[email protected]>
 jacky06 <[email protected]>
@@ -95,6 +97,7 @@
 xiaozhuangqing <[email protected]>
 xu-haiwei <[email protected]>
 yanyanhu <[email protected]>
+zhangboye <[email protected]>
 zhangguoqing <[email protected]>
 zhangyanxian <[email protected]>
 zhurong <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/ChangeLog 
new/python-senlinclient-2.4.0/ChangeLog
--- old/python-senlinclient-2.2.1/ChangeLog     2021-03-11 12:04:24.000000000 
+0100
+++ new/python-senlinclient-2.4.0/ChangeLog     2022-02-25 17:21:06.000000000 
+0100
@@ -1,10 +1,27 @@
 CHANGES
 =======
 
+2.4.0
+-----
+
+* Add Python3 yoga unit tests
+* Update master for stable/xena
+
+2.3.0
+-----
+
+* Fix lower-constraints
+* Use py3 as the default runtime for tox
+* Add Python3 xena unit tests
+* Update master for stable/wallaby
+
 2.2.1
 -----
 
 * Remove unicode from python client
+* Update TOX\_CONSTRAINTS\_FILE
+* Fix config and metadata in cluster update
+* Adds --wait argument for cluster CLI interactions
 
 2.2.0
 -----
@@ -13,6 +30,7 @@
 * Remove install unnecessary packages
 * Add Python3 wallaby unit tests
 * Update master for stable/victoria
+* trivial: Drop references to os-testr
 
 2.1.1
 -----
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/PKG-INFO 
new/python-senlinclient-2.4.0/PKG-INFO
--- old/python-senlinclient-2.2.1/PKG-INFO      2021-03-11 12:04:24.435627500 
+0100
+++ new/python-senlinclient-2.4.0/PKG-INFO      2022-02-25 17:21:06.534387400 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: python-senlinclient
-Version: 2.2.1
+Version: 2.4.0
 Summary: OpenStack Clustering API Client Library
 Home-page: https://docs.openstack.org/python-senlinclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/doc/source/contributor/index.rst 
new/python-senlinclient-2.4.0/doc/source/contributor/index.rst
--- old/python-senlinclient-2.2.1/doc/source/contributor/index.rst      
2021-03-11 12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/doc/source/contributor/index.rst      
2022-02-25 17:20:21.000000000 +0100
@@ -28,7 +28,7 @@
 -----------------
 There are a number of ways to run unit tests currently, and there's a
 combination of frameworks used depending on what commands you use.  The
-preferred method is to use tox, which calls ostestr via the tox.ini file.
+preferred method is to use tox, which calls stestr via the tox.ini file.
 To run all tests simply run::
 
     tox
@@ -52,4 +52,4 @@
     tox -epy27 senlinclient.tests.unit.v1.test_node
 
 For more information on these options and how to run tests, please see the
-`ostestr documentation <https://docs.openstack.org/os-testr/latest/>`_.
+`stestr documentation <https://stestr.readthedocs.io/en/latest/index.html>`_.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/lower-constraints.txt 
new/python-senlinclient-2.4.0/lower-constraints.txt
--- old/python-senlinclient-2.2.1/lower-constraints.txt 2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/lower-constraints.txt 2022-02-25 
17:20:21.000000000 +0100
@@ -15,9 +15,7 @@
 extras==1.0.0
 fasteners==0.7.0
 fixtures==3.0.0
-flake8==2.5.5
 future==0.16.0
-hacking==0.12.0
 idna==2.6
 imagesize==0.7.1
 iso8601==0.1.11
@@ -26,10 +24,9 @@
 jsonpatch==1.16
 jsonpointer==1.13
 jsonschema==2.6.0
-keystoneauth1==3.4.0
+keystoneauth1==3.11.0
 linecache2==1.0.0
 MarkupSafe==1.0
-mccabe==0.2.1
 monotonic==0.6
 mox3==0.20.0
 msgpack-python==0.4.0
@@ -39,7 +36,6 @@
 openstacksdk==0.24.0
 os-client-config==1.28.0
 os-service-types==1.2.0
-os-testr==1.0.0
 osc-lib==1.11.0
 oslo.concurrency==3.25.0
 oslo.config==5.2.0
@@ -51,13 +47,10 @@
 oslotest==3.2.0
 paramiko==2.0.0
 pbr==2.0.0
-pep8==1.5.7
 positional==1.2.1
 prettytable==0.7.2
 pyasn1==0.1.8
 pycparser==2.18
-pyflakes==0.8.1
-Pygments==2.2.0
 pyinotify==0.9.6
 pyOpenSSL==17.1.0
 pyparsing==2.1.0
@@ -72,8 +65,8 @@
 python-openstackclient==3.12.0
 python-subunit==1.0.0
 python-swiftclient==3.2.0
-pytz==2013.6
-PyYAML==3.13
+pytz==2015.7
+PyYAML==5.3.1
 requests==2.14.2
 requests-mock==1.2.0
 requestsexceptions==1.2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/python_senlinclient.egg-info/PKG-INFO 
new/python-senlinclient-2.4.0/python_senlinclient.egg-info/PKG-INFO
--- old/python-senlinclient-2.2.1/python_senlinclient.egg-info/PKG-INFO 
2021-03-11 12:04:24.000000000 +0100
+++ new/python-senlinclient-2.4.0/python_senlinclient.egg-info/PKG-INFO 
2022-02-25 17:21:06.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: python-senlinclient
-Version: 2.2.1
+Version: 2.4.0
 Summary: OpenStack Clustering API Client Library
 Home-page: https://docs.openstack.org/python-senlinclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/python_senlinclient.egg-info/SOURCES.txt 
new/python-senlinclient-2.4.0/python_senlinclient.egg-info/SOURCES.txt
--- old/python-senlinclient-2.2.1/python_senlinclient.egg-info/SOURCES.txt      
2021-03-11 12:04:24.000000000 +0100
+++ new/python-senlinclient-2.4.0/python_senlinclient.egg-info/SOURCES.txt      
2022-02-25 17:21:06.000000000 +0100
@@ -64,6 +64,8 @@
 releasenotes/source/unreleased.rst
 releasenotes/source/ussuri.rst
 releasenotes/source/victoria.rst
+releasenotes/source/wallaby.rst
+releasenotes/source/xena.rst
 releasenotes/source/_static/.placeholder
 releasenotes/source/_templates/.placeholder
 releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/python_senlinclient.egg-info/pbr.json 
new/python-senlinclient-2.4.0/python_senlinclient.egg-info/pbr.json
--- old/python-senlinclient-2.2.1/python_senlinclient.egg-info/pbr.json 
2021-03-11 12:04:24.000000000 +0100
+++ new/python-senlinclient-2.4.0/python_senlinclient.egg-info/pbr.json 
2022-02-25 17:21:06.000000000 +0100
@@ -1 +1 @@
-{"git_version": "e6edaee", "is_release": true}
\ No newline at end of file
+{"git_version": "9fc2edc", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/python_senlinclient.egg-info/requires.txt 
new/python-senlinclient-2.4.0/python_senlinclient.egg-info/requires.txt
--- old/python-senlinclient-2.2.1/python_senlinclient.egg-info/requires.txt     
2021-03-11 12:04:24.000000000 +0100
+++ new/python-senlinclient-2.4.0/python_senlinclient.egg-info/requires.txt     
2022-02-25 17:21:06.000000000 +0100
@@ -1,6 +1,6 @@
-PrettyTable<0.8,>=0.7.2
-PyYAML>=3.13
-keystoneauth1>=3.4.0
+PrettyTable>=0.7.2
+PyYAML>=5.3.1
+keystoneauth1>=3.11.0
 openstacksdk>=0.24.0
 osc-lib>=1.11.0
 oslo.i18n>=3.15.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/releasenotes/source/index.rst 
new/python-senlinclient-2.4.0/releasenotes/source/index.rst
--- old/python-senlinclient-2.2.1/releasenotes/source/index.rst 2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/releasenotes/source/index.rst 2022-02-25 
17:20:21.000000000 +0100
@@ -6,6 +6,8 @@
    :maxdepth: 1
 
    unreleased
+   xena
+   wallaby
    victoria
    ussuri
    train
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/releasenotes/source/wallaby.rst 
new/python-senlinclient-2.4.0/releasenotes/source/wallaby.rst
--- old/python-senlinclient-2.2.1/releasenotes/source/wallaby.rst       
1970-01-01 01:00:00.000000000 +0100
+++ new/python-senlinclient-2.4.0/releasenotes/source/wallaby.rst       
2022-02-25 17:20:21.000000000 +0100
@@ -0,0 +1,6 @@
+============================
+Wallaby Series Release Notes
+============================
+
+.. release-notes::
+   :branch: stable/wallaby
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/releasenotes/source/xena.rst 
new/python-senlinclient-2.4.0/releasenotes/source/xena.rst
--- old/python-senlinclient-2.2.1/releasenotes/source/xena.rst  1970-01-01 
01:00:00.000000000 +0100
+++ new/python-senlinclient-2.4.0/releasenotes/source/xena.rst  2022-02-25 
17:20:21.000000000 +0100
@@ -0,0 +1,6 @@
+=========================
+Xena Series Release Notes
+=========================
+
+.. release-notes::
+   :branch: stable/xena
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/requirements.txt 
new/python-senlinclient-2.4.0/requirements.txt
--- old/python-senlinclient-2.2.1/requirements.txt      2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/requirements.txt      2022-02-25 
17:20:21.000000000 +0100
@@ -3,13 +3,13 @@
 # process, which may cause wedges in the gate later.
 
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
-PrettyTable<0.8,>=0.7.2 # BSD
-keystoneauth1>=3.4.0 # Apache-2.0
+PrettyTable>=0.7.2 # BSD
+keystoneauth1>=3.11.0 # Apache-2.0
 openstacksdk>=0.24.0 # Apache-2.0
 osc-lib>=1.11.0 # Apache-2.0
 oslo.i18n>=3.15.3 # Apache-2.0
 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
 oslo.utils>=3.33.0 # Apache-2.0
 python-heatclient>=1.10.0 # Apache-2.0
-PyYAML>=3.13 # MIT
+PyYAML>=5.3.1 # MIT
 requests>=2.14.2 # Apache-2.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/senlinclient/common/exc.py 
new/python-senlinclient-2.4.0/senlinclient/common/exc.py
--- old/python-senlinclient-2.2.1/senlinclient/common/exc.py    2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/senlinclient/common/exc.py    2022-02-25 
17:20:21.000000000 +0100
@@ -38,6 +38,10 @@
     """Illegal file format detected."""
 
 
+class PollingExceededError(BaseException):
+    """Desired resource state not achived within polling period."""
+
+
 class HTTPException(BaseException):
     """Base exception for all HTTP-derived exceptions."""
     code = 'N/A'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/senlinclient/common/utils.py 
new/python-senlinclient-2.4.0/senlinclient/common/utils.py
--- old/python-senlinclient-2.2.1/senlinclient/common/utils.py  2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/senlinclient/common/utils.py  2022-02-25 
17:20:21.000000000 +0100
@@ -12,15 +12,21 @@
 
 
 from heatclient.common import template_utils
+import logging
+from openstack import exceptions as sdk_exc
 from oslo_serialization import jsonutils
 from oslo_utils import importutils
 import prettytable
+import time
 import yaml
 
 from senlinclient.common import exc
 from senlinclient.common.i18n import _
 
 
+log = logging.getLogger(__name__)
+
+
 def import_versioned_module(version, submodule=None):
     module = 'senlinclient.v%s' % version
     if submodule:
@@ -153,3 +159,78 @@
     }
 
     return new_spec
+
+
+def await_action(senlin_client, action_id,
+                 poll_count_max=10, poll_interval=5):
+
+    def check_action():
+        try:
+            action = senlin_client.get_action(action_id)
+        except sdk_exc.ResourceNotFound:
+            raise exc.CommandError(_('Action not found: %s')
+                                   % action_id)
+        action_states = ['succeeded', 'failed', 'cancelled']
+        if action.status.lower() in action_states:
+            log.info("Action %s completed with status %s."
+                     % (action.id, action.status))
+            return True
+        log.info("Awaiting action %s completion status (current: %s)."
+                 % (action.id, action.status))
+        return False
+
+    _check(check_action, poll_count_max, poll_interval)
+
+
+def await_cluster_status(senlin_client, cluster_id, statuses=None,
+                         poll_count_max=10, poll_interval=5):
+
+    if not statuses or len(statuses) <= 0:
+        statuses = ['ACTIVE', 'ERROR', 'WARNING']
+
+    def check_status():
+        try:
+            cluster = senlin_client.get_cluster(cluster_id)
+        except sdk_exc.ResourceNotFound:
+            raise exc.CommandError(_('Cluster not found: %s') % cluster_id)
+
+        if cluster.status.lower() in [fs.lower() for fs in statuses]:
+            return True
+        log.info("Awaiting cluster status (desired: %s - current: %s)." %
+                 (', '.join(statuses), cluster.status))
+        return False
+
+    _check(check_status, poll_count_max, poll_interval)
+
+
+def await_cluster_delete(senlin_client, cluster_id,
+                         poll_count_max=10, poll_interval=5):
+
+    def check_deleted():
+        try:
+            senlin_client.get_cluster(cluster_id)
+        except sdk_exc.ResourceNotFound:
+            log.info("Successfully deleted cluster %s." % cluster_id)
+            return True
+        log.info("Awaiting cluster deletion for %s." % cluster_id)
+        return False
+
+    _check(check_deleted, poll_count_max, poll_interval)
+
+
+def _check(check_func, poll_count_max=10, poll_interval=5):
+    # a negative poll_count_max is considered indefinite
+
+    poll_increment = 1
+    if poll_count_max < 0:
+        poll_count_max = 1
+        poll_increment = 0
+
+    poll_count = 0
+    while poll_count < poll_count_max:
+        if check_func():
+            return
+
+        time.sleep(poll_interval)
+        poll_count += poll_increment
+    raise exc.PollingExceededError()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/senlinclient/tests/unit/test_utils.py 
new/python-senlinclient-2.4.0/senlinclient/tests/unit/test_utils.py
--- old/python-senlinclient-2.2.1/senlinclient/tests/unit/test_utils.py 
2021-03-11 12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/senlinclient/tests/unit/test_utils.py 
2022-02-25 17:20:21.000000000 +0100
@@ -14,6 +14,7 @@
 from unittest import mock
 
 import testtools
+import time
 
 from senlinclient.common import exc
 from senlinclient.common.i18n import _
@@ -97,3 +98,40 @@
     def test_list_formatter_with_empty_list(self):
         params = []
         self.assertEqual('', utils.list_formatter(params))
+
+    @mock.patch.object(utils, '_check')
+    def test_await_cluster_action(self, mock_check):
+        utils.await_action('fake-client', 'test-action-id')
+        mock_check.assert_called_once()
+
+    @mock.patch.object(utils, '_check')
+    def test_await_cluster_status(self, mock_check):
+        utils.await_cluster_status('fake-client', 'ACTIVE')
+        mock_check.assert_called_once()
+
+    @mock.patch.object(utils, '_check')
+    def test_await_cluster_delete(self, mock_check):
+        utils.await_cluster_delete('fake-client', 'test-cluster-id')
+        mock_check.assert_called_once()
+
+    def test_check(self):
+        check_func = mock.Mock(return_value=True)
+
+        try:
+            utils._check(check_func)
+        except Exception:
+            self.fail("_check() unexpectedly raised an exception")
+
+        check_func.assert_called()
+
+    @mock.patch.object(time, 'sleep')
+    def test_check_raises(self, mock_sleep):
+        mock_check_func = mock.Mock(return_value=False)
+
+        poll_count = 2
+        poll_interval = 1
+
+        self.assertRaises(exc.PollingExceededError, utils._check,
+                          mock_check_func, poll_count, poll_interval)
+        mock_check_func.assert_called()
+        mock_sleep.assert_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-senlinclient-2.2.1/senlinclient/tests/unit/v1/test_cluster.py 
new/python-senlinclient-2.4.0/senlinclient/tests/unit/v1/test_cluster.py
--- old/python-senlinclient-2.2.1/senlinclient/tests/unit/v1/test_cluster.py    
2021-03-11 12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/senlinclient/tests/unit/v1/test_cluster.py    
2022-02-25 17:20:21.000000000 +0100
@@ -18,6 +18,7 @@
 from openstack import exceptions as sdk_exc
 from osc_lib import exceptions as exc
 
+from senlinclient.common import utils as senlin_utils
 from senlinclient.tests.unit.v1 import fakes
 from senlinclient.v1 import cluster as osc_cluster
 
@@ -202,13 +203,14 @@
     def setUp(self):
         super(TestClusterCreate, self).setUp()
         self.cmd = osc_cluster.CreateCluster(self.app, None)
+        self.cluster_id = '7d85f602-a948-4a30-afd4-e84f47471c15'
         fake_cluster = mock.Mock(
             config={},
             created_at="2015-02-11T15:13:20",
             data={},
             desired_capacity=0,
             domain_id=None,
-            id="7d85f602-a948-4a30-afd4-e84f47471c15",
+            id=self.cluster_id,
             init_time="2015-02-10T14:26:11",
             max_size=-1,
             metadata={},
@@ -265,6 +267,24 @@
         self.cmd.take_action(parsed_args)
         self.mock_client.create_cluster.assert_called_with(**kwargs)
 
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_create_with_wait(self, mock_await):
+        arglist = ['test_cluster', '--profile', 'mystack',
+                   '--min-size', '1', '--max-size', '10',
+                   '--desired-capacity', '2', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await.assert_called_once_with(self.mock_client, self.cluster_id)
+
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_create_without_wait(self, mock_await):
+        arglist = ['test_cluster', '--profile', 'mystack',
+                   '--min-size', '1', '--max-size', '10',
+                   '--desired-capacity', '2']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await.assert_not_called()
+
 
 class TestClusterUpdate(TestCluster):
 
@@ -334,6 +354,24 @@
                                   parsed_args)
         self.assertIn('Cluster not found: c6b8b252', str(error))
 
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_update_with_wait(self, mock_await):
+        arglist = ['--name', 'new_cluster', '--metadata', 'nk1=nv1;nk2=nv2',
+                   '--profile', 'new_profile', '--timeout', '30', '45edadcb',
+                   '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await.assert_called_once_with(self.mock_client,
+                                           self.fake_cluster.id)
+
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_update_without_wait(self, mock_await):
+        arglist = ['--name', 'new_cluster', '--metadata', 'nk1=nv1;nk2=nv2',
+                   '--profile', 'new_profile', '--timeout', '30', '45edadcb']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await.assert_not_called()
+
 
 class TestClusterDelete(TestCluster):
     def setUp(self):
@@ -422,6 +460,45 @@
         mock_stdin.readline.assert_called_with()
         self.mock_client.delete_cluster.assert_not_called()
 
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_delete')
+    def test_cluster_delete_with_wait(self, mock_await_cluster,
+                                      mock_await_action):
+        fake_action = {'id': 'fake-action-id'}
+        self.mock_client.delete_cluster = mock.Mock(return_value=fake_action)
+        arglist = ['my_cluster', '--force', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  fake_action['id'])
+        mock_await_cluster.assert_called_once_with(self.mock_client,
+                                                   'my_cluster')
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_delete')
+    def test_cluster_delete_without_wait(self, mock_await_cluster,
+                                         mock_await_action):
+        fake_action = {'id': 'fake-action-id'}
+        self.mock_client.delete_cluster = mock.Mock(return_value=fake_action)
+        arglist = ['my_cluster', '--force']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_cluster.assert_not_called()
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_delete')
+    def test_cluster_delete_with_wait_bad_action(self, mock_await_cluster,
+                                                 mock_await_action):
+        self.mock_client.delete_cluster.side_effect = (
+            Exception('test exception')
+        )
+        arglist = ['my_cluster', '--force', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_cluster.assert_not_called()
+
 
 class TestClusterResize(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -565,6 +642,46 @@
         self.assertEqual('Max size cannot be less than the specified '
                          'capacity.', str(error))
 
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_resize_with_wait(self, mock_await_status,
+                                      mock_await_action, mock_show):
+        arglist = ['--capacity', '2', 'my_cluster', "--wait"]
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_await_status.assert_called_once_with(self.mock_client,
+                                                  'my_cluster')
+        mock_show.assert_called_once()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_resize_without_wait(self, mock_await_status,
+                                         mock_await_action, mock_show):
+        arglist = ['--capacity', '2', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_resize_with_wait_no_action(self, mock_await_status,
+                                                mock_await_action, mock_show):
+        error = 'test error'
+        self.mock_client.resize_cluster = mock.Mock(return_value=error)
+        arglist = ['--capacity', '2', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterScaleIn(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -582,6 +699,48 @@
         self.mock_client.scale_in_cluster.assert_called_with('my_cluster',
                                                              '2')
 
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_in_with_wait(self, mock_await_status,
+                                        mock_await_action, mock_show):
+        arglist = ['--count', '2', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_await_status.assert_called_once_with(self.mock_client,
+                                                  'my_cluster')
+        mock_show.assert_called_once()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_in_without_wait(self, mock_await_status,
+                                           mock_await_action, mock_show):
+        arglist = ['--count', '2', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_in_with_wait_no_action(self, mock_await_status,
+                                                  mock_await_action,
+                                                  mock_show):
+        arglist = ['--count', '2', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.scale_in_cluster = mock.Mock(return_value=error)
+
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterScaleOut(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -599,6 +758,48 @@
         self.mock_client.scale_out_cluster.assert_called_with('my_cluster',
                                                               '2')
 
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_out_with_wait(self, mock_await_status,
+                                         mock_await_action, mock_show):
+        arglist = ['--count', '2', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_await_status.assert_called_once_with(self.mock_client,
+                                                  'my_cluster')
+        mock_show.assert_called_once()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_out_without_wait(self, mock_await_status,
+                                            mock_await_action, mock_show):
+        arglist = ['--count', '2', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_scale_out_with_wait_no_action(self, mock_await_status,
+                                                   mock_await_action,
+                                                   mock_show):
+        arglist = ['--count', '2', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.scale_out_cluster = mock.Mock(return_value=error)
+
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterPolicyAttach(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -618,6 +819,32 @@
             'my_policy',
             enabled=True)
 
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_attach_with_wait(self, mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_attach_without_wait(self, mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_attach_with_wait_no_action(self,
+                                                       mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.attach_policy_to_cluster = \
+            mock.Mock(return_value=error)
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+
 
 class TestClusterPolicyDetach(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -636,6 +863,32 @@
             'my_cluster',
             'my_policy')
 
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_dettach_with_wait(self, mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_dettach_without_wait(self, mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_policy_dettach_with_wait_no_action(self,
+                                                        mock_await_action):
+        arglist = ['--policy', 'my_policy', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.detach_policy_from_cluster = \
+            mock.Mock(return_value=error)
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+
 
 class TestClusterNodeList(TestCluster):
     columns = ['id', 'name', 'index', 'status', 'physical_id', 'created_at']
@@ -734,6 +987,40 @@
             'my_cluster',
             ['node1', 'node2'])
 
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_add_with_wait(self, mock_await_action, mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_show.assert_called_once_with(self.mock_client, 'my_cluster')
+
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_add_without_wait(self, mock_await_action, mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_add_with_wait_no_action(self, mock_await_action,
+                                                  mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.add_nodes_to_cluster = mock.Mock(return_value=error)
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterNodeDel(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -771,6 +1058,43 @@
             ['node1', 'node2'],
             destroy_after_deletion=False)
 
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_delete_with_wait(self, mock_await_action, mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_show.assert_called_once_with(self.mock_client, 'my_cluster')
+
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_delete_without_wait(self, mock_await_action,
+                                              mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, "_show_cluster")
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_node_delete_with_wait_no_action(self, mock_await_action,
+                                                     mock_show):
+        arglist = ['--nodes', 'node1', 'my_cluster', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.remove_nodes_from_cluster = \
+            mock.Mock(return_value=error)
+
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterCheck(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -798,6 +1122,48 @@
                                   parsed_args)
         self.assertIn('Cluster not found: cluster1', str(error))
 
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_check_with_wait(self, mock_await_action,
+                                     mock_await_status, mock_list):
+        arglist = ['cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_called_with(self.mock_client,
+                                             self.response['action'])
+        mock_await_status.assert_called_with(self.mock_client, 'cluster1')
+        mock_list.assert_called_with(self.mock_client, {'cluster1'})
+
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_check_without_wait(self, mock_await_action,
+                                        mock_await_status, mock_list):
+        arglist = ['cluster1']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_list.assert_not_called()
+
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_check_with_wait_no_action(self, mock_await_action,
+                                               mock_await_status, mock_list):
+        arglist = ['cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.check_cluster = mock.Mock(return_value=error)
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_list.assert_not_called()
+
 
 class TestClusterRecover(TestCluster):
     response = {"action": "8bb476c3-0f4c-44ee-9f64-c7b0260814de"}
@@ -826,6 +1192,48 @@
                                   parsed_args)
         self.assertIn('Cluster not found: cluster1', str(error))
 
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_recover_with_wait(self, mock_await_action,
+                                       mock_await_status, mock_list):
+        arglist = ['cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_called_with(self.mock_client,
+                                             self.response['action'])
+        mock_await_status.assert_called_with(self.mock_client, 'cluster1')
+        mock_list.assert_called_with(self.mock_client, {'cluster1'})
+
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_recover_without_wait(self, mock_await_action,
+                                          mock_await_status, mock_list):
+        arglist = ['cluster1']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_list.assert_not_called()
+
+    @mock.patch.object(osc_cluster, "_list_cluster_summaries")
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    @mock.patch.object(senlin_utils, 'await_action')
+    def test_cluster_recover_with_wait_no_action(self, mock_await_action,
+                                                 mock_await_status, mock_list):
+        arglist = ['cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.recover_cluster = mock.Mock(return_value=error)
+        self.cmd.take_action(parsed_args)
+
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_list.assert_not_called()
+
 
 class TestClusterOp(TestCluster):
 
@@ -856,6 +1264,48 @@
                                   parsed_args)
         self.assertIn('Cluster not found: cluster1', str(error))
 
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_op_with_wait(self, mock_await_status,
+                                  mock_await_action, mock_show):
+        arglist = ['--operation', 'dance', 'cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_called_once_with(self.mock_client,
+                                                  self.response['action'])
+        mock_await_status.assert_called_once_with(self.mock_client,
+                                                  'cluster1')
+        mock_show.assert_called_once()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_op_without_wait(self, mock_await_status,
+                                     mock_await_action, mock_show):
+        arglist = ['--operation', 'dance', 'cluster1']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
+    @mock.patch.object(osc_cluster, '_show_cluster')
+    @mock.patch.object(senlin_utils, 'await_action')
+    @mock.patch.object(senlin_utils, 'await_cluster_status')
+    def test_cluster_op_with_wait_no_action(self, mock_await_status,
+                                            mock_await_action, mock_show):
+        arglist = ['--operation', 'dance', 'cluster1', '--wait']
+        parsed_args = self.check_parser(self.cmd, arglist, [])
+        error = {'error': 'test-error'}
+        self.mock_client.perform_operation_on_cluster = \
+            mock.Mock(return_value=error)
+
+        self.cmd.take_action(parsed_args)
+        mock_await_action.assert_not_called()
+        mock_await_status.assert_not_called()
+        mock_show.assert_not_called()
+
 
 class TestClusterCollect(TestCluster):
     response = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/senlinclient/v1/cluster.py 
new/python-senlinclient-2.4.0/senlinclient/v1/cluster.py
--- old/python-senlinclient-2.2.1/senlinclient/v1/cluster.py    2021-03-11 
12:03:35.000000000 +0100
+++ new/python-senlinclient-2.4.0/senlinclient/v1/cluster.py    2022-02-25 
17:20:21.000000000 +0100
@@ -210,6 +210,11 @@
             metavar='<cluster-name>',
             help=_('Name of the cluster to create')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster creation to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -230,6 +235,8 @@
         }
 
         cluster = senlin_client.create_cluster(**attrs)
+        if parsed_args.wait:
+            senlin_utils.await_cluster_status(senlin_client, cluster.id)
         return _show_cluster(senlin_client, cluster.id)
 
 
@@ -262,7 +269,6 @@
                    "If false, it will be applied to all existing nodes. "
                    "If true, any newly created nodes will use the new profile,"
                    "but existing nodes will not be changed. Default is False.")
-
         )
         parser.add_argument(
             '--timeout',
@@ -288,6 +294,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to be updated')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster update to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -304,12 +315,23 @@
                 parsed_args.profile_only,
                 strict=True,
             ),
-            'metadata': senlin_utils.format_parameters(parsed_args.metadata),
-            'config': senlin_utils.format_parameters(parsed_args.config),
             'timeout': parsed_args.timeout,
         }
 
+        if parsed_args.config is not None:
+            attrs['config'] = senlin_utils.format_parameters(
+                parsed_args.config)
+        if parsed_args.metadata is not None:
+            attrs['metadata'] = senlin_utils.format_parameters(
+                parsed_args.metadata)
+
         senlin_client.update_cluster(cluster, **attrs)
+        if parsed_args.wait:
+            # PATCH operations do not currently return an action to await.
+            # introducing a delay to allow the cluster to transition state
+            # out of ACTIVE before inspection.
+            time.sleep(1)
+            senlin_utils.await_cluster_status(senlin_client, cluster.id)
         return _show_cluster(senlin_client, cluster.id)
 
 
@@ -336,6 +358,11 @@
             action='store_true',
             help=_('Skip yes/no prompt (assume yes).')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster delete to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -361,17 +388,21 @@
         result = {}
         for cid in parsed_args.cluster:
             try:
-                cluster_delete_action = senlin_client.delete_cluster(
+                action = senlin_client.delete_cluster(
                     cid, False, parsed_args.force_delete)
-                result[cid] = ('OK', cluster_delete_action['id'])
+                result[cid] = ('OK', action['id'])
             except Exception as ex:
                 result[cid] = ('ERROR', str(ex))
 
-        for rid, res in result.items():
-            senlin_utils.print_action_result(rid, res)
+        for cid, a in result.items():
+            senlin_utils.print_action_result(cid, a)
+            if parsed_args.wait:
+                if a[0] == 'OK':
+                    senlin_utils.await_action(senlin_client, a[1])
+                    senlin_utils.await_cluster_delete(senlin_client, cid)
 
 
-class ResizeCluster(command.Command):
+class ResizeCluster(command.ShowOne):
     """Resize a cluster."""
 
     log = logging.getLogger(__name__ + ".ResizeCluster")
@@ -432,6 +463,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster resize to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -446,6 +482,7 @@
         min_size = parsed_args.min_size
         max_size = parsed_args.max_size
         min_step = parsed_args.min_step
+        wait = parsed_args.wait
 
         if sum(v is not None for v in (capacity, adjustment, percentage,
                                        min_size, max_size)) == 0:
@@ -507,13 +544,21 @@
         action_args['strict'] = parsed_args.strict
 
         resp = senlin_client.resize_cluster(parsed_args.cluster, **action_args)
+
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                senlin_utils.await_cluster_status(senlin_client,
+                                                  parsed_args.cluster)
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
 
-class ScaleInCluster(command.Command):
+
+class ScaleInCluster(command.ShowOne):
     """Scale in a cluster by the specified number of nodes."""
 
     log = logging.getLogger(__name__ + ".ScaleInCluster")
@@ -530,6 +575,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster scale-in to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -544,11 +594,18 @@
                 'Unable to scale in cluster: %s') % resp['error']['message'])
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                senlin_utils.await_cluster_status(senlin_client,
+                                                  parsed_args.cluster)
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
 
-class ScaleOutCluster(command.Command):
+
+class ScaleOutCluster(command.ShowOne):
     """Scale out a cluster by the specified number of nodes."""
 
     log = logging.getLogger(__name__ + ".ScaleOutCluster")
@@ -565,6 +622,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster scale-out to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -579,9 +641,16 @@
                 'Unable to scale out cluster: %s') % resp['error']['message'])
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                senlin_utils.await_cluster_status(senlin_client,
+                                                  parsed_args.cluster)
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
+
 
 class ClusterPolicyAttach(command.Command):
     """Attach policy to cluster."""
@@ -608,6 +677,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster policy-attach to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -624,6 +698,8 @@
                                                       **kwargs)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
         else:
             print('Request error: %s' % resp)
 
@@ -646,6 +722,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster policy-detach to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -655,6 +736,8 @@
                                                         parsed_args.policy)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
         else:
             print('Request error: %s' % resp)
 
@@ -737,7 +820,7 @@
         )
 
 
-class ClusterNodeAdd(command.Command):
+class ClusterNodeAdd(command.ShowOne):
     """Add specified nodes to cluster."""
     log = logging.getLogger(__name__ + ".ClusterNodeAdd")
 
@@ -755,6 +838,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster members add to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -765,11 +853,16 @@
                                                   node_ids)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
+
 
-class ClusterNodeDel(command.Command):
+class ClusterNodeDel(command.ShowOne):
     """Delete specified nodes from cluster."""
     log = logging.getLogger(__name__ + ".ClusterNodeDel")
 
@@ -795,6 +888,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster members delete to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -808,11 +906,16 @@
             parsed_args.cluster, node_ids, **kwargs)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
+
 
-class ClusterNodeReplace(command.Command):
+class ClusterNodeReplace(command.ShowOne):
     """Replace the nodes in a cluster with specified nodes."""
     log = logging.getLogger(__name__ + ".ClusterNodeReplace")
 
@@ -833,6 +936,11 @@
             metavar='<cluster>',
             help=_('Name or ID of cluster to operate on')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster members replace to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -847,11 +955,16 @@
                                                       nodepairs)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                return _show_cluster(senlin_client, parsed_args.cluster)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
+
 
-class CheckCluster(command.Command):
+class CheckCluster(command.Lister):
     """Check the cluster(s)."""
     log = logging.getLogger(__name__ + ".CheckCluster")
 
@@ -863,11 +976,19 @@
             nargs='+',
             help=_('ID or name of cluster(s) to operate on.')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster check to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
         self.log.debug("take_action(%s)", parsed_args)
         senlin_client = self.app.client_manager.clustering
+
+        cluster_actions = {}
+
         for cid in parsed_args.cluster:
             try:
                 resp = senlin_client.check_cluster(cid)
@@ -877,11 +998,39 @@
                 print('Cluster check request on cluster %(cid)s is '
                       'accepted by action %(action)s.'
                       % {'cid': cid, 'action': resp['action']})
+                cluster_actions[cid] = resp['action']
             else:
                 print('Request error: %s' % resp)
 
+        # generate the output after all actions have been accepted/rejected
+        if parsed_args.wait and len(cluster_actions) > 0:
+            for cid, action in cluster_actions.items():
+                senlin_utils.await_action(senlin_client, action)
+                senlin_utils.await_cluster_status(senlin_client, cid)
+            return _list_cluster_summaries(senlin_client,
+                                           cluster_actions.keys())
+
+        return '', ''
+
 
-class RecoverCluster(command.Command):
+def _list_cluster_summaries(senlin_client, cluster_ids):
+    clusters = []
+    for cluster_id in cluster_ids:
+        try:
+            cluster = senlin_client.get_cluster(cluster_id)
+        except sdk_exc.ResourceNotFound:
+            raise exc.CommandError(_('Cluster not found: %s') % cluster_id)
+
+        clusters.append(cluster)
+
+    columns = ['ID', 'Name', 'Status', 'Status Reason']
+    formatters = {}
+    props = (utils.get_item_properties(c, columns, formatters=formatters)
+             for c in clusters)
+    return columns, props
+
+
+class RecoverCluster(command.Lister):
     """Recover the cluster(s)."""
     log = logging.getLogger(__name__ + ".RecoverCluster")
 
@@ -893,7 +1042,6 @@
             nargs='+',
             help=_('ID or name of cluster(s) to operate on.')
         )
-
         parser.add_argument(
             '--check',
             metavar='<boolean>',
@@ -901,6 +1049,11 @@
             help=_("Whether the cluster should check it's nodes status before "
                    "doing cluster recover. Default is false")
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster recover to complete')
+        )
 
         return parser
 
@@ -912,6 +1065,7 @@
             'check': strutils.bool_from_string(parsed_args.check, strict=True)
         }
 
+        cluster_actions = {}
         for cid in parsed_args.cluster:
             try:
                 resp = senlin_client.recover_cluster(cid, **params)
@@ -921,9 +1075,20 @@
                 print('Cluster recover request on cluster %(cid)s is '
                       'accepted by action %(action)s.'
                       % {'cid': cid, 'action': resp['action']})
+                cluster_actions[cid] = resp['action']
             else:
                 print('Request error: %s' % resp)
 
+        # generate the output after all actions have been accepted/rejected
+        if parsed_args.wait and len(cluster_actions) > 0:
+            for cid, action in cluster_actions.items():
+                senlin_utils.await_action(senlin_client, action)
+                senlin_utils.await_cluster_status(senlin_client, cid)
+            return _list_cluster_summaries(senlin_client,
+                                           cluster_actions.keys())
+
+        return '', ''
+
 
 class ClusterCollect(command.Lister):
     """Collect attributes across a cluster."""
@@ -966,7 +1131,7 @@
                  for a in attrs))
 
 
-class ClusterOp(command.Lister):
+class ClusterOp(command.ShowOne):
     """Perform an operation on all nodes across a cluster."""
     log = logging.getLogger(__name__ + ".ClusterOp")
 
@@ -991,6 +1156,11 @@
             metavar='<cluster>',
             help=_('ID or name of cluster to operate on.')
         )
+        parser.add_argument(
+            '--wait',
+            action='store_true',
+            help=_('Wait for cluster operation to complete')
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -1009,9 +1179,15 @@
             raise exc.CommandError(_('Cluster not found: %s') % cid)
         if 'action' in resp:
             print('Request accepted by action: %s' % resp['action'])
+            if parsed_args.wait:
+                senlin_utils.await_action(senlin_client, resp['action'])
+                senlin_utils.await_cluster_status(senlin_client, cid)
+                return _show_cluster(senlin_client, cid)
         else:
             print('Request error: %s' % resp)
 
+        return '', ''
+
 
 class ClusterRun(command.Command):
     """Run scripts on cluster."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-senlinclient-2.2.1/tox.ini 
new/python-senlinclient-2.4.0/tox.ini
--- old/python-senlinclient-2.2.1/tox.ini       2021-03-11 12:03:35.000000000 
+0100
+++ new/python-senlinclient-2.4.0/tox.ini       2022-02-25 17:20:21.000000000 
+0100
@@ -1,5 +1,5 @@
 [tox]
-envlist = py38,pep8,releasenotes
+envlist = py3,pep8,releasenotes
 minversion = 3.1.1
 skipsdist = True
 ignore_basepython_conflict = True
@@ -11,7 +11,7 @@
 usedevelop = True
 install_command = pip install {opts} {packages}
 deps =
-       
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+       
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
        -r{toxinidir}/requirements.txt
        -r{toxinidir}/test-requirements.txt
 commands =
@@ -54,13 +54,13 @@
 
 [testenv:docs]
 deps =
-  
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+  
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
   -r{toxinidir}/doc/requirements.txt
 commands = sphinx-build -W -b html doc/source doc/build/html
 
 [testenv:releasenotes]
 deps =
-  
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
+  
-c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
   -r{toxinidir}/doc/requirements.txt
 commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html 
releasenotes/source releasenotes/build/html
 

Reply via email to