diff --git a/senlinclient/tests/unit/v1/test_shell.py b/senlinclient/tests/unit/v1/test_shell.py index cd69874..4889a1a 100644 --- a/senlinclient/tests/unit/v1/test_shell.py +++ b/senlinclient/tests/unit/v1/test_shell.py @@ -1441,6 +1441,40 @@ class ShellTest(testtools.TestCase): mock_print.assert_called_once_with(attrs, fields, formatters=formatters) + def test_do_cluster_op(self): + service = mock.Mock() + args = { + 'id': 'cluster1', + 'operation': 'dance', + 'params': ['style=tango'] + } + args = self._make_args(args) + attrs = { + 'style': 'tango' + } + service.perform_operation_on_cluster = mock.Mock() + + sh.do_cluster_op(service, args) + + service.perform_operation_on_cluster.assert_called_once_with( + 'cluster1', 'dance', **attrs) + + def test_do_cluster_op_not_found(self): + service = mock.Mock() + ex = exc.HTTPNotFound + service.perform_operation_on_cluster.side_effect = ex + args = { + 'id': 'cluster1', + 'operation': 'swim', + 'params': [] + } + args = self._make_args(args) + + ex = self.assertRaises(exc.CommandError, + sh.do_cluster_op, service, args) + msg = _('Cluster "cluster1" is not found') + self.assertEqual(msg, six.text_type(ex)) + @mock.patch.object(utils, 'print_list') def test_do_node_list(self, mock_print): service = mock.Mock() @@ -1529,6 +1563,31 @@ class ShellTest(testtools.TestCase): sh.do_node_show(service, args) mock_show.assert_called_once_with(service, 'node1', False) + @mock.patch.object(sh, '_show_node') + def test_do_node_update(self, mock_show): + service = mock.Mock() + args = { + 'id': 'node_id', + 'name': 'node1', + 'role': 'master', + 'profile': 'profile1', + 'metadata': ['user=demo'], + } + args = self._make_args(args) + attrs = { + 'name': 'node1', + 'role': 'master', + 'profile_id': 'profile1', + 'metadata': {'user': 'demo'}, + } + node = mock.Mock() + node.id = 'node_id' + service.get_node.return_value = node + sh.do_node_update(service, args) + service.get_node.assert_called_once_with('node_id') + service.update_node.assert_called_once_with(node, **attrs) + mock_show.assert_called_once_with(service, 'node_id') + def test_do_node_delete(self): service = mock.Mock() args = self._make_args({'id': ['node1']}) @@ -1589,30 +1648,39 @@ class ShellTest(testtools.TestCase): msg = _('Failed to recover some of the specified nodes.') self.assertEqual(msg, six.text_type(ex)) - @mock.patch.object(sh, '_show_node') - def test_do_node_update(self, mock_show): + def test_do_node_op(self): service = mock.Mock() args = { - 'id': 'node_id', - 'name': 'node1', - 'role': 'master', - 'profile': 'profile1', - 'metadata': ['user=demo'], + 'id': 'node1', + 'operation': 'dance', + 'params': ['style=tango'] } args = self._make_args(args) attrs = { - 'name': 'node1', - 'role': 'master', - 'profile_id': 'profile1', - 'metadata': {'user': 'demo'}, + 'style': 'tango' } - node = mock.Mock() - node.id = 'node_id' - service.get_node.return_value = node - sh.do_node_update(service, args) - service.get_node.assert_called_once_with('node_id') - service.update_node.assert_called_once_with(node, **attrs) - mock_show.assert_called_once_with(service, 'node_id') + service.perform_operation_on_node = mock.Mock() + + sh.do_node_op(service, args) + + service.perform_operation_on_node.assert_called_once_with( + 'node1', 'dance', **attrs) + + def test_do_node_op_not_found(self): + service = mock.Mock() + ex = exc.HTTPNotFound + service.perform_operation_on_node.side_effect = ex + args = { + 'id': 'node1', + 'operation': 'swim', + 'params': [] + } + args = self._make_args(args) + + ex = self.assertRaises(exc.CommandError, + sh.do_node_op, service, args) + msg = _('Node "node1" is not found') + self.assertEqual(msg, six.text_type(ex)) @mock.patch.object(utils, 'print_list') def test_do_event_list(self, mock_print): diff --git a/senlinclient/v1/client.py b/senlinclient/v1/client.py index 1ad99eb..b9cd55e 100644 --- a/senlinclient/v1/client.py +++ b/senlinclient/v1/client.py @@ -320,6 +320,16 @@ class Client(object): """ return self.service.recover_cluster(cluster, **params) + def perform_operation_on_cluster(self, cluster, operation, **params): + """Perform an operation on a cluster. + + Doc link: + https://developer.openstack.org/api-ref/clustering/ + #perform-an-operation-on-a-cluster + """ + return self.service.perform_operation_on_cluster(cluster, operation, + **params) + def nodes(self, **queries): """List nodes @@ -377,6 +387,16 @@ class Client(object): """ return self.service.recover_node(node, **params) + def perform_operation_on_node(self, node, operation, **params): + """Perform an operation on a node. + + Doc link: + https://developer.openstack.org/api-ref/clustering/ + #perform-an-operation-on-a-node + """ + return self.service.perform_operation_on_node(node, operation, + **params) + def receivers(self, **queries): """List receivers diff --git a/senlinclient/v1/shell.py b/senlinclient/v1/shell.py index c99af7c..edc35e8 100644 --- a/senlinclient/v1/shell.py +++ b/senlinclient/v1/shell.py @@ -1177,6 +1177,28 @@ def do_cluster_recover(service, args): 'action %(action)s.' % {'cid': cid, 'action': resp['action']}) +@utils.arg('-p', '--params', metavar='<"KEY1=VALUE1;KEY2=VALUE2...">', + help=_("Parameter name and values for the operation specified. " + "This can be specified multiple times, or once with " + "key-value pairs separated by a semicolon."), + action='append') +@utils.arg('-o', '--operation', metavar='', + help=_("Name of an operation to be executed on the cluster.")) +@utils.arg('id', metavar='', + help=_('ID or name of a cluster.')) +def do_cluster_op(service, args): + """Run an operation on a cluster.""" + show_deprecated('senlin cluster-op', 'openstack cluster op') + params = utils.format_parameters(args.params) + + try: + service.perform_operation_on_cluster(args.id, args.operation, + **params) + except exc.HTTPNotFound: + raise exc.CommandError(_('Cluster "%s" is not found') % args.id) + print('Request accepted') + + # NODES @@ -1394,6 +1416,31 @@ def do_node_recover(service, args): print('Request accepted') +@utils.arg('-p', '--params', metavar='<"KEY1=VALUE1;KEY2=VALUE2...">', + help=_("Parameter name and values for the operation specified. " + "This can be specified multiple times, or once with " + "key-value pairs separated by a semicolon."), + action='append') +@utils.arg('-o', '--operation', metavar='', + help=_("Name of an operation to be executed on the node")) +@utils.arg('id', metavar='', + help=_('ID or name of a node.')) +def do_node_op(service, args): + """Run an operation on a node.""" + show_deprecated('senlin node-op', 'openstack cluster node op') + if args.params: + params = utils.format_parameters(args.params) + else: + params = {} + + try: + service.perform_operation_on_node(args.id, args.operation, + **params) + except exc.HTTPNotFound: + raise exc.CommandError(_('Node "%s" is not found') % args.id) + print('Request accepted') + + # RECEIVERS diff --git a/tox.ini b/tox.ini index f510c8e..d6c61b8 100644 --- a/tox.ini +++ b/tox.ini @@ -42,7 +42,7 @@ commands= commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] -ignore = D100,D101,D102,D103,D104,D105,D200,D201,D202,D204,D205,D300,D301,D400,D401 +ignore = D100,D101,D102,D103,D104,D105,D200,D201,D202,D204,D205,D300,D301,D400,D401,I100,I201 show-source = True enable-extensions = H203,H106 exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build