From 901bf963c601898b8257a211ea5613025d927e53 Mon Sep 17 00:00:00 2001 From: Duc Truong Date: Mon, 21 Sep 2020 17:45:02 -0700 Subject: [PATCH] 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 --- senlinclient/common/exc.py | 4 + senlinclient/common/utils.py | 81 ++++ senlinclient/tests/unit/test_utils.py | 38 ++ senlinclient/tests/unit/v1/test_cluster.py | 452 ++++++++++++++++++++- senlinclient/v1/cluster.py | 201 ++++++++- 5 files changed, 760 insertions(+), 16 deletions(-) diff --git a/senlinclient/common/exc.py b/senlinclient/common/exc.py index 2d886999..4053594b 100644 --- a/senlinclient/common/exc.py +++ b/senlinclient/common/exc.py @@ -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' diff --git a/senlinclient/common/utils.py b/senlinclient/common/utils.py index 29b227a5..e0eb346c 100644 --- a/senlinclient/common/utils.py +++ b/senlinclient/common/utils.py @@ -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() diff --git a/senlinclient/tests/unit/test_utils.py b/senlinclient/tests/unit/test_utils.py index e362d677..244a2c9d 100644 --- a/senlinclient/tests/unit/test_utils.py +++ b/senlinclient/tests/unit/test_utils.py @@ -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() diff --git a/senlinclient/tests/unit/v1/test_cluster.py b/senlinclient/tests/unit/v1/test_cluster.py index 9b5f0b1a..c8aeebc0 100644 --- a/senlinclient/tests/unit/v1/test_cluster.py +++ b/senlinclient/tests/unit/v1/test_cluster.py @@ -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 = [ diff --git a/senlinclient/v1/cluster.py b/senlinclient/v1/cluster.py index 867a6cda..2fbdf8e1 100644 --- a/senlinclient/v1/cluster.py +++ b/senlinclient/v1/cluster.py @@ -210,6 +210,11 @@ class CreateCluster(command.ShowOne): metavar='', 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='', 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='', 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='', 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='', 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='', 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='', 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='', 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='', 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='', 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='', @@ -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='', 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."""