From 3ac32b592ca8cbfbfd4a8c96f79aa971de351fd4 Mon Sep 17 00:00:00 2001 From: Brendan Shephard Date: Thu, 6 Jan 2022 01:49:35 +0000 Subject: [PATCH] Move baremetal provide commands from Ansible Currently, we leverage Ansible to handle the baremetal provide process. As part of our efforts to support work on Tripleo.Next, these Ansible workflows will need to be migrated to tripleoclient. This change is consolodating Python methods from tripleo-ansible into tripleoclient. Change-Id: I1ff12506d4f6a7d5868aac7128b9f9dc3b8893ff --- tripleoclient/exceptions.py | 7 + tripleoclient/tests/fakes.py | 21 ++ .../tests/v1/overcloud_node/fakes.py | 10 + .../v1/overcloud_node/test_overcloud_node.py | 189 +++++++++++++-- .../tests/v2/overcloud_node/fakes.py | 10 + .../v2/overcloud_node/test_overcloud_node.py | 220 ++++++++++++++---- .../tests/workflows/test_baremetal.py | 8 - tripleoclient/v1/overcloud_node.py | 33 ++- tripleoclient/v2/overcloud_node.py | 24 +- tripleoclient/workflows/baremetal.py | 44 ---- tripleoclient/workflows/tripleo_baremetal.py | 177 ++++++++++++++ 11 files changed, 603 insertions(+), 140 deletions(-) create mode 100644 tripleoclient/workflows/tripleo_baremetal.py diff --git a/tripleoclient/exceptions.py b/tripleoclient/exceptions.py index aac367bc7..ca50d2df5 100644 --- a/tripleoclient/exceptions.py +++ b/tripleoclient/exceptions.py @@ -143,3 +143,10 @@ class HeatPodMessageQueueException(Base): class InvalidPlaybook(Base): """Invalid playbook path specified""" + + +class NoNodeFound(Base): + """No nodes matching specifications found""" + def __init__(self): + message = "No nodes matching specifications could be found. " + super(NoNodeFound, self).__init__(message) diff --git a/tripleoclient/tests/fakes.py b/tripleoclient/tests/fakes.py index 556ce30e2..b6df9ea76 100644 --- a/tripleoclient/tests/fakes.py +++ b/tripleoclient/tests/fakes.py @@ -422,3 +422,24 @@ class FakeFlavor(object): 'capabilities:boot_option': 'local', 'capabilities:profile': self.profile } + + +class FakeMachine: + def __init__(self, id, name=None, driver=None, driver_info=None, + chassis_uuid=None, instance_info=None, instance_uuid=None, + properties=None, reservation=None, last_error=None, + provision_state='available', is_maintenance=False, + power_state='power off'): + self.id = id + self.name = name + self.driver = driver + self.driver_info = driver_info + self.chassis_uuid = chassis_uuid + self.instance_info = instance_info + self.instance_uuid = instance_uuid + self.properties = properties + self.reservation = reservation + self.last_error = last_error + self.provision_state = provision_state + self.is_maintenance = is_maintenance + self.power_state = power_state diff --git a/tripleoclient/tests/v1/overcloud_node/fakes.py b/tripleoclient/tests/v1/overcloud_node/fakes.py index bb4ce88f9..212fcede3 100644 --- a/tripleoclient/tests/v1/overcloud_node/fakes.py +++ b/tripleoclient/tests/v1/overcloud_node/fakes.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. # +import uuid from unittest import mock @@ -35,3 +36,12 @@ class TestOvercloudNode(fakes.FakePlaybookExecution): ) self.mock_playbook.start() self.addCleanup(self.mock_playbook.stop) + + +def make_fake_machine(machine_name, provision_state='manageable', + is_maintenance=False, machine_id=None): + if not machine_id: + machine_id = uuid.uuid4().hex + return(fakes.FakeMachine(id=machine_id, name=machine_name, + provision_state=provision_state, + is_maintenance=is_maintenance)) diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index 2132cc3e1..a8a3b1d37 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -21,6 +21,8 @@ import os import tempfile from unittest import mock +import openstack + from osc_lib import exceptions as oscexc from osc_lib.tests import utils as test_utils import yaml @@ -375,40 +377,96 @@ class TestDeleteNode(fakes.TestDeleteNode): mock_warning.assert_called_once_with(expected_message) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', + autospec=True, name='mock_bm') +@mock.patch('openstack.config', autospec=True, + name='mock_conf') +@mock.patch('openstack.connect', autospec=True, + name='mock_connect') +@mock.patch.object(openstack.connection, + 'Connection', autospec=True) class TestProvideNode(fakes.TestOvercloudNode): def setUp(self): super(TestProvideNode, self).setUp() - # Get the command object to test self.cmd = overcloud_node.ProvideNode(self.app, None) - def test_provide_all_manageable_nodes(self): + iterate_timeout = mock.MagicMock() + iterate_timeout.start() + + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1' + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176' + ) + + def test_provide_all_manageable_nodes(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node2]) + ] + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2] parsed_args = self.check_parser(self.cmd, ['--all-manageable'], [('all_manageable', True)]) self.cmd.take_action(parsed_args) - def test_provide_one_node(self): + def test_provide_one_node(self, mock_conn, + mock_connect, mock_conf, + mock_bm): node_id = 'node_uuid1' + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node] + parsed_args = self.check_parser(self.cmd, [node_id], [('node_uuids', [node_id])]) self.cmd.take_action(parsed_args) - def test_provide_multiple_nodes(self): + def test_provide_multiple_nodes(self, mock_conn, + mock_connect, mock_conf, + mock_bm): node_id1 = 'node_uuid1' node_id2 = 'node_uuid2' argslist = [node_id1, node_id2] verifylist = [('node_uuids', [node_id1, node_id2])] + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2 + ] + parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', + autospec=True, name='mock_bm') +@mock.patch('openstack.config', autospec=True, + name='mock_conf') +@mock.patch('openstack.connect', autospec=True, + name='mock_connect') +@mock.patch.object(openstack.connection, + 'Connection', autospec=True) class TestCleanNode(fakes.TestOvercloudNode): def setUp(self): @@ -417,41 +475,102 @@ class TestCleanNode(fakes.TestOvercloudNode): # Get the command object to test self.cmd = overcloud_node.CleanNode(self.app, None) - def _check_clean_all_manageable(self, parsed_args, provide=False): + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1' + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176' + ) + + def _check_clean_all_manageable(self, parsed_args, mock_conn, + mock_connect, mock_conf, + mock_bm, + provide=False): + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node]) + ] + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node] self.cmd.take_action(parsed_args) - def _check_clean_nodes(self, parsed_args, nodes, provide=False): + def _check_clean_nodes(self, parsed_args, nodes, mock_conn, + mock_connect, mock_conf, + mock_bm, provide=False): self.cmd.take_action(parsed_args) - def test_clean_all_manageable_nodes_without_provide(self): + def test_clean_all_manageable_nodes_without_provide(self, mock_conn, + mock_connect, + mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.nodes.return_value = iter([ + self.fake_baremetal_node + ]) parsed_args = self.check_parser(self.cmd, ['--all-manageable'], [('all_manageable', True)]) - self._check_clean_all_manageable(parsed_args, provide=False) + self._check_clean_all_manageable(parsed_args, mock_conn, + mock_connect, mock_conf, + mock_bm, provide=False) - def test_clean_all_manageable_nodes_with_provide(self): + def test_clean_all_manageable_nodes_with_provide(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node]), + iter([self.fake_baremetal_node])] + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node] parsed_args = self.check_parser(self.cmd, ['--all-manageable', '--provide'], [('all_manageable', True), ('provide', True)]) - self._check_clean_all_manageable(parsed_args, provide=True) + self._check_clean_all_manageable(parsed_args, mock_conn, + mock_connect, mock_conf, + mock_bm, provide=False) - def test_clean_nodes_without_provide(self): + def test_clean_nodes_without_provide(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm nodes = ['node_uuid1', 'node_uuid2'] parsed_args = self.check_parser(self.cmd, nodes, [('node_uuids', nodes)]) - self._check_clean_nodes(parsed_args, nodes, provide=False) + self._check_clean_nodes(parsed_args, nodes, mock_conn, + mock_connect, mock_conf, + mock_bm, provide=False) + + def test_clean_nodes_with_provide(self, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm - def test_clean_nodes_with_provide(self): nodes = ['node_uuid1', 'node_uuid2'] argslist = nodes + ['--provide'] + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2 + ] + parsed_args = self.check_parser(self.cmd, argslist, [('node_uuids', nodes), ('provide', True)]) - self._check_clean_nodes(parsed_args, nodes, provide=True) + self._check_clean_nodes(parsed_args, nodes, mock_conn, + mock_connect, mock_conf, + mock_bm, provide=False) class TestImportNodeMultiArch(fakes.TestOvercloudNode): @@ -673,6 +792,17 @@ class TestConfigureNode(fakes.TestOvercloudNode): self.cmd.take_action(parsed_args) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', autospec=True, + name="mock_bm") +@mock.patch('openstack.config', autospec=True, name='mock_conf') +@mock.patch('openstack.connect', autospec=True, name='mock_connect') +@mock.patch.object(openstack.connection, 'Connection', autospec=True) +@mock.patch('tripleo_common.utils.nodes._populate_node_mapping', + name='mock_nodemap') +@mock.patch('tripleo_common.utils.nodes.register_all_nodes', + name='mock_tcnode') +@mock.patch('oslo_concurrency.processutils.execute', + name="mock_subproc") class TestDiscoverNode(fakes.TestOvercloudNode): def setUp(self): @@ -688,8 +818,19 @@ class TestDiscoverNode(fakes.TestOvercloudNode): self.addCleanup(self.gcn.stop) self.http_boot = '/var/lib/ironic/httpboot' + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1' + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176' + ) - def test_with_ip_range(self): + def test_with_ip_range(self, mock_subproc, mock_tcnode, + mock_nodemap, mock_conn, + mock_connect, mock_conf, + mock_bm): argslist = ['--range', '10.0.0.0/24', '--credentials', 'admin:password'] verifylist = [('ip_addresses', '10.0.0.0/24'), @@ -698,7 +839,10 @@ class TestDiscoverNode(fakes.TestOvercloudNode): parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) - def test_with_address_list(self): + def test_with_address_list(self, mock_subproc, mock_tcnode, + mock_nodemap, mock_conn, + mock_connect, mock_conf, + mock_bm): argslist = ['--ip', '10.0.0.1', '--ip', '10.0.0.2', '--credentials', 'admin:password'] verifylist = [('ip_addresses', ['10.0.0.1', '10.0.0.2']), @@ -707,7 +851,18 @@ class TestDiscoverNode(fakes.TestOvercloudNode): parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) - def test_with_all_options(self): + def test_with_all_options(self, mock_subproc, mock_tcnode, + mock_nodemap, mock_conn, + mock_connect, mock_conf, + mock_bm): + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2, + self.fake_baremetal_node, + self.fake_baremetal_node2 + ] argslist = ['--range', '10.0.0.0/24', '--credentials', 'admin:password', '--credentials', 'admin2:password2', diff --git a/tripleoclient/tests/v2/overcloud_node/fakes.py b/tripleoclient/tests/v2/overcloud_node/fakes.py index 21baff532..808d082d0 100644 --- a/tripleoclient/tests/v2/overcloud_node/fakes.py +++ b/tripleoclient/tests/v2/overcloud_node/fakes.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. # +import uuid from tripleoclient.tests import fakes @@ -26,3 +27,12 @@ class TestOvercloudNode(fakes.FakePlaybookExecution): def setUp(self): super(TestOvercloudNode, self).setUp() + + +def make_fake_machine(machine_name, provision_state, + is_maintenance, machine_id=None): + if not machine_id: + machine_id = uuid.uuid4().hex + return(fakes.FakeMachine(id=machine_id, name=machine_name, + provision_state=provision_state, + is_maintenance=is_maintenance)) diff --git a/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py index ed52ac3fc..923dfacd2 100644 --- a/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py @@ -20,13 +20,22 @@ import os import tempfile from unittest import mock +import openstack from osc_lib.tests import utils as test_utils from tripleoclient import constants from tripleoclient.tests.v2.overcloud_node import fakes from tripleoclient.v2 import overcloud_node +from tripleoclient.workflows import tripleo_baremetal as tb +@mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', autospec=True, + name="mock_bm") +@mock.patch('openstack.config', autospec=True, name='mock_conf') +@mock.patch('openstack.connect', autospec=True, name='mock_connect') +@mock.patch.object(openstack.connection, 'Connection', autospec=True) class TestImportNode(fakes.TestOvercloudNode): def setUp(self): @@ -49,6 +58,18 @@ class TestImportNode(fakes.TestOvercloudNode): ] }] + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1', + provision_state='manageable', + is_maintenance=False + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176', + provision_state='manageable', + is_maintenance=False + ) self.json_file = tempfile.NamedTemporaryFile( mode='w', delete=False, suffix='.json') json.dump(self.nodes_list, self.json_file) @@ -72,18 +93,24 @@ class TestImportNode(fakes.TestOvercloudNode): for i in ('agent.kernel', 'agent.ramdisk')])) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_only(self, mock_playbook): + def test_import_only(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name], [('introspect', False), ('provide', False)]) self.cmd.take_action(parsed_args) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_and_introspect(self, mock_playbook): + def test_import_and_introspect(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name, '--introspect'], @@ -103,25 +130,45 @@ class TestImportNode(fakes.TestOvercloudNode): } ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_and_provide(self, mock_playbook): + def test_import_and_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name, '--provide'], [('introspect', False), ('provide', True)]) + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2] + self.cmd.take_action(parsed_args) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_and_introspect_and_provide(self, mock_playbook): + def test_import_and_introspect_and_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name, '--introspect', '--provide'], [('introspect', True), ('provide', True)]) + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2] + self.cmd.take_action(parsed_args) mock_playbook.assert_called_with( workdir=mock.ANY, @@ -130,22 +177,30 @@ class TestImportNode(fakes.TestOvercloudNode): playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, verbosity=mock.ANY, extra_vars={ - 'node_uuids': ['MOCK_NODE_UUID'] + 'node_uuids': ['MOCK_NODE_UUID'], + 'run_validations': False, + 'concurrency': 20 } ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_with_netboot(self, mock_playbook): + def test_import_with_netboot(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name, '--no-deploy-image'], [('no_deploy_image', True)]) self.cmd.take_action(parsed_args) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_import_with_no_deployed_image(self, mock_playbook): + def test_import_with_no_deployed_image(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): parsed_args = self.check_parser(self.cmd, [self.json_file.name, '--instance-boot-option', @@ -154,16 +209,37 @@ class TestImportNode(fakes.TestOvercloudNode): self.cmd.take_action(parsed_args) +@mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) +@mock.patch.object(openstack.baremetal.v1._proxy, 'Proxy', autospec=True, + name="mock_bm") +@mock.patch('openstack.config', autospec=True, name='mock_conf') +@mock.patch('openstack.connect', autospec=True, name='mock_connect') +@mock.patch.object(openstack.connection, 'Connection', autospec=True) class TestIntrospectNode(fakes.TestOvercloudNode): def setUp(self): super(TestIntrospectNode, self).setUp() # Get the command object to test self.cmd = overcloud_node.IntrospectNode(self.app, None) + self.fake_baremetal_node = fakes.make_fake_machine( + machine_name='node1', + machine_id='4e540e11-1366-4b57-85d5-319d168d98a1', + provision_state='manageable', + is_maintenance=False + ) + self.fake_baremetal_node2 = fakes.make_fake_machine( + machine_name='node2', + machine_id='9070e42d-1ad7-4bd0-b868-5418bc9c7176', + provision_state='manageable', + is_maintenance=False + ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) def test_introspect_all_manageable_nodes_without_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, mock_playbook): parsed_args = self.check_parser(self.cmd, ['--all-manageable'], @@ -185,29 +261,54 @@ class TestIntrospectNode(fakes.TestOvercloudNode): } ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) def test_introspect_all_manageable_nodes_with_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, mock_playbook): parsed_args = self.check_parser(self.cmd, ['--all-manageable', '--provide'], [('all_manageable', True), ('provide', True)]) + tb.TripleoProvide.provide = mock.MagicMock() + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal.nodes.side_effect = [ + iter([self.fake_baremetal_node, + self.fake_baremetal_node2]), + iter([self.fake_baremetal_node, + self.fake_baremetal_node2]) + ] + + expected_nodes = ['4e540e11-1366-4b57-85d5-319d168d98a1', + '9070e42d-1ad7-4bd0-b868-5418bc9c7176'] self.cmd.take_action(parsed_args) mock_playbook.assert_called_with( workdir=mock.ANY, - playbook='cli-overcloud-node-provide.yaml', + playbook='cli-baremetal-introspect.yaml', inventory=mock.ANY, playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, verbosity=mock.ANY, extra_vars={ - 'node_uuids': [] + 'node_uuids': [], + 'run_validations': False, + 'concurrency': 20, + 'node_timeout': 1200, + 'max_retries': 1, + 'retry_timeout': 120, } ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_introspect_nodes_without_provide(self, mock_playbook): + tb.TripleoProvide.provide.assert_called_with( + expected_nodes) + + def test_introspect_nodes_without_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): nodes = ['node_uuid1', 'node_uuid2'] parsed_args = self.check_parser(self.cmd, nodes, @@ -229,37 +330,48 @@ class TestIntrospectNode(fakes.TestOvercloudNode): } ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_introspect_nodes_with_provide(self, mock_playbook): - nodes = ['node_uuid1', 'node_uuid2'] + def test_introspect_nodes_with_provide(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): + nodes = ['node1', 'node2'] argslist = nodes + ['--provide'] parsed_args = self.check_parser(self.cmd, argslist, [('node_uuids', nodes), ('provide', True)]) + tb.TripleoProvide.provide = mock.MagicMock() + mock_conn.return_value = mock_bm + mock_bm.baremetal = mock_bm + mock_bm.baremetal_introspection = mock_bm + mock_bm.baremetal.get_node.side_effect = [ + self.fake_baremetal_node, + self.fake_baremetal_node2] + self.cmd.take_action(parsed_args) - mock_playbook.assert_called_with( - workdir=mock.ANY, - playbook='cli-overcloud-node-provide.yaml', - inventory=mock.ANY, - playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, - verbosity=mock.ANY, - extra_vars={ - 'node_uuids': nodes - } + + tb.TripleoProvide.provide.assert_called_with( + nodes=nodes ) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_introspect_no_node_or_flag_specified(self, mock_playbook): + def test_introspect_no_node_or_flag_specified(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): self.assertRaises(test_utils.ParserException, self.check_parser, self.cmd, [], []) - @mock.patch('tripleoclient.utils.run_ansible_playbook', - autospec=True) - def test_introspect_uuids_and_all_both_specified(self, mock_playbook): + def test_introspect_uuids_and_all_both_specified(self, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook): argslist = ['node_id1', 'node_id2', '--all-manageable'] verifylist = [('node_uuids', ['node_id1', 'node_id2']), ('all_manageable', True)] @@ -267,7 +379,12 @@ class TestIntrospectNode(fakes.TestOvercloudNode): self.check_parser, self.cmd, argslist, verifylist) - def _check_introspect_all_manageable(self, parsed_args, provide=False): + def _check_introspect_all_manageable(self, parsed_args, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook, provide=False): self.cmd.take_action(parsed_args) call_list = [mock.call( @@ -285,7 +402,12 @@ class TestIntrospectNode(fakes.TestOvercloudNode): self.assertEqual(self.workflow.executions.create.call_count, 2 if provide else 1) - def _check_introspect_nodes(self, parsed_args, nodes, provide=False): + def _check_introspect_nodes(self, parsed_args, nodes, + mock_conn, + mock_connect, + mock_conf, + mock_bm, + mock_playbook, provide=False): self.cmd.take_action(parsed_args) call_list = [mock.call( diff --git a/tripleoclient/tests/workflows/test_baremetal.py b/tripleoclient/tests/workflows/test_baremetal.py index e18fe994a..85dfd55a9 100644 --- a/tripleoclient/tests/workflows/test_baremetal.py +++ b/tripleoclient/tests/workflows/test_baremetal.py @@ -96,9 +96,6 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution): instance_boot_option='local' ), [mock.ANY]) - def test_provide_success(self): - baremetal.provide(node_uuids=[]) - def test_introspect_success(self): baremetal.introspect(self.app.client_manager, node_uuids=[], run_validations=True, concurrency=20, @@ -111,11 +108,6 @@ class TestBaremetalWorkflows(fakes.FakePlaybookExecution): node_timeout=1200, max_retries=1, retry_timeout=120, ) - def test_provide_manageable_nodes_success(self): - baremetal.provide_manageable_nodes( - self.app.client_manager - ) - def test_configure_success(self): baremetal.configure(self.app.client_manager, node_uuids=[]) diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index 27ef4fd6a..33d15a0b8 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -34,6 +34,7 @@ from tripleoclient import command from tripleoclient import constants from tripleoclient import utils as oooutils from tripleoclient.workflows import baremetal +from tripleoclient.workflows import tripleo_baremetal as tb class DeleteNode(command.Command): @@ -214,19 +215,22 @@ class ProvideNode(command.Command): action='store_true', help=_("Provide all nodes currently in 'manageable'" " state")) + group.add_argument("--verbosity", + type=int, + default=1, + help=_("Print debug output during execution")) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) + provide = tb.TripleoProvide(verbosity=parsed_args.verbosity) + if parsed_args.node_uuids: - baremetal.provide(node_uuids=parsed_args.node_uuids, - verbosity=oooutils.playbook_verbosity(self)) + provide.provide(nodes=parsed_args.node_uuids) else: - baremetal.provide_manageable_nodes( - self.app.client_manager, - verbosity=oooutils.playbook_verbosity(self)) + provide.provide_manageable_nodes() class CleanNode(command.Command): @@ -247,6 +251,10 @@ class CleanNode(command.Command): action='store_true', help=_("Clean all nodes currently in 'manageable'" " state")) + group.add_argument("--verbosity", + type=int, + default=1, + help=_("Print debug output during execution")) parser.add_argument('--provide', action='store_true', help=_('Provide (make available) the nodes once ' @@ -270,11 +278,11 @@ class CleanNode(command.Command): ) if parsed_args.provide: + provide = tb.TripleoProvide(verbosity=parsed_args.verbosity) if nodes: - baremetal.provide(node_uuids=nodes, - verbosity=oooutils.playbook_verbosity(self)) + provide.provide(nodes=nodes) else: - baremetal.provide_manageable_nodes(self.app.client_manager) + provide.provide_manageable_nodes() class ConfigureNode(command.Command): @@ -410,6 +418,10 @@ class DiscoverNode(command.Command): default=120, help=_('Maximum timeout between introspection' 'retries')) + parser.add_argument("--verbosity", + type=int, + default=1, + help=_("Print debug output during execution")) return parser # FIXME(tonyb): This is not multi-arch safe :( @@ -457,9 +469,8 @@ class DiscoverNode(command.Command): ) if parsed_args.provide: - baremetal.provide( - node_uuids=nodes_uuids, - verbosity=oooutils.playbook_verbosity(self)) + provide = tb.TripleoProvide(verbosity=parsed_args.verbosity) + provide.provide(nodes=nodes_uuids) class ExtractProvisionedNode(command.Command): diff --git a/tripleoclient/v2/overcloud_node.py b/tripleoclient/v2/overcloud_node.py index 48c283d2b..e3bc70d4f 100644 --- a/tripleoclient/v2/overcloud_node.py +++ b/tripleoclient/v2/overcloud_node.py @@ -38,6 +38,7 @@ from tripleoclient.v1.overcloud_node import ConfigureNode # noqa from tripleoclient.v1.overcloud_node import DeleteNode # noqa from tripleoclient.v1.overcloud_node import DiscoverNode # noqa from tripleoclient.v1.overcloud_node import ProvideNode # noqa +from tripleoclient.workflows import tripleo_baremetal as tb class ImportNode(command.Command): @@ -87,6 +88,9 @@ class ImportNode(command.Command): default=20, help=_('Maximum number of nodes to introspect at ' 'once.')) + parser.add_argument('--verbosity', type=int, + default=1, + help=_('Print debug logs during execution')) parser.add_argument('env_file', type=argparse.FileType('r')) return parser @@ -132,10 +136,8 @@ class ImportNode(command.Command): ) if parsed_args.provide: - baremetal.provide( - verbosity=oooutils.playbook_verbosity(self=self), - node_uuids=nodes_uuids - ) + provide = tb.TripleoProvide(verbosity=parsed_args.verbosity) + provide.provide(nodes=nodes_uuids) class IntrospectNode(command.Command): @@ -179,6 +181,9 @@ class IntrospectNode(command.Command): default=120, help=_('Maximum timeout between introspection' 'retries')) + parser.add_argument('--verbosity', type=int, + default=1, + help=_('Print debug logs during execution')) return parser def take_action(self, parsed_args): @@ -209,16 +214,13 @@ class IntrospectNode(command.Command): # NOTE(cloudnull): This is using the old provide function, in a future # release this may be ported to a standalone playbook if parsed_args.provide: + provide = tb.TripleoProvide(verbosity=parsed_args.verbosity) if parsed_args.node_uuids: - baremetal.provide( - node_uuids=parsed_args.node_uuids, - verbosity=oooutils.playbook_verbosity(self=self) + provide.provide( + nodes=parsed_args.node_uuids, ) else: - baremetal.provide_manageable_nodes( - clients=self.app.client_manager, - verbosity=oooutils.playbook_verbosity(self=self) - ) + provide.provide_manageable_nodes() class ProvisionNode(command.Command): diff --git a/tripleoclient/workflows/baremetal.py b/tripleoclient/workflows/baremetal.py index feaf9544f..ba345a264 100644 --- a/tripleoclient/workflows/baremetal.py +++ b/tripleoclient/workflows/baremetal.py @@ -105,50 +105,6 @@ def register_or_update(clients, nodes_json, kernel_name=None, return registered_nodes -def provide(node_uuids, verbosity=0): - """Provide Baremetal Nodes - - :param node_uuids: List of instance UUID(s). - :type node_uuids: List - - :param verbosity: Verbosity level - :type verbosity: Integer - """ - - with utils.TempDirs() as tmp: - utils.run_ansible_playbook( - playbook='cli-overcloud-node-provide.yaml', - inventory='localhost,', - workdir=tmp, - playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, - verbosity=verbosity, - extra_vars={ - 'node_uuids': node_uuids - } - ) - - print('Successfully provided nodes: {}'.format(node_uuids)) - - -def provide_manageable_nodes(clients, verbosity=0): - """Provide all manageable Nodes - - :param clients: Application client object. - :type clients: Object - - :param verbosity: Verbosity level - :type verbosity: Integer - """ - - provide( - node_uuids=[ - i.uuid for i in clients.baremetal.node.list() - if i.provision_state == "manageable" and not i.maintenance - ], - verbosity=verbosity - ) - - def introspect(clients, node_uuids, run_validations, concurrency, node_timeout, max_retries, retry_timeout, verbosity=0): """Introspect Baremetal Nodes diff --git a/tripleoclient/workflows/tripleo_baremetal.py b/tripleoclient/workflows/tripleo_baremetal.py new file mode 100644 index 000000000..4f760556f --- /dev/null +++ b/tripleoclient/workflows/tripleo_baremetal.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# 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 + +from openstack import connect as sdkclient +from openstack import exceptions +from openstack.utils import iterate_timeout +from tripleoclient import exceptions as ooo_exceptions + + +class TripleoBaremetal(object): + + """Base class for TripleO Baremetal operations. + + The TripleoBase class provides access to commonly used elements + required to interact with and perform baremetal operations for TripleO. + + :param timeout: How long to wait until we consider this job to have + timed out + :type timeout: integer + + :param verbosity: How verbose should we be. Currently, this just sets + DEBUG for any non-zero value provided. + :type verbosity: integer + """ + + def __init__(self, timeout: int = 1200, verbosity: int = 1): + self.conn = sdkclient( + cloud='undercloud' + ) + self.timeout = timeout + self.log = logging.getLogger(__name__) + if verbosity > 0: + self.log.setLevel(logging.DEBUG) + + def all_manageable_nodes(self): + """This method returns a list of manageable nodes from Ironic + + We take no arguments and instead create a list of nodes that + are in the manageable state and NOT in maintenenace. We return the + subsequent list. + + Raises: + NoNodeFound: If no nodes match the above description, we will raise + an exception. + + Returns: + nodes: The List of manageable nodes that are not currently in + maintenance. + """ + nodes = [n.id for n in self.conn.baremetal.nodes( + provision_state='manageable', is_maintenance=False)] + + if not nodes: + raise ooo_exceptions.NoNodeFound + + return nodes + + +class TripleoProvide(TripleoBaremetal): + + """TripleoProvide handles state transition of baremetal nodes. + + The TripleoProvide class handles the transition of nodes between the + manageable and available states. + + :param wait_for_bridge_mapping: Bool to determine whether or not we are + waiting for the bridge mapping to be + active in ironic-neutron-agent + :type wait_for_bridge_mapping: bool + + """ + + def __init__(self, wait_for_bridge_mappings: bool = False, + verbosity: int = 1): + + super().__init__(verbosity) + self.wait_for_bridge_mappings = wait_for_bridge_mappings + + def _wait_for_unlocked(self, node: str, timeout: int): + timeout_msg = f'Timeout waiting for node {node} to be unlocked' + + for count in iterate_timeout(timeout, timeout_msg): + node_info = self.conn.baremetal.get_node( + node, + fields=['reservation'] + ) + + if node_info.reservation is None: + return + + def _wait_for_bridge_mapping(self, node: str): + + client = self.conn.network + timeout_msg = (f'Timeout waiting for node {node} to have ' + 'bridge_mappings set in the ironic-neutron-agent ' + 'entry') + + # default agent polling period is 30s, so wait 60s + timeout = 60 + + for count in iterate_timeout(timeout, timeout_msg): + agents = list( + client.agents(host=node, binary='ironic-neutron-agent')) + + if agents: + if agents[0].configuration.get('bridge_mappings'): + return + + def provide(self, nodes: str): + + """Transition nodes to the Available state. + + provide handles the state transition from the nodes current state + to the available state + + :param nodes: The node UUID or name that we will be working on + :type nodes: String + """ + + client = self.conn.baremetal + node_timeout = self.timeout + nodes_wait = nodes[:] + + for node in nodes: + self.log.info('Providing node: {}'.format(node)) + self._wait_for_unlocked(node, node_timeout) + + if self.wait_for_bridge_mappings: + self._wait_for_bridge_mapping(node) + + try: + client.set_node_provision_state( + node, + "provide", + wait=False) + + except Exception as e: + nodes_wait.remove(node) + self.log.error( + "Can not start providing for node {}: {}".format( + nodes, e)) + return + + try: + self.log.info( + "Waiting for available state: {}".format(nodes_wait)) + + client.wait_for_nodes_provision_state( + nodes=nodes_wait, + expected_state='available', + timeout=self.timeout, + fail=False + ) + + except exceptions.ResourceFailure as e: + self.log.error("Failed providing nodes due to failure: {}".format( + e)) + return + + except exceptions.ResourceTimeout as e: + self.log.error("Failed providing nodes due to timeout: {}".format( + e)) + + def provide_manageable_nodes(self): + self.provide(self.all_manageable_nodes())