From ac205b9f0c2ff95e99f06a8cc34d9fefde624daa Mon Sep 17 00:00:00 2001 From: dixiaoli Date: Fri, 19 Feb 2016 16:26:26 +0800 Subject: [PATCH] Add OpenstackClient plugin for cluster node list This change implements the "openstack cluster node list" command Based on the existing senlin command: senlin node-list Change-Id: Ifb2c492cca6435b398f111cd2be5b6b88d97a115 Blueprint: senlin-support-python-openstackclient --- senlinclient/osc/v1/node.py | 111 +++++++++++++++ senlinclient/tests/unit/osc/v1/test_node.py | 142 ++++++++++++++++++++ setup.cfg | 1 + 3 files changed, 254 insertions(+) create mode 100644 senlinclient/osc/v1/node.py create mode 100644 senlinclient/tests/unit/osc/v1/test_node.py diff --git a/senlinclient/osc/v1/node.py b/senlinclient/osc/v1/node.py new file mode 100644 index 00000000..a7072bf7 --- /dev/null +++ b/senlinclient/osc/v1/node.py @@ -0,0 +1,111 @@ +# 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 node 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 ListNode(lister.Lister): + """Show list of nodes.""" + + log = logging.getLogger(__name__ + ".ListNode") + + def get_parser(self, prog_name): + parser = super(ListNode, self).get_parser(prog_name) + parser.add_argument( + '--cluster', + metavar='', + help=_('ID or name of cluster from which nodes are to be listed') + ) + parser.add_argument( + '--filters', + metavar='', + help=_("Filter parameters to apply on returned nodes. " + "This can be specified multiple times, or once with " + "parameters separated by a semicolon. The valid filter" + " keys are: ['status','name']"), + 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:'['index', 'name', 'status', 'init_at', " + "'created_at', 'updated_at']'") + ) + parser.add_argument( + '--limit', + metavar='', + help=_('Limit the number of nodes returned') + ) + parser.add_argument( + '--marker', + metavar='', + help=_('Only return nodes that appear after the given node ID') + ) + parser.add_argument( + '--global-project', + default=False, action="store_true", + help=_('Indicate that this node list should include nodes from ' + 'all projects. This option is subject to access policy ' + 'checking. Default is False') + ) + 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', 'index', 'status', 'cluster_id', + 'physical_id', 'profile_name', 'created_at', 'updated_at'] + queries = { + 'cluster_id': parsed_args.cluster, + 'sort': parsed_args.sort, + 'limit': parsed_args.limit, + 'marker': parsed_args.marker, + 'global_project': parsed_args.global_project, + } + + if parsed_args.filters: + queries.update(senlin_utils.format_parameters(parsed_args.filters)) + + nodes = senlin_client.nodes(**queries) + + if not parsed_args.full_id: + formatters = { + 'id': lambda x: x[:8], + 'cluster_id': lambda x: x[:8] if x else '', + 'physical_id': lambda x: x[:8] if x else '' + } + else: + formatters = {} + + return ( + columns, + (utils.get_item_properties(n, columns, formatters=formatters) + for n in nodes) + ) diff --git a/senlinclient/tests/unit/osc/v1/test_node.py b/senlinclient/tests/unit/osc/v1/test_node.py new file mode 100644 index 00000000..4174c008 --- /dev/null +++ b/senlinclient/tests/unit/osc/v1/test_node.py @@ -0,0 +1,142 @@ +# 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 node as sdk_node +from openstack import exceptions as sdk_exc + +from senlinclient.osc.v1 import node as osc_node +from senlinclient.tests.unit.osc.v1 import fakes + + +class TestNode(fakes.TestClusteringv1): + def setUp(self): + super(TestNode, self).setUp() + self.mock_client = self.app.client_manager.clustering + + +class TestNodeList(TestNode): + + columns = ['id', 'name', 'index', 'status', 'cluster_id', + 'physical_id', 'profile_name', 'created_at', 'updated_at'] + + response = {"nodes": [ + { + "cluster_id": None, + "created_at": "2015-02-27T04:39:21", + "data": {}, + "details": {}, + "domain": None, + "id": "573aa1ba-bf45-49fd-907d-6b5d6e6adfd3", + "index": -1, + "init_at": "2015-02-27T04:39:18", + "metadata": {}, + "name": "node00a", + "physical_id": "cc028275-d078-4729-bf3e-154b7359814b", + "profile_id": "edc63d0a-2ca4-48fa-9854-27926da76a4a", + "profile_name": "mystack", + "project": "6e18cc2bdbeb48a5b3cad2dc499f6804", + "role": None, + "status": "ACTIVE", + "status_reason": "Creation succeeded", + "updated_at": None, + "user": "5e5bf8027826429c96af157f68dc9072" + } + ]} + + defaults = { + 'cluster_id': None, + 'global_project': False, + 'marker': None, + 'limit': None, + 'sort': None, + } + + def setUp(self): + super(TestNodeList, self).setUp() + self.cmd = osc_node.ListNode(self.app, None) + self.mock_client.nodes = mock.Mock( + return_value=sdk_node.Node(None, {})) + + def test_node_list_defaults(self): + arglist = [] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.nodes.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_node_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.nodes.assert_called_with(**self.defaults) + self.assertEqual(self.columns, columns) + + def test_node_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.nodes.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_node_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.nodes.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_node_list_sort_invalid_key(self): + self.mock_client.nodes = 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.nodes.side_effect = sdk_exc.HttpException() + self.assertRaises(sdk_exc.HttpException, + self.cmd.take_action, parsed_args) + + def test_node_list_sort_invalid_direction(self): + self.mock_client.nodes = 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.nodes.side_effect = sdk_exc.HttpException() + self.assertRaises(sdk_exc.HttpException, + self.cmd.take_action, parsed_args) + + def test_node_list_filter(self): + kwargs = copy.deepcopy(self.defaults) + kwargs['name'] = 'my_node' + arglist = ['--filter', 'name=my_node'] + parsed_args = self.check_parser(self.cmd, arglist, []) + columns, data = self.cmd.take_action(parsed_args) + self.mock_client.nodes.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) + + def test_node_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.nodes.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) diff --git a/setup.cfg b/setup.cfg index 196e5a0f..95117ac9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ openstack.cli.extension = clustering = senlinclient.osc.plugin openstack.clustering.v1 = + cluster_node_list = senlinclient.osc.v1.node:ListNode cluster_profile_create = senlinclient.osc.v1.profile:CreateProfile cluster_profile_delete = senlinclient.osc.v1.profile:DeleteProfile cluster_profile_list = senlinclient.osc.v1.profile:ListProfile