From 8fd2ac3ba1c48189d60d73043c4b6868de2633f2 Mon Sep 17 00:00:00 2001 From: Ruby Loo <rloo@yahoo-inc.com> Date: Thu, 2 Apr 2015 18:17:33 +0000 Subject: [PATCH] Consistent and more valid strings for Booleans The set of valid input strings for Boolean values was inconsistent among the subcommands of the Ironic CLI. egs: -for node-set-console-mode: 'true', 'false' -for node-set-maintenance: 'on', 'off', 'true', 'false', 'True', 'False' This change allows the same set of valid values for all the subcommands: '0', '1', 'f', 'false', 'n', 'no', 'off', 'on', 't', 'true', 'y', 'yes'. For invalid strings, we output the subcommand usage along with the argument, invalid string, and valid string choices. At the API level, if NodeManager.set_maintenance() is passed an invalid state (maintenance mode) value, an InvalidAttribute exception is raised. Change-Id: I541695388489cdc640265df8ee6a9999e1442870 Closes-Bug: #1415943 --- ironicclient/common/utils.py | 28 ++++++++++ ironicclient/shell.py | 6 ++- ironicclient/tests/unit/test_utils.py | 19 +++++++ ironicclient/tests/unit/v1/test_node.py | 54 ++++++++++++++++--- ironicclient/tests/unit/v1/test_node_shell.py | 40 ++++++++++++-- ironicclient/v1/node.py | 48 ++++++++++++++--- ironicclient/v1/node_shell.py | 26 ++++----- 7 files changed, 188 insertions(+), 33 deletions(-) diff --git a/ironicclient/common/utils.py b/ironicclient/common/utils.py index 75378b019..4affab94c 100644 --- a/ironicclient/common/utils.py +++ b/ironicclient/common/utils.py @@ -26,6 +26,7 @@ import subprocess import tempfile from oslo_utils import importutils +from oslo_utils import strutils from ironicclient.common.i18n import _ from ironicclient import exc @@ -249,3 +250,30 @@ def check_empty_arg(arg, arg_descriptor): if not arg.strip(): raise exc.CommandError(_('%(arg)s cannot be empty or only have blank' ' spaces') % {'arg': arg_descriptor}) + + +def bool_argument_value(arg_name, bool_str, strict=True, default=False): + """Returns the Boolean represented by bool_str. + + Returns the Boolean value for the argument named arg_name. The value is + represented by the string bool_str. If the string is an invalid Boolean + string: if strict is True, a CommandError exception is raised; otherwise + the default value is returned. + + :param arg_name: The name of the argument + :param bool_str: The string representing a Boolean value + :param strict: Used if the string is invalid. If True, raises an exception. + If False, returns the default value. + :param default: The default value to return if the string is invalid + and not strict + :returns: the Boolean value represented by bool_str or the default value + if bool_str is invalid and strict is False + :raises CommandError: if bool_str is an invalid Boolean string + + """ + try: + val = strutils.bool_from_string(bool_str, strict, default) + except ValueError as e: + raise exc.CommandError(_("argument %(arg)s: %(err)s.") + % {'arg': arg_name, 'err': e}) + return val diff --git a/ironicclient/shell.py b/ironicclient/shell.py index f972c26fe..92bd5179d 100644 --- a/ironicclient/shell.py +++ b/ironicclient/shell.py @@ -253,7 +253,8 @@ class IronicShell(object): parser = self.get_base_parser() self.subcommands = {} - subparsers = parser.add_subparsers(metavar='<subcommand>') + subparsers = parser.add_subparsers(metavar='<subcommand>', + dest='subparser_name') submodule = utils.import_versioned_module(version, 'shell') submodule.enhance_parser(parser, subparsers, self.subcommands) utils.define_commands_from_module(subparsers, self, self.subcommands) @@ -536,6 +537,9 @@ class IronicShell(object): args.func(client, args) except exc.Unauthorized: raise exc.CommandError(_("Invalid OpenStack Identity credentials")) + except exc.CommandError as e: + subcommand_parser = self.subcommands[args.subparser_name] + subcommand_parser.error(e) @cliutils.arg('command', metavar='<subcommand>', nargs='?', help='Display help for <subcommand>') diff --git a/ironicclient/tests/unit/test_utils.py b/ironicclient/tests/unit/test_utils.py index 4ce123cac..f395a87da 100644 --- a/ironicclient/tests/unit/test_utils.py +++ b/ironicclient/tests/unit/test_utils.py @@ -95,6 +95,25 @@ class UtilsTest(test_utils.BaseTestCase): self.assertRaises(exc.CommandError, utils.split_and_deserialize, 'foo:bar') + def test_bool_arg_value(self): + self.assertTrue(utils.bool_argument_value('arg', 'y', strict=True)) + self.assertTrue(utils.bool_argument_value('arg', 'TrUe', strict=True)) + self.assertTrue(utils.bool_argument_value('arg', '1', strict=True)) + self.assertTrue(utils.bool_argument_value('arg', 1, strict=True)) + + self.assertFalse(utils.bool_argument_value('arg', '0', strict=True)) + self.assertFalse(utils.bool_argument_value('arg', 'No', strict=True)) + + self.assertRaises(exc.CommandError, + utils.bool_argument_value, 'arg', 'ee', strict=True) + + self.assertFalse(utils.bool_argument_value('arg', 'ee', strict=False)) + self.assertTrue(utils.bool_argument_value('arg', 'ee', strict=False, + default=True)) + # No check that default is a Boolean... + self.assertEqual('foo', utils.bool_argument_value('arg', 'ee', + strict=False, default='foo')) + class CommonParamsForListTest(test_utils.BaseTestCase): def setUp(self): diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index e30fdfef4..8fd9bfaf7 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -66,6 +66,8 @@ CONSOLE_DATA_ENABLED = {'console_enabled': True, 'console_info': {'test-console': 'test-console-data'}} CONSOLE_DATA_DISABLED = {'console_enabled': False, 'console_info': None} +CONSOLE_ENABLE = 'true' + BOOT_DEVICE = {'boot_device': 'pxe', 'persistent': False} SUPPORTED_BOOT_DEVICE = {'supported_boot_devices': ['pxe']} @@ -232,7 +234,7 @@ fake_responses = { CONSOLE_DATA_ENABLED, ), 'PUT': ( - {'enabled': 'true'}, + {'enabled': CONSOLE_ENABLE}, None, ), }, @@ -422,6 +424,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_unassociated_string(self): + nodes = self.mgr.list(associated="False") + expect = [ + ('GET', '/v1/nodes/?associated=False', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE1['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_maintenance(self): nodes = self.mgr.list(maintenance=True) expect = [ @@ -431,6 +442,15 @@ class NodeManagerTest(testtools.TestCase): self.assertThat(nodes, HasLength(1)) self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_maintenance_string(self): + nodes = self.mgr.list(maintenance="True") + expect = [ + ('GET', '/v1/nodes/?maintenance=True', {}, None), + ] + self.assertEqual(expect, self.api.calls) + self.assertThat(nodes, HasLength(1)) + self.assertEqual(NODE2['uuid'], getattr(nodes[0], 'uuid')) + def test_node_list_no_maintenance(self): nodes = self.mgr.list(maintenance=False) expect = [ @@ -621,6 +641,20 @@ class NodeManagerTest(testtools.TestCase): self.assertEqual(expect, self.api.calls) self.assertEqual(None, maintenance) + def test_node_set_maintenance_bad(self): + self.assertRaises(exc.InvalidAttribute, self.mgr.set_maintenance, + NODE1['uuid'], 'bad') + + def test_node_set_maintenance_bool(self): + maintenance = self.mgr.set_maintenance(NODE1['uuid'], True, + maint_reason='reason') + body = {'reason': 'reason'} + expect = [ + ('PUT', '/v1/nodes/%s/maintenance' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual(None, maintenance) + def test_node_set_power_state(self): power_state = self.mgr.set_power_state(NODE1['uuid'], "on") body = {'target': 'power on'} @@ -705,13 +739,17 @@ class NodeManagerTest(testtools.TestCase): sorted(states.to_dict().keys())) def test_node_set_console_mode(self): - enabled = 'true' - self.mgr.set_console_mode(NODE1['uuid'], enabled) - body = {'enabled': enabled} - expect = [ - ('PUT', '/v1/nodes/%s/states/console' % NODE1['uuid'], {}, body), - ] - self.assertEqual(expect, self.api.calls) + global ENABLE + for enabled in ['true', True, 'False', False]: + self.api.calls = [] + ENABLE = enabled + self.mgr.set_console_mode(NODE1['uuid'], enabled) + body = {'enabled': enabled} + expect = [ + ('PUT', '/v1/nodes/%s/states/console' % NODE1['uuid'], {}, + body), + ] + self.assertEqual(expect, self.api.calls) def test_node_get_console(self): info = self.mgr.get_console(NODE1['uuid']) diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index f54cfe6ba..dbbb01e90 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -222,7 +222,7 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_set_maintenance(client_mock, args) client_mock.node.set_maintenance.assert_called_once_with( - 'node_uuid', 'true', maint_reason='reason') + 'node_uuid', True, maint_reason='reason') def test_do_node_set_maintenance_false(self): client_mock = mock.MagicMock() @@ -234,7 +234,19 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_set_maintenance(client_mock, args) client_mock.node.set_maintenance.assert_called_once_with( - 'node_uuid', 'false', maint_reason=None) + 'node_uuid', False, maint_reason=None) + + def test_do_node_set_maintenance_bad(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + args.maintenance_mode = 'yuck' + # NOTE(jroll) None is the default. <3 mock. + args.reason = None + + self.assertRaises(exceptions.CommandError, + n_shell.do_node_set_maintenance, client_mock, args) + self.assertFalse(client_mock.node.set_maintenance.called) def test_do_node_set_maintenance_false_with_reason_fails(self): client_mock = mock.MagicMock() @@ -256,7 +268,7 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_set_maintenance(client_mock, args) client_mock.node.set_maintenance.assert_called_once_with( - 'node_uuid', 'on', maint_reason='reason') + 'node_uuid', True, maint_reason='reason') def test_do_node_set_maintenance_off(self): client_mock = mock.MagicMock() @@ -268,7 +280,7 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_set_maintenance(client_mock, args) client_mock.node.set_maintenance.assert_called_once_with( - 'node_uuid', 'off', maint_reason=None) + 'node_uuid', False, maint_reason=None) def test_do_node_set_maintenance_off_with_reason_fails(self): client_mock = mock.MagicMock() @@ -403,6 +415,26 @@ class NodeShellTest(utils.BaseTestCase): client_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'provide', configdrive=None) + def test_do_node_set_console_mode(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + args.enabled = 'true' + + n_shell.do_node_set_console_mode(client_mock, args) + client_mock.node.set_console_mode.assert_called_once_with( + 'node_uuid', True) + + def test_do_node_set_console_mode_bad(self): + client_mock = mock.MagicMock() + args = mock.MagicMock() + args.node = 'node_uuid' + args.enabled = 'yuck' + + self.assertRaises(exceptions.CommandError, + n_shell.do_node_set_console_mode, client_mock, args) + self.assertFalse(client_mock.node.set_console_mode.called) + def test_do_node_set_boot_device(self): client_mock = mock.MagicMock() args = mock.MagicMock() diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index f351dea60..5774d98b7 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -14,6 +14,8 @@ import os +from oslo_utils import strutils + from ironicclient.common import base from ironicclient.common.i18n import _ from ironicclient.common import utils @@ -39,11 +41,15 @@ class NodeManager(base.Manager): detail=False, sort_key=None, sort_dir=None): """Retrieve a list of nodes. - :param associated: Optional, boolean whether to return a list of - associated or unassociated nodes. - :param maintenance: Optional, boolean value that indicates whether - to get nodes in maintenance mode ("True"), or not - in maintenance mode ("False"). + :param associated: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return a list of associated (True or "True") or + unassociated (False or "False") nodes. + :param maintenance: Optional. Either a Boolean or a string + representation of a Boolean that indicates whether + to return nodes in maintenance mode (True or + "True"), or not in maintenance mode (False or + "False"). :param marker: Optional, the UUID of a node, eg the last node from a previous result set. Return the next result set. @@ -196,11 +202,32 @@ class NodeManager(base.Manager): _('Unknown HTTP method: %s') % http_method) def set_maintenance(self, node_id, state, maint_reason=None): + """Set the maintenance mode for the node. + + :param node_id: The UUID of the node. + :param state: the maintenance mode; either a Boolean or a string + representation of a Boolean (eg, 'true', 'on', 'false', + 'off'). True to put the node in maintenance mode; False + to take the node out of maintenance mode. + :param maint_reason: Optional string. Reason for putting node + into maintenance mode. + :raises: InvalidAttribute if state is an invalid string (that doesn't + represent a Boolean). + + """ + if isinstance(state, bool): + maintenance_mode = state + else: + try: + maintenance_mode = strutils.bool_from_string(state, True) + except ValueError as e: + raise exc.InvalidAttribute(_("Argument 'state': %(err)s") % + {'err': e}) path = "%s/maintenance" % node_id - if state in ('true', 'on'): + if maintenance_mode: reason = {'reason': maint_reason} return self._update(self._path(path), reason, method='PUT') - if state in ('false', 'off'): + else: return self._delete(self._path(path)) def set_power_state(self, node_id, state): @@ -241,6 +268,13 @@ class NodeManager(base.Manager): return info.to_dict() def set_console_mode(self, node_uuid, enabled): + """Set the console mode for the node. + + :param node_uuid: The UUID of the node. + :param enabled: Either a Boolean or a string representation of a + Boolean. True to enable the console; False to disable. + + """ path = "%s/states/console" % node_uuid target = {'enabled': enabled} return self._update(self._path(path), target, method='PUT') diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index dedefd7f2..622a63642 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -74,12 +74,10 @@ def do_node_show(cc, args): @cliutils.arg( '--maintenance', metavar='<boolean>', - choices=['true', 'True', 'false', 'False'], help="List nodes in maintenance mode: 'true' or 'false'.") @cliutils.arg( '--associated', metavar='<boolean>', - choices=['true', 'True', 'false', 'False'], help="List nodes by instance association: 'true' or 'false'.") @cliutils.arg( '--detail', @@ -91,9 +89,11 @@ def do_node_list(cc, args): """List the nodes which are registered with the Ironic service.""" params = {} if args.associated is not None: - params['associated'] = args.associated + params['associated'] = utils.bool_argument_value("--associated", + args.associated) if args.maintenance is not None: - params['maintenance'] = args.maintenance + params['maintenance'] = utils.bool_argument_value("--maintenance", + args.maintenance) params['detail'] = args.detail if args.detail: @@ -291,20 +291,21 @@ def do_node_port_list(cc, args): @cliutils.arg( 'maintenance_mode', metavar='<maintenance-mode>', - choices=['true', 'True', 'false', 'False', 'on', 'off'], help="'true' or 'false'; 'on' or 'off'.") @cliutils.arg( '--reason', metavar='<reason>', default=None, - help=('Reason for setting maintenance mode to "true" or "on";' - ' not valid when setting to "false" or "off".')) + help=("Reason for setting maintenance mode to 'true' or 'on';" + " not valid when setting to 'false' or 'off'.")) def do_node_set_maintenance(cc, args): """Enable or disable maintenance mode for a node.""" - if args.reason and args.maintenance_mode.lower() in ('false', 'off'): + maintenance_mode = utils.bool_argument_value("<maintenance-mode>", + args.maintenance_mode) + if args.reason and not maintenance_mode: raise exceptions.CommandError(_('Cannot set "reason" when turning off ' 'maintenance mode.')) - cc.node.set_maintenance(args.node, args.maintenance_mode.lower(), + cc.node.set_maintenance(args.node, maintenance_mode, maint_reason=args.reason) @@ -370,12 +371,11 @@ def do_node_get_console(cc, args): @cliutils.arg( 'enabled', metavar='<enabled>', - choices=['true', 'false'], - help="Enable or disable console access for a node. Supported options are: " - "'true' or 'false'.") + help="Enable or disable console access for a node: 'true' or 'false'.") def do_node_set_console_mode(cc, args): """Enable or disable serial console access for a node.""" - cc.node.set_console_mode(args.node, args.enabled) + enable = utils.bool_argument_value("<enabled>", args.enabled) + cc.node.set_console_mode(args.node, enable) @cliutils.arg('node', metavar='<node>', help="Name or UUID of the node.")