diff --git a/releasenotes/notes/overcloud-node-bios-c9ae89e35a96c7b1.yaml b/releasenotes/notes/overcloud-node-bios-c9ae89e35a96c7b1.yaml new file mode 100644 index 000000000..11c2d4c9c --- /dev/null +++ b/releasenotes/notes/overcloud-node-bios-c9ae89e35a96c7b1.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Adds new commands to run BIOS cleaning on nodes:: + + openstack overcloud node bios configure \ + --configuration <..> [--all-manageable|uuid1,uuid2,..] + + openstack overcloud node bios reset \ + [--all-manageable|uuid1,uuid2,..] + + The first command configures given BIOS settings on given nodes or all + manageable nodes; the second command reset BIOS settings to factory + default on given nodes or all manageable nodes. diff --git a/setup.cfg b/setup.cfg index e8e55043a..b84737e8b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -72,6 +72,8 @@ openstack.tripleoclient.v1 = overcloud_node_provide = tripleoclient.v1.overcloud_node:ProvideNode overcloud_node_discover = tripleoclient.v1.overcloud_node:DiscoverNode overcloud_node_clean = tripleoclient.v1.overcloud_node:CleanNode + overcloud_node_bios_configure = tripleoclient.v1.overcloud_bios:ConfigureBIOS + overcloud_node_bios_reset = tripleoclient.v1.overcloud_bios:ResetBIOS overcloud_parameters_set = tripleoclient.v1.overcloud_parameters:SetParameters overcloud_plan_create = tripleoclient.v1.overcloud_plan:CreatePlan overcloud_plan_delete = tripleoclient.v1.overcloud_plan:DeletePlan diff --git a/tripleoclient/tests/v1/test_overcloud_bios.py b/tripleoclient/tests/v1/test_overcloud_bios.py new file mode 100644 index 000000000..2c44fca05 --- /dev/null +++ b/tripleoclient/tests/v1/test_overcloud_bios.py @@ -0,0 +1,264 @@ +# 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 json +import tempfile + +from osc_lib.tests import utils as test_utils + +from tripleoclient.tests.v1.baremetal import fakes +from tripleoclient.v1 import overcloud_bios + + +class Base(fakes.TestBaremetal): + def setUp(self): + super(Base, self).setUp() + self.workflow = self.app.client_manager.workflow_engine + self.conf = { + "settings": [ + {"name": "virtualization", "value": "on"}, + {"name": "hyperthreading", "value": "on"} + ] + } + tripleoclient = self.app.client_manager.tripleoclient + self.websocket = tripleoclient.messaging_websocket() + self.websocket.wait_for_messages.return_value = iter([{ + 'status': "SUCCESS", + 'execution_id': 'fake id', + 'root_execution_id': 'fake id', + }]) + + self.execution = self.workflow.executions.create.return_value + self.execution.id = 'fake id' + self.execution.output = '{"result": null}' + + +class TestConfigureBIOS(Base): + + def setUp(self): + super(TestConfigureBIOS, self).setUp() + self.cmd = overcloud_bios.ConfigureBIOS(self.app, None) + + def test_configure_specified_nodes_ok(self): + conf = json.dumps(self.conf) + arglist = ['--configuration', conf, 'node_uuid1', 'node_uuid2'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.apply_bios_settings', + workflow_input={ + 'node_uuids': ['node_uuid1', 'node_uuid2'], + 'configuration': self.conf, + } + ) + + def test_configure_specified_nodes_and_configuration_from_file(self): + with tempfile.NamedTemporaryFile('w+t') as fp: + json.dump(self.conf, fp) + fp.flush() + arglist = ['--configuration', fp.name, 'node_uuid1', 'node_uuid2'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', fp.name) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.apply_bios_settings', + workflow_input={ + 'node_uuids': ['node_uuid1', 'node_uuid2'], + 'configuration': self.conf, + } + ) + + def test_configure_no_nodes(self): + arglist = ['--configuration', '{}'] + verifylist = [ + ('configuration', '{}') + ] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + self.assertFalse(self.workflow.executions.create.called) + + def test_configure_specified_nodes_and_configuration_not_yaml(self): + arglist = ['--configuration', ':', 'node_uuid1', 'node_uuid2'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', ':') + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaisesRegex(RuntimeError, 'cannot be parsed as YAML', + self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) + + def test_configure_specified_nodes_and_configuration_bad_type(self): + for conf in ('[]', '{"settings": 42}', '{settings: [42]}'): + arglist = ['--configuration', conf, 'node_uuid1', 'node_uuid2'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(TypeError, self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) + + def test_configure_specified_nodes_and_configuration_bad_value(self): + conf = '{"another_key": [{}]}' + arglist = ['--configuration', conf, 'node_uuid1', 'node_uuid2'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises(ValueError, self.cmd.take_action, parsed_args) + self.assertFalse(self.workflow.executions.create.called) + + def test_configure_uuids_and_all_both_specified(self): + conf = json.dumps(self.conf) + arglist = ['--configuration', conf, 'node_uuid1', 'node_uuid2', + '--all-manageable'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('configuration', conf), + ('all-manageable', True) + ] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + + def test_configure_all_manageable_nodes_ok(self): + conf = json.dumps(self.conf) + arglist = ['--configuration', conf, '--all-manageable'] + verifylist = [ + ('all_manageable', True), + ('configuration', conf) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.apply_bios_settings_on_manageable_nodes', + workflow_input={'configuration': self.conf}) + + def test_configure_all_manageable_nodes_fail(self): + conf = json.dumps(self.conf) + arglist = ['--configuration', conf, '--all-manageable'] + verifylist = [ + ('all_manageable', True), + ('configuration', conf) + ] + + self.websocket.wait_for_messages.return_value = iter([{ + "status": "FAILED", + "message": "Test failure.", + 'execution_id': 'fake id', + 'root_execution_id': 'fake id', + }]) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaisesRegex(RuntimeError, 'Failed to apply BIOS settings', + self.cmd.take_action, parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.apply_bios_settings_on_manageable_nodes', + workflow_input={'configuration': self.conf}) + + +class TestResetBIOS(Base): + + def setUp(self): + super(TestResetBIOS, self).setUp() + self.cmd = overcloud_bios.ResetBIOS(self.app, None) + + def test_reset_specified_nodes_ok(self): + arglist = ['node_uuid1', 'node_uuid2'] + verifylist = [('node_uuids', ['node_uuid1', 'node_uuid2'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.reset_bios_settings', + workflow_input={'node_uuids': ['node_uuid1', 'node_uuid2']}) + + def test_reset_specified_nodes_fail(self): + arglist = ['node_uuid1', 'node_uuid2'] + verifylist = [('node_uuids', ['node_uuid1', 'node_uuid2'])] + + self.websocket.wait_for_messages.return_value = iter([{ + "status": "FAILED", + "message": "Test failure.", + 'execution_id': 'fake id', + 'root_execution_id': 'fake id', + }]) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaisesRegex(RuntimeError, 'Failed to reset BIOS settings', + self.cmd.take_action, parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.reset_bios_settings', + workflow_input={'node_uuids': ['node_uuid1', 'node_uuid2']}) + + def test_reset_all_manageable_nodes_ok(self): + arglist = ['--all-manageable'] + verifylist = [('all_manageable', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.reset_bios_settings_on_manageable_nodes', + workflow_input={}) + + def test_reset_all_manageable_nodes_fail(self): + arglist = ['--all-manageable'] + verifylist = [('all_manageable', True)] + + self.websocket.wait_for_messages.return_value = iter([{ + "status": "FAILED", + "message": "Test failure.", + 'execution_id': 'fake id', + 'root_execution_id': 'fake id', + }]) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaisesRegex(RuntimeError, 'Failed to reset BIOS settings', + self.cmd.take_action, parsed_args) + + self.workflow.executions.create.assert_called_once_with( + 'tripleo.baremetal.v1.reset_bios_settings_on_manageable_nodes', + workflow_input={}) + + def test_reset_no_nodes(self): + arglist = [] + verifylist = [] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + self.assertFalse(self.workflow.executions.create.called) + + def test_reset_uuids_and_all_both_specified(self): + arglist = ['node_uuid1', 'node_uuid2', '--all-manageable'] + verifylist = [ + ('node_uuids', ['node_uuid1', 'node_uuid2']), + ('all-manageable', True) + ] + self.assertRaises(test_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) diff --git a/tripleoclient/v1/overcloud_bios.py b/tripleoclient/v1/overcloud_bios.py new file mode 100644 index 000000000..c62740cf4 --- /dev/null +++ b/tripleoclient/v1/overcloud_bios.py @@ -0,0 +1,119 @@ +# Copyright 2018 Red Hat, Inc. +# +# 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 logging +import os + +from osc_lib.i18n import _ +import yaml + +from tripleoclient import command +from tripleoclient.workflows import baremetal + + +class ConfigureBIOS(command.Command): + """Apply BIOS configuration on given nodes""" + + log = logging.getLogger(__name__ + ".ConfigureBIOS") + + def get_parser(self, prog_name): + parser = super(ConfigureBIOS, self).get_parser(prog_name) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('node_uuids', + nargs="*", + metavar="", + default=[], + help=_('Baremetal Node UUIDs for the node(s) to ' + 'configure BIOS')) + group.add_argument("--all-manageable", + action='store_true', + help=_("Configure BIOS for all nodes currently in " + "'manageable' state")) + parser.add_argument('--configuration', metavar='', + dest='configuration', + help=_('BIOS configuration (YAML/JSON string or ' + 'file name).')) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action({args})".format(args=parsed_args)) + + if os.path.exists(parsed_args.configuration): + with open(parsed_args.configuration, 'r') as fp: + configuration = yaml.safe_load(fp.read()) + else: + try: + configuration = yaml.safe_load(parsed_args.configuration) + except yaml.YAMLError as exc: + raise RuntimeError( + _('Configuration is not an existing file and cannot be ' + 'parsed as YAML: %s') % exc) + + # Basic sanity check, we defer the full check to Ironic + try: + settings = configuration['settings'] + except KeyError: + raise ValueError( + _('Configuration must contain key "settings"')) + except TypeError: + raise TypeError( + _('Configuration must be an object, got %r instead') + % configuration) + + if (not isinstance(settings, list) or + not all(isinstance(item, dict) for item in settings)): + raise TypeError( + _('BIOS settings list is expected to be a list of ' + 'objects, got %r instead') % settings) + + clients = self.app.client_manager + if parsed_args.node_uuids: + baremetal.apply_bios_configuration( + clients, node_uuids=parsed_args.node_uuids, + configuration=configuration) + else: + baremetal.apply_bios_configuration_on_manageable_nodes( + clients, configuration=configuration) + + +class ResetBIOS(command.Command): + """Reset BIOS configuration to factory default""" + + log = logging.getLogger(__name__ + ".ResetBIOS") + + def get_parser(self, prog_name): + parser = super(ResetBIOS, self).get_parser(prog_name) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('node_uuids', + nargs="*", + metavar="", + default=[], + help=_('Baremetal Node UUIDs for the node(s) to ' + 'reset BIOS')) + group.add_argument("--all-manageable", + action='store_true', + help=_("Reset BIOS on all nodes currently in " + "'manageable' state")) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action({args})".format(args=parsed_args)) + + clients = self.app.client_manager + if parsed_args.node_uuids: + baremetal.reset_bios_configuration( + clients, node_uuids=parsed_args.node_uuids) + else: + baremetal.reset_bios_configuration_on_manageable_nodes(clients) diff --git a/tripleoclient/workflows/baremetal.py b/tripleoclient/workflows/baremetal.py index a47784c30..899d356b2 100644 --- a/tripleoclient/workflows/baremetal.py +++ b/tripleoclient/workflows/baremetal.py @@ -392,3 +392,121 @@ def clean_manageable_nodes(clients, **workflow_input): 'Error cleaning nodes: {}'.format(payload['message'])) print('Cleaned %d node(s)' % len(payload['cleaned_nodes'])) + + +def apply_bios_configuration(clients, **workflow_input): + """Apply BIOS settings on nodes. + + Run the tripleo.baremetal.v1.apply_bios_settings Mistral workflow. + """ + + workflow_client = clients.workflow_engine + ooo_client = clients.tripleoclient + + print('Applying BIOS settings for given nodes, this may take time') + + with ooo_client.messaging_websocket() as ws: + execution = base.start_workflow( + workflow_client, + 'tripleo.baremetal.v1.apply_bios_settings', + workflow_input=workflow_input + ) + + for payload in base.wait_for_messages(workflow_client, ws, execution): + if payload.get('message'): + print(payload['message']) + + if payload['status'] == 'SUCCESS': + print('Success') + else: + raise RuntimeError( + 'Failed to apply BIOS settings: {}'.format(payload['message'])) + + +def apply_bios_configuration_on_manageable_nodes(clients, **workflow_input): + """Apply BIOS settings on manageable nodes. + + Run the tripleo.baremetal.v1.apply_bios_settings_on_manageable_nodes + Mistral workflow. + """ + + workflow_client = clients.workflow_engine + ooo_client = clients.tripleoclient + + print('Applying BIOS settings for manageable nodes, this may take time') + + with ooo_client.messaging_websocket() as ws: + execution = base.start_workflow( + workflow_client, + 'tripleo.baremetal.v1.apply_bios_settings_on_manageable_nodes', + workflow_input=workflow_input + ) + + for payload in base.wait_for_messages(workflow_client, ws, execution): + if payload.get('message'): + print(payload['message']) + + if payload['status'] == 'SUCCESS': + print('Success') + else: + raise RuntimeError( + 'Failed to apply BIOS settings: {}'.format(payload['message'])) + + +def reset_bios_configuration(clients, **workflow_input): + """Reset BIOS settings on nodes. + + Run the tripleo.baremetal.v1.reset_bios_settings Mistral workflow. + """ + + workflow_client = clients.workflow_engine + ooo_client = clients.tripleoclient + + print('Reset BIOS settings on given nodes, this may take time') + + with ooo_client.messaging_websocket() as ws: + execution = base.start_workflow( + workflow_client, + 'tripleo.baremetal.v1.reset_bios_settings', + workflow_input=workflow_input + ) + + for payload in base.wait_for_messages(workflow_client, ws, execution): + if payload.get('message'): + print(payload['message']) + + if payload['status'] == 'SUCCESS': + print('Success') + else: + raise RuntimeError( + 'Failed to reset BIOS settings: {}'.format(payload['message'])) + + +def reset_bios_configuration_on_manageable_nodes(clients, **workflow_input): + """Reset BIOS settings on manageable nodes. + + Run the tripleo.baremetal.v1.reset_bios_settings_on_manageable_nodes + Mistral workflow. + """ + + workflow_client = clients.workflow_engine + ooo_client = clients.tripleoclient + + print('Reset BIOS settings on manageable nodes, this may take time') + + with ooo_client.messaging_websocket() as ws: + execution = base.start_workflow( + workflow_client, + 'tripleo.baremetal.v1.reset_bios_settings_on_manageable_nodes', + workflow_input=workflow_input + ) + + for payload in base.wait_for_messages(workflow_client, ws, execution): + if payload.get('message'): + print(payload['message']) + + if payload['status'] == 'SUCCESS': + print('Success') + else: + raise RuntimeError( + 'Failed to reset BIOS settings: {}'.format(payload['message']))