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())