Adds --wait argument for cluster CLI interactions

Add option to wait for CLI command to complete for the following cluster
commands: create, update, delete, resize, scale in, scale out, policy
attach, policy detach, node add, node delete, node replace, cluster
check, recover and cluster operation.

Change-Id: I5663ca7286c55da4491644f979d5ab44f0cfc915
This commit is contained in:
Duc Truong
2020-09-21 17:45:02 -07:00
parent aeb56bd25c
commit 901bf963c6
5 changed files with 760 additions and 16 deletions

View File

@@ -38,6 +38,10 @@ class FileFormatError(BaseException):
"""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'

View File

@@ -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 @@ def process_stack_spec(spec):
}
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()

View File

@@ -14,6 +14,7 @@ from heatclient.common import template_utils
from unittest import mock
import testtools
import time
from senlinclient.common import exc
from senlinclient.common.i18n import _
@@ -97,3 +98,40 @@ class UtilTest(testtools.TestCase):
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()

View File

@@ -18,6 +18,7 @@ from unittest import mock
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 @@ class TestClusterCreate(TestCluster):
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 @@ class TestClusterCreate(TestCluster):
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 @@ class TestClusterUpdate(TestCluster):
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 @@ class TestClusterDelete(TestCluster):
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 @@ class TestClusterResize(TestCluster):
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 @@ class TestClusterScaleIn(TestCluster):
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 @@ class TestClusterScaleOut(TestCluster):
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 @@ class TestClusterPolicyAttach(TestCluster):
'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 @@ class TestClusterPolicyDetach(TestCluster):
'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 @@ class TestClusterNodeAdd(TestCluster):
'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 @@ class TestClusterNodeDel(TestCluster):
['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 @@ class TestClusterCheck(TestCluster):
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 @@ class TestClusterRecover(TestCluster):
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 @@ class TestClusterOp(TestCluster):
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 = [

View File

@@ -210,6 +210,11 @@ class CreateCluster(command.ShowOne):
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 @@ class CreateCluster(command.ShowOne):
}
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 @@ class UpdateCluster(command.ShowOne):
"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 @@ class UpdateCluster(command.ShowOne):
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):
@@ -310,6 +321,12 @@ class UpdateCluster(command.ShowOne):
}
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 +353,11 @@ class DeleteCluster(command.Command):
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 +383,21 @@ class DeleteCluster(command.Command):
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 +458,11 @@ class ResizeCluster(command.Command):
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 +477,7 @@ class ResizeCluster(command.Command):
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 +539,21 @@ class ResizeCluster(command.Command):
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 +570,11 @@ class ScaleInCluster(command.Command):
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 +589,18 @@ class ScaleInCluster(command.Command):
'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 +617,11 @@ class ScaleOutCluster(command.Command):
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 +636,16 @@ class ScaleOutCluster(command.Command):
'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 +672,11 @@ class ClusterPolicyAttach(command.Command):
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 +693,8 @@ class ClusterPolicyAttach(command.Command):
**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 +717,11 @@ class ClusterPolicyDetach(command.Command):
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 +731,8 @@ class ClusterPolicyDetach(command.Command):
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 +815,7 @@ class ClusterNodeList(command.Lister):
)
class ClusterNodeAdd(command.Command):
class ClusterNodeAdd(command.ShowOne):
"""Add specified nodes to cluster."""
log = logging.getLogger(__name__ + ".ClusterNodeAdd")
@@ -755,6 +833,11 @@ class ClusterNodeAdd(command.Command):
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 +848,16 @@ class ClusterNodeAdd(command.Command):
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 +883,11 @@ class ClusterNodeDel(command.Command):
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 +901,16 @@ class ClusterNodeDel(command.Command):
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 +931,11 @@ class ClusterNodeReplace(command.Command):
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 +950,16 @@ class ClusterNodeReplace(command.Command):
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 +971,19 @@ class CheckCluster(command.Command):
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 +993,39 @@ class CheckCluster(command.Command):
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())
class RecoverCluster(command.Command):
return '', ''
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 +1037,6 @@ class RecoverCluster(command.Command):
nargs='+',
help=_('ID or name of cluster(s) to operate on.')
)
parser.add_argument(
'--check',
metavar='<boolean>',
@@ -901,6 +1044,11 @@ class RecoverCluster(command.Command):
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 +1060,7 @@ class RecoverCluster(command.Command):
'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 +1070,20 @@ class RecoverCluster(command.Command):
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 +1126,7 @@ class ClusterCollect(command.Lister):
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 +1151,11 @@ class ClusterOp(command.Lister):
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 +1174,15 @@ class ClusterOp(command.Lister):
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."""