From e86521970467117a0d3d131738e169c45c664ca2 Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Sat, 27 Feb 2016 16:57:25 +0800 Subject: [PATCH] Add OSC plugin for openstack cluster action list This change implements the "openstack cluster action list" command Based on the existing senlin command: senlin action-list Change-Id: I4ba6b29b9313489c8fdc97ca6f03ba9af8333731 Blueprint: senlin-support-python-openstackclient --- senlinclient/osc/v1/action.py | 105 ++++++++++++ senlinclient/tests/unit/osc/v1/test_action.py | 157 ++++++++++++++++++ setup.cfg | 1 + 3 files changed, 263 insertions(+) create mode 100644 senlinclient/osc/v1/action.py create mode 100644 senlinclient/tests/unit/osc/v1/test_action.py diff --git a/senlinclient/osc/v1/action.py b/senlinclient/osc/v1/action.py new file mode 100644 index 0000000..3e07cd2 --- /dev/null +++ b/senlinclient/osc/v1/action.py @@ -0,0 +1,105 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Clustering v1 action implementations""" + +import logging + +from cliff import lister +from openstackclient.common import utils + +from senlinclient.common.i18n import _ +from senlinclient.common import utils as senlin_utils + + +class ListAction(lister.Lister): + """List actions.""" + + log = logging.getLogger(__name__ + ".ListAction") + + def get_parser(self, prog_name): + parser = super(ListAction, self).get_parser(prog_name) + parser.add_argument( + '--filters', + metavar='', + help=_("Filter parameters to apply on returned actions. " + "This can be specified multiple times, or once with " + "parameters separated by a semicolon. The valid filter " + "keys are: ['name', 'target', 'action', 'status']"), + action='append' + ) + parser.add_argument( + '--sort', + metavar='', + help=_("Sorting option which is a string containing a list of " + "keys separated by commas. Each key can be optionally " + "appended by a sort direction (:asc or :desc). The valid " + "sort keys are: ['name', 'target', 'action', 'created_at'," + " 'status']") + ) + parser.add_argument( + '--limit', + metavar='', + help=_('Limit the number of actions returned') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Only return actions that appear after the given node ID') + ) + parser.add_argument( + '--full-id', + default=False, + action="store_true", + help=_('Print full IDs in list') + ) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)", parsed_args) + + senlin_client = self.app.client_manager.clustering + + columns = ['id', 'name', 'action', 'status', 'target', 'depends_on', + 'depended_by', 'created_at'] + + queries = { + 'sort': parsed_args.sort, + 'limit': parsed_args.limit, + 'marker': parsed_args.marker, + } + + if parsed_args.filters: + queries.update(senlin_utils.format_parameters(parsed_args.filters)) + + actions = senlin_client.actions(**queries) + + formatters = {} + if parsed_args.full_id: + f_depon = lambda x: '\n'.join(a for a in x) + f_depby = lambda x: '\n'.join(a for a in x) + + formatters['depends_on'] = f_depon + formatters['depended_by'] = f_depby + else: + formatters['id'] = lambda x: x[:8] + formatters['target'] = lambda x: x[:8] + f_depon = lambda x: '\n'.join(a[:8] for a in x) + f_depby = lambda x: '\n'.join(a[:8] for a in x) + formatters['depends_on'] = f_depon + formatters['depended_by'] = f_depby + + return ( + columns, + (utils.get_item_properties(a, columns, formatters=formatters) + for a in actions) + ) diff --git a/senlinclient/tests/unit/osc/v1/test_action.py b/senlinclient/tests/unit/osc/v1/test_action.py new file mode 100644 index 0000000..5b43432 --- /dev/null +++ b/senlinclient/tests/unit/osc/v1/test_action.py @@ -0,0 +1,157 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import mock + +from openstack.cluster.v1 import action as sdk_action +from openstack import exceptions as sdk_exc + +from senlinclient.osc.v1 import action as osc_action +from senlinclient.tests.unit.osc.v1 import fakes + + +class TestAction(fakes.TestClusteringv1): + def setUp(self): + super(TestAction, self).setUp() + self.mock_client = self.app.client_manager.clustering + + +class TestActionList(TestAction): + columns = ['id', 'name', 'action', 'status', 'target', 'depends_on', + 'depended_by', 'created_at'] + response = {"actions": [ + { + "action": "NODE_CREATE", + "cause": "RPC Request", + "created_at": "2015-12-04T04:54:41", + "depended_by": [], + "depends_on": [], + "end_time": 1425550000.0, + "id": "2366d440-c73e-4961-9254-6d1c3af7c167", + "inputs": {}, + "interval": -1, + "name": "node_create_0df0931b", + "outputs": {}, + "owner": 'null', + "start_time": 1425550000.0, + "status": "SUCCEEDED", + "status_reason": "Action completed successfully.", + "target": "0df0931b-e251-4f2e-8719-4ebfda3627ba", + "timeout": 3600, + "updated_at": 'null' + }, + { + "action": "NODE_DELETE", + "cause": "RPC Request", + "created_at": "2015-11-04T05:21:41", + "depended_by": [], + "depends_on": [], + "end_time": 1425550000.0, + "id": "edce3528-864f-41fb-8759-f4707925cc09", + "inputs": {}, + "interval": -1, + "name": "node_delete_f0de9b9c", + "outputs": {}, + "owner": 'null', + "start_time": 1425550000.0, + "status": "SUCCEEDED", + "status_reason": "Action completed successfully.", + "target": "f0de9b9c-6d48-4a46-af21-2ca8607777fe", + "timeout": 3600, + "updated_at": 'null' + } + ]} + + defaults = { + 'marker': None, + 'limit': None, + 'sort': None, + } + + def setUp(self): + super(TestActionList, self).setUp() + self.cmd = osc_action.ListAction(self.app, None) + self.mock_client.actions = mock.Mock( + return_value=sdk_action.Action(None, {})) + + def test_action_list_defaults(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_action_list_full_id(self): + arglist = ['--full-id'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_action_list_limit(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['limit'] = '3' + arglist = ['--limit', '3'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_action_list_sort(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['sort'] = 'name:asc' + arglist = ['--sort', 'name:asc'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_action_list_sort_invalid_key(self): + self.mock_client.actions = mock.Mock( + return_value=self.response) + kwargs = copy.deepcopy(self.defaults) + kwargs['sort'] = 'bad_key' + arglist = ['--sort', 'bad_key'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.actions.side_effect = sdk_exc.HttpException() + self.assertRaises(sdk_exc.HttpException, + self.cmd.take_action, parsed_args) + + def test_action_list_sort_invalid_direction(self): + self.mock_client.actions = mock.Mock( + return_value=self.response) + kwargs = copy.deepcopy(self.defaults) + kwargs['sort'] = 'name:bad_direction' + arglist = ['--sort', 'name:bad_direction'] + parsed_args = self.check_parser(self.cmd, arglist, []) + self.mock_client.actions.side_effect = sdk_exc.HttpException() + self.assertRaises(sdk_exc.HttpException, + self.cmd.take_action, parsed_args) + + def test_action_list_filter(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['name'] = 'my_action' + arglist = ['--filter', 'name=my_action'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_action_list_marker(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['marker'] = 'a9448bf6' + arglist = ['--marker', 'a9448bf6'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.actions.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) diff --git a/setup.cfg b/setup.cfg index c1ff848..0fd44f9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ openstack.cli.extension = clustering = senlinclient.osc.plugin openstack.clustering.v1 = + cluster_action_list = senlinclient.osc.v1.action:ListAction cluster_policy_binding_list = senlinclient.osc.v1.cluster_policy:ClusterPolicyList cluster_policy_binding_show = senlinclient.osc.v1.cluster_policy:ClusterPolicyShow cluster_policy_binding_update = senlinclient.osc.v1.cluster_policy:ClusterPolicyUpdate