diff --git a/ironicclient/common/http.py b/ironicclient/common/http.py index 35d1ccb8d..14cf9391b 100644 --- a/ironicclient/common/http.py +++ b/ironicclient/common/http.py @@ -44,7 +44,7 @@ from ironicclient import exc # http://specs.openstack.org/openstack/ironic-specs/specs/kilo/api-microversions.html # noqa # for full details. DEFAULT_VER = '1.9' -LAST_KNOWN_API_VERSION = 37 +LAST_KNOWN_API_VERSION = 38 LATEST_VERSION = '1.{}'.format(LAST_KNOWN_API_VERSION) LOG = logging.getLogger(__name__) diff --git a/ironicclient/osc/v1/baremetal_node.py b/ironicclient/osc/v1/baremetal_node.py index 350ba5208..313871520 100755 --- a/ironicclient/osc/v1/baremetal_node.py +++ b/ironicclient/osc/v1/baremetal_node.py @@ -67,12 +67,14 @@ class ProvisionStateBaremetalNode(command.Command): clean_steps = utils.handle_json_or_file_arg(clean_steps) config_drive = getattr(parsed_args, 'config_drive', None) + rescue_password = getattr(parsed_args, 'rescue_password', None) baremetal_client.node.set_provision_state( parsed_args.node, parsed_args.provision_state, configdrive=config_drive, - cleansteps=clean_steps) + cleansteps=clean_steps, + rescuepassword=rescue_password) class ProvisionStateWithWait(ProvisionStateBaremetalNode): @@ -926,6 +928,25 @@ class RebuildBaremetalNode(ProvisionStateWithWait): return parser +class RescueBaremetalNode(ProvisionStateWithWait): + """Set provision state of baremetal node to 'rescue'""" + + log = logging.getLogger(__name__ + ".RescueBaremetalNode") + PROVISION_STATE = 'rescue' + + def get_parser(self, prog_name): + parser = super(RescueBaremetalNode, self).get_parser(prog_name) + + parser.add_argument( + '--rescue-password', + metavar='', + required=True, + default=None, + help=("The password that will be used to login to the rescue " + "ramdisk. The value should be a string.")) + return parser + + class SetBaremetalNode(command.Command): """Set baremetal properties""" @@ -1221,6 +1242,13 @@ class UndeployBaremetalNode(ProvisionStateWithWait): PROVISION_STATE = 'deleted' +class UnrescueBaremetalNode(ProvisionStateWithWait): + """Set provision state of baremetal node to 'unrescue'""" + + log = logging.getLogger(__name__ + ".UnrescueBaremetalNode") + PROVISION_STATE = 'unrescue' + + class UnsetBaremetalNode(command.Command): """Unset baremetal properties""" log = logging.getLogger(__name__ + ".UnsetBaremetalNode") diff --git a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py index f6becf3b7..5bdfe74e2 100644 --- a/ironicclient/tests/unit/osc/v1/test_baremetal_node.py +++ b/ironicclient/tests/unit/osc/v1/test_baremetal_node.py @@ -55,7 +55,8 @@ class TestAdopt(TestBaremetal): self.cmd.take_action(parsed_args) self.baremetal_mock.node.set_provision_state.assert_called_once_with( - 'node_uuid', 'adopt', cleansteps=None, configdrive=None) + 'node_uuid', 'adopt', + cleansteps=None, configdrive=None, rescuepassword=None) def test_adopt_no_wait(self): arglist = ['node_uuid'] @@ -1199,7 +1200,7 @@ class TestDeployBaremetalProvisionState(TestBaremetal): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'active', - cleansteps=None, configdrive='path/to/drive') + cleansteps=None, configdrive='path/to/drive', rescuepassword=None) def test_deploy_no_wait(self): arglist = ['node_uuid'] @@ -1379,6 +1380,80 @@ class TestCleanBaremetalProvisionState(TestBaremetal): poll_interval=10, timeout=0) +class TestRescueBaremetalProvisionState(TestBaremetal): + def setUp(self): + super(TestRescueBaremetalProvisionState, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.RescueBaremetalNode(self.app, None) + + def test_rescue_baremetal_no_wait(self): + arglist = ['node_uuid', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'rescue', cleansteps=None, configdrive=None, + rescuepassword='supersecret') + + def test_rescue_baremetal_provision_state_rescue_and_wait(self): + arglist = ['node_uuid', + '--wait', '15', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ('wait_timeout', 15) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='rescue', + poll_interval=10, timeout=15) + + def test_rescue_baremetal_provision_state_default_wait(self): + arglist = ['node_uuid', + '--wait', + '--rescue-password', 'supersecret'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'rescue'), + ('rescue_password', 'supersecret'), + ('wait_timeout', 0) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='rescue', + poll_interval=10, timeout=0) + + def test_rescue_baremetal_no_rescue_password(self): + arglist = ['node_uuid'] + verifylist = [('node', 'node_uuid'), + ('provision_state', 'rescue')] + + self.assertRaises(oscutils.ParserException, + self.check_parser, + self.cmd, arglist, verifylist) + + class TestInspectBaremetalProvisionState(TestBaremetal): def setUp(self): super(TestInspectBaremetalProvisionState, self).setUp() @@ -1515,7 +1590,8 @@ class TestRebuildBaremetalProvisionState(TestBaremetal): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', - cleansteps=None, configdrive='path/to/drive') + cleansteps=None, configdrive='path/to/drive', + rescuepassword=None) def test_rebuild_no_wait(self): arglist = ['node_uuid'] @@ -1530,7 +1606,8 @@ class TestRebuildBaremetalProvisionState(TestBaremetal): self.baremetal_mock.node.set_provision_state.assert_called_once_with( 'node_uuid', 'rebuild', - cleansteps=None, configdrive=None) + cleansteps=None, configdrive=None, + rescuepassword=None) self.baremetal_mock.node.wait_for_provision_state.assert_not_called() @@ -1628,6 +1705,65 @@ class TestUndeployBaremetalProvisionState(TestBaremetal): poll_interval=10, timeout=0) +class TestUnrescueBaremetalProvisionState(TestBaremetal): + def setUp(self): + super(TestUnrescueBaremetalProvisionState, self).setUp() + + # Get the command object to test + self.cmd = baremetal_node.UnrescueBaremetalNode(self.app, None) + + def test_unrescue_no_wait(self): + arglist = ['node_uuid'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.baremetal_mock.node.set_provision_state.assert_called_once_with( + 'node_uuid', 'unrescue', cleansteps=None, configdrive=None, + rescuepassword=None) + + def test_unrescue_baremetal_provision_state_active_and_wait(self): + arglist = ['node_uuid', + '--wait', '15'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ('wait_timeout', 15) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='active', + poll_interval=10, timeout=15) + + def test_unrescue_baremetal_provision_state_default_wait(self): + arglist = ['node_uuid', + '--wait'] + verifylist = [ + ('node', 'node_uuid'), + ('provision_state', 'unrescue'), + ('wait_timeout', 0) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + test_node = self.baremetal_mock.node + test_node.wait_for_provision_state.assert_called_once_with( + 'node_uuid', expected_state='active', + poll_interval=10, timeout=0) + + class TestBaremetalReboot(TestBaremetal): def setUp(self): super(TestBaremetalReboot, self).setUp() diff --git a/ironicclient/tests/unit/v1/test_node.py b/ironicclient/tests/unit/v1/test_node.py index 7fde845dc..2fd06b313 100644 --- a/ironicclient/tests/unit/v1/test_node.py +++ b/ironicclient/tests/unit/v1/test_node.py @@ -1352,6 +1352,17 @@ class NodeManagerTest(testtools.TestCase): ] self.assertEqual(expect, self.api.calls) + def test_node_set_provision_state_with_rescuepassword(self): + rescuepassword = 'supersecret' + target_state = 'rescue' + self.mgr.set_provision_state(NODE1['uuid'], target_state, + rescuepassword=rescuepassword) + body = {'target': target_state, 'rescue_password': rescuepassword} + expect = [ + ('PUT', '/v1/nodes/%s/states/provision' % NODE1['uuid'], {}, body), + ] + self.assertEqual(expect, self.api.calls) + def test_node_states(self): states = self.mgr.states(NODE1['uuid']) expect = [ diff --git a/ironicclient/v1/node.py b/ironicclient/v1/node.py index b8571abfd..0be96f700 100644 --- a/ironicclient/v1/node.py +++ b/ironicclient/v1/node.py @@ -478,12 +478,13 @@ class NodeManager(base.CreateManager): return self.get(path) def set_provision_state(self, node_uuid, state, configdrive=None, - cleansteps=None): + cleansteps=None, rescuepassword=None): """Set the provision state for the node. :param node_uuid: The UUID or name of the node. :param state: The desired provision state. One of 'active', 'deleted', - 'rebuild', 'inspect', 'provide', 'manage', 'clean', 'abort'. + 'rebuild', 'inspect', 'provide', 'manage', 'clean', 'abort', + 'rescue', 'unrescue'. :param configdrive: A gzipped, base64-encoded configuration drive string OR the path to the configuration drive file OR the path to a directory containing the config drive files. In case it's a @@ -493,6 +494,10 @@ class NodeManager(base.CreateManager): dictionaries; each dictionary should have keys 'interface' and 'step', and optional key 'args'. This must be specified (and is only valid) when setting provision-state to 'clean'. + :param rescuepassword: A string to be used as the login password + inside the rescue ramdisk once a node is rescued. This must be + specified (and is only valid) when setting provision-state to + 'rescue'. :raises: InvalidAttribute if there was an error with the clean steps :returns: The status of the request """ @@ -509,6 +514,8 @@ class NodeManager(base.CreateManager): body['configdrive'] = configdrive elif cleansteps: body['clean_steps'] = cleansteps + elif rescuepassword: + body['rescue_password'] = rescuepassword return self.update(path, body, http_method='PUT') diff --git a/ironicclient/v1/utils.py b/ironicclient/v1/utils.py index 19c8e4899..4c0719306 100644 --- a/ironicclient/v1/utils.py +++ b/ironicclient/v1/utils.py @@ -43,6 +43,10 @@ PROVISION_ACTIONS = { 'adopt': {'expected_state': 'active', 'poll_interval': _SHORT_ACTION_POLL_INTERVAL}, 'abort': None, # no support for --wait in abort + 'rescue': {'expected_state': 'rescue', + 'poll_interval': _LONG_ACTION_POLL_INTERVAL}, + 'unrescue': {'expected_state': 'active', + 'poll_interval': _LONG_ACTION_POLL_INTERVAL}, } PROVISION_STATES = list(PROVISION_ACTIONS) diff --git a/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml b/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml new file mode 100644 index 000000000..57da86b75 --- /dev/null +++ b/releasenotes/notes/add-rescue-unrescue-support-f78266514ca59346.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds the below commands to OSC to support rescue mode for ironic + available starting with API version 1.38: + + * ``openstack baremetal node rescue`` + * ``openstack baremetal node unrescue`` diff --git a/setup.cfg b/setup.cfg index 4a40c8d67..3f777b574 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,10 +66,12 @@ openstack.baremetal.v1 = baremetal_node_reboot = ironicclient.osc.v1.baremetal_node:RebootBaremetalNode baremetal_node_rebuild = ironicclient.osc.v1.baremetal_node:RebuildBaremetalNode baremetal_node_remove_trait = ironicclient.osc.v1.baremetal_node:RemoveTraitBaremetalNode + baremetal_node_rescue = ironicclient.osc.v1.baremetal_node:RescueBaremetalNode baremetal_node_set = ironicclient.osc.v1.baremetal_node:SetBaremetalNode baremetal_node_show = ironicclient.osc.v1.baremetal_node:ShowBaremetalNode baremetal_node_trait_list = ironicclient.osc.v1.baremetal_node:ListTraitsBaremetalNode baremetal_node_undeploy = ironicclient.osc.v1.baremetal_node:UndeployBaremetalNode + baremetal_node_unrescue = ironicclient.osc.v1.baremetal_node:UnrescueBaremetalNode baremetal_node_unset = ironicclient.osc.v1.baremetal_node:UnsetBaremetalNode baremetal_node_validate = ironicclient.osc.v1.baremetal_node:ValidateBaremetalNode baremetal_node_vif_attach = ironicclient.osc.v1.baremetal_node:VifAttachBaremetalNode