From 249215e3d5607830ed6e6c7fe6f2dda33782f211 Mon Sep 17 00:00:00 2001 From: Tang Chen Date: Sat, 21 Nov 2015 11:19:08 +0800 Subject: [PATCH] Add soft reboot/poweroff power states. Soft reboot/poweroff is now being implemented in Ironic. And the Ironic driver in Nova should support this new feature. This patch adds soft reboot and soft poweroff states. Co-Authored-By: Naohiro Tamura Partial-Bug: #1526226 Change-Id: Icd2859784f3df85c08162c14464bce58067aab1d Depends-On: I1c9bbd1f11f6a8565607c874b3c99aa10eeb62a5 --- ironicclient/tests/functional/base.py | 5 +- ironicclient/tests/unit/v1/test_node.py | 53 +++++++++++++++++-- ironicclient/tests/unit/v1/test_node_shell.py | 50 +++++++++++++++-- ironicclient/v1/node.py | 37 +++++++++++-- ironicclient/v1/node_shell.py | 21 +++++++- ...soft-reboot-poweroff-e33d078a05db3894.yaml | 4 ++ 6 files changed, 155 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/soft-reboot-poweroff-e33d078a05db3894.yaml diff --git a/ironicclient/tests/functional/base.py b/ironicclient/tests/functional/base.py index f4dd7b001..1110787f0 100644 --- a/ironicclient/tests/functional/base.py +++ b/ironicclient/tests/functional/base.py @@ -268,9 +268,10 @@ class FunctionalTestBase(base.ClientTestBase): 'node-set-maintenance', params='{0} {1} {2}'.format(node_id, maintenance_mode, params)) - def set_node_power_state(self, node_id, power_state): + def set_node_power_state(self, node_id, power_state, params=''): self.ironic('node-set-power-state', - params='{0} {1}'.format(node_id, power_state)) + params='{0} {1} {2}' + .format(node_id, power_state, params)) def set_node_provision_state(self, node_id, provision_state, params=''): self.ironic('node-set-provision-state', diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 5f4583d97..412335bee 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -59,8 +59,8 @@ PORTGROUP = {'uuid': '11111111-2222-3333-4444-555555555555', 'address': 'AA:BB:CC:DD:EE:FF', 'extra': {}} -POWER_STATE = {'power_state': 'power off', - 'target_power_state': 'power on'} +POWER_STATE = {'power_state': 'power on', + 'target_power_state': 'power off'} DRIVER_IFACES = {'deploy': {'result': True}, 'power': {'result': False, 'reason': 'Invalid IPMI username'}, @@ -891,13 +891,56 @@ class NodeManagerTest(testtools.TestCase): self.assertIsNone(maintenance) def test_node_set_power_state(self): - power_state = self.mgr.set_power_state(NODE1['uuid'], "on") - body = {'target': 'power on'} + power_state = self.mgr.set_power_state(NODE1['uuid'], "off") + body = {'target': 'power off'} expect = [ ('PUT', '/v1/nodes/%s/states/power' % NODE1['uuid'], {}, body), ] self.assertEqual(expect, self.api.calls) - self.assertEqual('power on', power_state.target_power_state) + self.assertEqual('power off', power_state.target_power_state) + + def test_node_set_power_timeout(self): + power_state = self.mgr.set_power_state(NODE1['uuid'], "off", timeout=2) + body = {'target': 'power off', 'timeout': 2} + expect = [ + ('PUT', '/v1/nodes/%s/states/power' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('power off', power_state.target_power_state) + + def test_node_set_power_timeout_str(self): + power_state = self.mgr.set_power_state(NODE1['uuid'], "off", + timeout="2") + body = {'target': 'power off', 'timeout': 2} + expect = [ + ('PUT', '/v1/nodes/%s/states/power' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('power off', power_state.target_power_state) + + def test_node_set_power_state_soft(self): + power_state = self.mgr.set_power_state(NODE1['uuid'], "off", soft=True) + body = {'target': 'soft power off'} + expect = [ + ('PUT', '/v1/nodes/%s/states/power' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + self.assertEqual('power off', power_state.target_power_state) + + def test_node_set_power_state_soft_fail(self): + self.assertRaises(ValueError, + self.mgr.set_power_state, + NODE1['uuid'], 'on', soft=True) + + def test_node_set_power_state_on_timeout_fail(self): + self.assertRaises(ValueError, + self.mgr.set_power_state, + NODE1['uuid'], 'off', soft=False, timeout=0) + + def test_node_set_power_state_on_timeout_type_error(self): + self.assertRaises(ValueError, + self.mgr.set_power_state, + NODE1['uuid'], 'off', soft=False, timeout='a') def test_set_target_raid_config(self): self.mgr.set_target_raid_config( diff --git a/ironicclient/tests/unit/v1/test_node_shell.py b/ironicclient/tests/unit/v1/test_node_shell.py index 584b22f8f..186f73208 100644 --- a/ironicclient/tests/unit/v1/test_node_shell.py +++ b/ironicclient/tests/unit/v1/test_node_shell.py @@ -376,15 +376,26 @@ class NodeShellTest(utils.BaseTestCase): n_shell.do_node_set_maintenance, client_mock, args) - def _do_node_set_power_state_helper(self, power_state): + def _do_node_set_power_state_helper(self, power_state, + soft=False, timeout=None, error=False): client_mock = mock.MagicMock() args = mock.MagicMock() args.node = 'node_uuid' args.power_state = power_state + args.soft = soft + args.power_timeout = timeout - n_shell.do_node_set_power_state(client_mock, args) - client_mock.node.set_power_state.assert_called_once_with('node_uuid', - power_state) + if error: + client_mock.node = mock.MagicMock() + client_mock.node.set_power_state = mock.MagicMock() + client_mock.node.set_power_state.side_effect = ValueError("fake") + self.assertRaises(exc.CommandError, + n_shell.do_node_set_power_state, + client_mock, args) + else: + n_shell.do_node_set_power_state(client_mock, args) + client_mock.node.set_power_state.assert_called_once_with( + 'node_uuid', power_state, soft, timeout=timeout) def test_do_node_set_power_state_on(self): self._do_node_set_power_state_helper('on') @@ -395,6 +406,37 @@ class NodeShellTest(utils.BaseTestCase): def test_do_node_set_power_state_reboot(self): self._do_node_set_power_state_helper('reboot') + def test_do_node_set_power_state_on_timeout(self): + self._do_node_set_power_state_helper('on', timeout=10) + + def test_do_node_set_power_state_on_timeout_fail(self): + self._do_node_set_power_state_helper('on', timeout=0, error=True) + + def test_do_node_set_power_state_off_timeout(self): + self._do_node_set_power_state_helper('off', timeout=10) + + def test_do_node_set_power_state_reboot_timeout(self): + self._do_node_set_power_state_helper('reboot', timeout=10) + + def test_do_node_set_power_state_soft_on_fail(self): + self._do_node_set_power_state_helper('on', soft=True, error=True) + + def test_do_node_set_power_state_soft_off(self): + self._do_node_set_power_state_helper('off', soft=True) + + def test_do_node_set_power_state_soft_reboot(self): + self._do_node_set_power_state_helper('reboot', soft=True) + + def test_do_node_set_power_state_soft_on_timeout_fail(self): + self._do_node_set_power_state_helper('on', soft=True, timeout=10, + error=True) + + def test_do_node_set_power_state_soft_off_timeout(self): + self._do_node_set_power_state_helper('off', soft=True, timeout=10) + + def test_do_node_set_power_state_soft_reboot_timeout(self): + self._do_node_set_power_state_helper('reboot', soft=True, timeout=10) + def test_do_node_set_target_raid_config_file(self): contents = '{"raid": "config"}' diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index 10354694e..f7205b8de 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -28,6 +28,8 @@ _power_states = { 'on': 'power on', 'off': 'power off', 'reboot': 'rebooting', + 'soft off': 'soft power off', + 'soft reboot': 'soft rebooting', } @@ -273,10 +275,39 @@ class NodeManager(base.CreateManager): else: return self.delete(path) - def set_power_state(self, node_id, state): + def set_power_state(self, node_id, state, soft=False, timeout=None): + """Sets power state for a node. + + :param node_id: Node identifier + :param state: One of target power state, 'on', 'off', or 'reboot' + :param soft: The flag for graceful power 'off' or 'reboot' + :param timeout: The timeout (in seconds) positive integer value (> 0) + :raises: ValueError if 'soft' or 'timeout' option is invalid + :returns: The status of the request + """ + if state == 'on' and soft: + raise ValueError( + _("'soft' option is invalid for the power-state 'on'")) + path = "%s/states/power" % node_id - target = {'target': _power_states.get(state, state)} - return self.update(path, target, http_method='PUT') + + requested_state = 'soft ' + state if soft else state + target = _power_states.get(requested_state, state) + + body = {'target': target} + if timeout is not None: + msg = _("'timeout' option for setting power state must have " + "positive integer value (> 0)") + try: + timeout = int(timeout) + except (ValueError, TypeError): + raise ValueError(msg) + + if timeout <= 0: + raise ValueError(msg) + body = {'target': target, 'timeout': timeout} + + return self.update(path, body, http_method='PUT') def set_target_raid_config(self, node_ident, target_raid_config): """Sets target_raid_config for a node. diff --git a/ironicclient/v1/node_shell.py b/ironicclient/v1/node_shell.py index 5cb03d405..3b6001273 100644 --- a/ironicclient/v1/node_shell.py +++ b/ironicclient/v1/node_shell.py @@ -14,6 +14,7 @@ # under the License. import argparse +import six from ironicclient.common.apiclient import exceptions from ironicclient.common import cliutils @@ -409,9 +410,27 @@ def do_node_set_maintenance(cc, args): metavar='', choices=['on', 'off', 'reboot'], help="'on', 'off', or 'reboot'.") +@cliutils.arg( + '--soft', + dest='soft', + action='store_true', + default=False, + help=("Gracefully change the power state. Only valid for 'off' and " + "'reboot' power states.")) +@cliutils.arg( + '--power-timeout', + metavar='', + type=int, + default=None, + help=("Timeout (in seconds, positive integer) to wait for the target " + "power state before erroring out.")) def do_node_set_power_state(cc, args): """Power a node on or off or reboot.""" - cc.node.set_power_state(args.node, args.power_state) + try: + cc.node.set_power_state(args.node, args.power_state, args.soft, + timeout=args.power_timeout) + except ValueError as e: + raise exc.CommandError(six.text_type(e)) @cliutils.arg('node', metavar='', help="Name or UUID of the node.") diff --git a/releasenotes/notes/soft-reboot-poweroff-e33d078a05db3894.yaml b/releasenotes/notes/soft-reboot-poweroff-e33d078a05db3894.yaml new file mode 100644 index 000000000..cbbd530c2 --- /dev/null +++ b/releasenotes/notes/soft-reboot-poweroff-e33d078a05db3894.yaml @@ -0,0 +1,4 @@ +--- +features: + - Add optional arguments '--soft' and '--power-timeout' to the command + "node-set-power-state".