From 818c7a7a1a961b68658daebf0c736fbb17bd74b5 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Tue, 21 Jan 2020 13:16:27 -0600 Subject: [PATCH] Start moving the import CLI command to the Ansible implementation Change-Id: I2285b23282a275097512dc35628cb7fa8ac94d61 Signed-off-by: Kevin Carter --- setup.cfg | 2 +- .../v2/overcloud_node/test_overcloud_node.py | 150 ++++++++++++++++++ tripleoclient/v2/overcloud_node.py | 95 ++++++++++- 3 files changed, 245 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 569f8493b..6b13419c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,7 +70,7 @@ openstack.tripleoclient.v2 = overcloud_image_upload = tripleoclient.v1.overcloud_image:UploadOvercloudImage overcloud_node_configure = tripleoclient.v1.overcloud_node:ConfigureNode overcloud_node_delete = tripleoclient.v1.overcloud_node:DeleteNode - overcloud_node_import = tripleoclient.v1.overcloud_node:ImportNode + overcloud_node_import = tripleoclient.v2.overcloud_node:ImportNode overcloud_node_introspect = tripleoclient.v2.overcloud_node:IntrospectNode overcloud_node_provide = tripleoclient.v1.overcloud_node:ProvideNode overcloud_node_discover = tripleoclient.v1.overcloud_node:DiscoverNode diff --git a/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py index 5bcb367c1..9f2541011 100644 --- a/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v2/overcloud_node/test_overcloud_node.py @@ -13,7 +13,12 @@ # under the License. # +import collections +import fixtures +import json import mock +import os +import tempfile from osc_lib.tests import utils as test_utils @@ -22,6 +27,151 @@ from tripleoclient.tests.v2.overcloud_node import fakes from tripleoclient.v2 import overcloud_node +class TestImportNode(fakes.TestOvercloudNode): + + def setUp(self): + super(TestImportNode, self).setUp() + + self.nodes_list = [{ + "pm_user": "stack", + "pm_addr": "192.168.122.1", + "pm_password": "KEY1", + "pm_type": "pxe_ssh", + "mac": [ + "00:0b:d0:69:7e:59" + ], + }, { + "pm_user": "stack", + "pm_addr": "192.168.122.2", + "pm_password": "KEY2", + "pm_type": "pxe_ssh", + "mac": [ + "00:0b:d0:69:7e:58" + ] + }] + + # NOTE(cloudnull): Workflow and client calls are still mocked because + # mistal is still presnet here. + self.workflow = self.app.client_manager.workflow_engine + execution = mock.Mock() + execution.id = "IDID" + self.workflow.executions.create.return_value = execution + client = self.app.client_manager.tripleoclient + self.websocket = client.messaging_websocket() + self.websocket.wait_for_messages.return_value = [{ + "status": "SUCCESS", + "message": "Success", + "registered_nodes": [{ + "uuid": "MOCK_NODE_UUID" + }], + "execution_id": execution.id + }] + + self.json_file = tempfile.NamedTemporaryFile( + mode='w', delete=False, suffix='.json') + json.dump(self.nodes_list, self.json_file) + self.json_file.close() + self.addCleanup(os.unlink, self.json_file.name) + + # Get the command object to test + self.cmd = overcloud_node.ImportNode(self.app, None) + + image = collections.namedtuple('image', ['id', 'name']) + self.app.client_manager.image = mock.Mock() + self.app.client_manager.image.images.list.return_value = [ + image(id=3, name='overcloud-full'), + ] + + self.http_boot = '/var/lib/ironic/httpboot' + + self.useFixture(fixtures.MockPatch( + 'os.path.exists', autospec=True, + side_effect=lambda path: path in [os.path.join(self.http_boot, i) + for i in ('agent.kernel', + 'agent.ramdisk')])) + + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_import_only(self, 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): + parsed_args = self.check_parser(self.cmd, + [self.json_file.name, + '--introspect'], + [('introspect', True), + ('provide', False)]) + self.cmd.take_action(parsed_args) + mock_playbook.assert_called_once_with( + workdir=mock.ANY, + playbook=mock.ANY, + inventory=mock.ANY, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + extra_vars={ + 'node_uuids': ['MOCK_NODE_UUID'], + 'run_validations': False, + 'concurrency': 20 + } + ) + + @mock.patch('tripleoclient.utils.run_ansible_playbook', + autospec=True) + def test_import_and_provide(self, mock_playbook): + parsed_args = self.check_parser(self.cmd, + [self.json_file.name, + '--provide'], + [('introspect', False), + ('provide', True)]) + 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): + parsed_args = self.check_parser(self.cmd, + [self.json_file.name, + '--introspect', + '--provide'], + [('introspect', True), + ('provide', True)]) + self.cmd.take_action(parsed_args) + mock_playbook.assert_called_once_with( + workdir=mock.ANY, + playbook=mock.ANY, + inventory=mock.ANY, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + extra_vars={ + '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): + 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): + parsed_args = self.check_parser(self.cmd, + [self.json_file.name, + '--instance-boot-option', + 'netboot'], + [('instance_boot_option', 'netboot')]) + self.cmd.take_action(parsed_args) + + class TestIntrospectNode(fakes.TestOvercloudNode): def setUp(self): diff --git a/tripleoclient/v2/overcloud_node.py b/tripleoclient/v2/overcloud_node.py index a06d3af82..01b3b2fac 100644 --- a/tripleoclient/v2/overcloud_node.py +++ b/tripleoclient/v2/overcloud_node.py @@ -13,7 +13,9 @@ # under the License. # +import argparse import logging +import os from osc_lib.i18n import _ @@ -28,12 +30,103 @@ from tripleoclient.v1.overcloud_node import CleanNode # noqa 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 ImportNode # noqa from tripleoclient.v1.overcloud_node import ProvideNode # noqa from tripleoclient.v1.overcloud_node import ProvisionNode # noqa from tripleoclient.v1.overcloud_node import UnprovisionNode # noqa +class ImportNode(command.Command): + """Import baremetal nodes from a JSON, YAML or CSV file. + + The node status will be set to 'manageable' by default. + """ + + log = logging.getLogger(__name__ + ".ImportNode") + + def get_parser(self, prog_name): + parser = super(ImportNode, self).get_parser(prog_name) + parser.add_argument('--introspect', + action='store_true', + help=_('Introspect the imported nodes')) + parser.add_argument('--run-validations', action='store_true', + default=False, + help=_('Run the pre-deployment validations. These' + ' external validations are from the' + ' TripleO Validations project.')) + parser.add_argument('--validate-only', action='store_true', + default=False, + help=_('Validate the env_file and then exit ' + 'without actually importing the nodes.')) + parser.add_argument('--provide', + action='store_true', + help=_('Provide (make available) the nodes')) + parser.add_argument('--no-deploy-image', action='store_true', + help=_('Skip setting the deploy kernel and ' + 'ramdisk.')) + parser.add_argument('--instance-boot-option', + choices=['local', 'netboot'], default=None, + help=_('Whether to set instances for booting from' + ' local hard drive (local) or network ' + ' (netboot).')) + parser.add_argument("--http-boot", + default=os.environ.get( + 'HTTP_BOOT', + constants.IRONIC_HTTP_BOOT_BIND_MOUNT), + help=_("Root directory for the " + " ironic-python-agent image")) + parser.add_argument('--concurrency', type=int, + default=20, + help=_('Maximum number of nodes to introspect at ' + 'once.')) + parser.add_argument('env_file', type=argparse.FileType('r')) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + + nodes_config = oooutils.parse_env_file(parsed_args.env_file) + parsed_args.env_file.close() + + if parsed_args.validate_only: + return baremetal.validate_nodes(self.app.client_manager, + nodes_json=nodes_config) + + # Look for *specific* deploy images and update the node data if + # one is found. + if not parsed_args.no_deploy_image: + oooutils.update_nodes_deploy_data(nodes_config, + http_boot=parsed_args.http_boot) + nodes = baremetal.register_or_update( + self.app.client_manager, + nodes_json=nodes_config, + instance_boot_option=parsed_args.instance_boot_option + ) + + nodes_uuids = [node['uuid'] for node in nodes] + + if parsed_args.introspect: + extra_vars = { + "node_uuids": nodes_uuids, + "run_validations": parsed_args.run_validations, + "concurrency": parsed_args.concurrency, + } + + with oooutils.TempDirs() as tmp: + oooutils.run_ansible_playbook( + playbook='cli-baremetal-introspect.yaml', + inventory='localhost,', + workdir=tmp, + playbook_dir=constants.ANSIBLE_TRIPLEO_PLAYBOOKS, + extra_vars=extra_vars + ) + + if parsed_args.provide: + baremetal.provide( + self.app.client_manager, + node_uuids=nodes_uuids + ) + + class IntrospectNode(command.Command): """Introspect specified nodes or all nodes in 'manageable' state."""