diff --git a/setup.cfg b/setup.cfg index c7aecdbf2..adf25dfb2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -67,6 +67,7 @@ openstack.tripleoclient.v1 = overcloud_image_build = tripleoclient.v1.overcloud_image:BuildOvercloudImage overcloud_image_upload = tripleoclient.v1.overcloud_image:UploadOvercloudImage overcloud_node_delete = tripleoclient.v1.overcloud_node:DeleteNode + overcloud_node_import = tripleoclient.v1.overcloud_node:ImportNode overcloud_node_introspect = tripleoclient.v1.overcloud_node:IntrospectNode overcloud_node_provide = tripleoclient.v1.overcloud_node:ProvideNode overcloud_plan_list = tripleoclient.v1.overcloud_plan:ListPlans diff --git a/tripleoclient/tests/v1/baremetal/test_baremetal.py b/tripleoclient/tests/v1/baremetal/test_baremetal.py index 3deea1c7d..604843782 100644 --- a/tripleoclient/tests/v1/baremetal/test_baremetal.py +++ b/tripleoclient/tests/v1/baremetal/test_baremetal.py @@ -13,7 +13,6 @@ # under the License. # -import copy import json import os import tempfile @@ -369,19 +368,15 @@ pxe_ssh,192.168.122.2,stack,"KEY2",00:0b:d0:69:7e:58""") def _check_workflow_call(self, local=True, provide=True, kernel_name='bm-deploy-kernel', ramdisk_name='bm-deploy-ramdisk'): - nodes_list = copy.deepcopy(self.nodes_list) - for node in nodes_list: - if local: - node['capabilities'] = {'boot_option': 'local'} - else: - node['capabilities'] = {'boot_option': 'netboot'} call_list = [mock.call( 'tripleo.baremetal.v1.register_or_update', workflow_input={ 'kernel_name': kernel_name, - 'nodes_json': nodes_list, + 'nodes_json': self.nodes_list, 'queue_name': 'UUID4', - 'ramdisk_name': ramdisk_name} + 'ramdisk_name': ramdisk_name, + 'instance_boot_option': 'local' if local else 'netboot' + } )] if provide: diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index 70a068712..775c9b87c 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -13,11 +13,14 @@ # under the License. # +import copy +import json import mock +import os +import tempfile from openstackclient.tests import utils as test_utils - from tripleoclient.tests.v1.overcloud_node import fakes from tripleoclient.v1 import overcloud_node @@ -234,3 +237,133 @@ class TestIntrospectNode(fakes.TestOvercloudNode): self.assertRaises(test_utils.ParserException, self.check_parser, self.cmd, argslist, verifylist) + + +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" + ] + }] + + 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) + + self.workflow = self.app.client_manager.workflow_engine + client = self.app.client_manager.tripleoclient + self.websocket = client.messaging_websocket() + + # Get the command object to test + self.cmd = overcloud_node.ImportNode(self.app, None) + + def _check_workflow_call(self, parsed_args, introspect=False, + provide=False, local=True, no_deploy_image=False): + self.websocket.wait_for_message.return_value = { + "status": "SUCCESS", + "message": "Success", + "registered_nodes": [{ + "uuid": "MOCK_NODE_UUID" + }] + } + + self.cmd.take_action(parsed_args) + + nodes_list = copy.deepcopy(self.nodes_list) + + call_count = 1 + call_list = [mock.call( + 'tripleo.baremetal.v1.register_or_update', workflow_input={ + 'nodes_json': nodes_list, + 'queue_name': 'UUID4', + 'kernel_name': None if no_deploy_image else 'bm-deploy-kernel', + 'ramdisk_name': (None + if no_deploy_image else 'bm-deploy-ramdisk'), + 'instance_boot_option': 'local' if local else 'netboot' + } + )] + + if introspect: + call_count += 1 + call_list.append(mock.call( + 'tripleo.baremetal.v1.introspect', workflow_input={ + 'node_uuids': ['MOCK_NODE_UUID'], + 'queue_name': 'UUID4'} + )) + + if provide: + call_count += 1 + call_list.append(mock.call( + 'tripleo.baremetal.v1.provide', workflow_input={ + 'node_uuids': ['MOCK_NODE_UUID'], + 'queue_name': 'UUID4' + } + )) + + self.workflow.executions.create.assert_has_calls(call_list) + self.assertEqual(self.workflow.executions.create.call_count, + call_count) + + def test_import_only(self): + argslist = [self.json_file.name] + verifylist = [('introspect', False), + ('provide', False)] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + self._check_workflow_call(parsed_args) + + def test_import_and_introspect(self): + argslist = [self.json_file.name, '--introspect'] + verifylist = [('introspect', True), + ('provide', False)] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + self._check_workflow_call(parsed_args, introspect=True) + + def test_import_and_provide(self): + argslist = [self.json_file.name, '--provide'] + verifylist = [('introspect', False), + ('provide', True)] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + self._check_workflow_call(parsed_args, provide=True) + + def test_import_and_introspect_and_provide(self): + argslist = [self.json_file.name, '--introspect', '--provide'] + verifylist = [('introspect', True), + ('provide', True)] + + parsed_args = self.check_parser(self.cmd, argslist, verifylist) + self._check_workflow_call(parsed_args, introspect=True, provide=True) + + def test_import_with_netboot(self): + arglist = [self.json_file.name, '--instance-boot-option', 'netboot'] + verifylist = [('instance_boot_option', 'netboot')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self._check_workflow_call(parsed_args, local=False) + + def test_import_with_no_deployed_image(self): + arglist = [self.json_file.name, '--no-deploy-image'] + verifylist = [('no_deploy_image', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self._check_workflow_call(parsed_args, no_deploy_image=True) diff --git a/tripleoclient/utils.py b/tripleoclient/utils.py index 8a182b297..f63661f32 100644 --- a/tripleoclient/utils.py +++ b/tripleoclient/utils.py @@ -15,6 +15,7 @@ from __future__ import print_function import base64 +import csv import hashlib import json import logging @@ -26,6 +27,7 @@ import socket import struct import subprocess import time +import yaml from heatclient.common import event_utils from heatclient.exc import HTTPNotFound @@ -837,3 +839,47 @@ def get_roles_info(parsed_args): 'swift-storage': (parsed_args.swift_storage_flavor, parsed_args.swift_storage_scale) } + + +def _csv_to_nodes_dict(nodes_csv): + """Convert CSV to a list of dicts formatted for os_cloud_config + + Given a CSV file in the format below, convert it into the + structure expected by os_cloud_config JSON files. + + pm_type, pm_addr, pm_user, pm_password, mac + """ + + data = [] + + for row in csv.reader(nodes_csv): + node = { + "pm_user": row[2], + "pm_addr": row[1], + "pm_password": row[3], + "pm_type": row[0], + "mac": [ + row[4] + ] + } + data.append(node) + + return data + + +def parse_env_file(env_file, file_type=None): + if file_type == 'json' or env_file.name.endswith('.json'): + nodes_config = json.load(env_file) + elif file_type == 'csv' or env_file.name.endswith('.csv'): + nodes_config = _csv_to_nodes_dict(env_file) + elif env_file.name.endswith('.yaml'): + nodes_config = yaml.safe_load(env_file) + else: + raise exceptions.InvalidConfiguration( + _("Invalid file extension for %s, must be json, yaml or csv") % + env_file.name) + + if 'nodes' in nodes_config: + nodes_config = nodes_config['nodes'] + + return nodes_config diff --git a/tripleoclient/v1/baremetal.py b/tripleoclient/v1/baremetal.py index e2b739f55..a6e28b9a4 100644 --- a/tripleoclient/v1/baremetal.py +++ b/tripleoclient/v1/baremetal.py @@ -16,12 +16,10 @@ from __future__ import print_function import argparse -import csv import json import logging import time import uuid -import yaml from cliff import command from cliff import lister @@ -35,32 +33,6 @@ from tripleoclient import utils from tripleoclient.workflows import baremetal -def _csv_to_nodes_dict(nodes_csv): - """Convert CSV to a list of dicts formatted for os_cloud_config - - Given a CSV file in the format below, convert it into the - structure expected by os_cloud_config JSON files. - - pm_type, pm_addr, pm_user, pm_password, mac - """ - - data = [] - - for row in csv.reader(nodes_csv): - node = { - "pm_user": row[2], - "pm_addr": row[1], - "pm_password": row[3], - "pm_type": row[0], - "mac": [ - row[4] - ] - } - data.append(node) - - return data - - class ValidateInstackEnv(command.Command): """Validate `instackenv.json` which is used in `baremetal import`.""" @@ -188,19 +160,13 @@ class ImportBaremetal(command.Command): self.log.debug("take_action(%s)" % parsed_args) - if parsed_args.json or parsed_args.file_in.name.endswith('.json'): - nodes_config = json.load(parsed_args.file_in) - elif parsed_args.csv or parsed_args.file_in.name.endswith('.csv'): - nodes_config = _csv_to_nodes_dict(parsed_args.file_in) - elif parsed_args.file_in.name.endswith('.yaml'): - nodes_config = yaml.safe_load(parsed_args.file_in) - else: - raise exceptions.InvalidConfiguration( - _("Invalid file extension for %s, must be json, yaml or csv") % - parsed_args.file_in.name) + file_type = None + if parsed_args.json: + file_type = 'json' + elif parsed_args.csv: + file_type = 'csv' - if 'nodes' in nodes_config: - nodes_config = nodes_config['nodes'] + nodes_config = utils.parse_env_file(parsed_args.file_in, file_type) client = self.app.client_manager.baremetal if parsed_args.initial_state == "enroll": @@ -210,14 +176,6 @@ class ImportBaremetal(command.Command): _("OS_BAREMETAL_API_VERSION must be >=1.11 for use of " "'enroll' provision state; currently %s") % api_version) - # NOTE (dprince) move this to tripleo-common? - for node in nodes_config: - caps = node.get('capabilities', {}) - if not isinstance(caps, dict): - caps = utils.capabilities_to_dict(caps) - caps.setdefault('boot_option', parsed_args.instance_boot_option) - node['capabilities'] = caps - queue_name = str(uuid.uuid4()) if parsed_args.no_deploy_image: @@ -232,7 +190,8 @@ class ImportBaremetal(command.Command): nodes_json=nodes_config, queue_name=queue_name, kernel_name=deploy_kernel, - ramdisk_name=deploy_ramdisk + ramdisk_name=deploy_ramdisk, + instance_boot_option=parsed_args.instance_boot_option ) node_uuids = [node['uuid'] for node in nodes] diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index 9d0025a26..c742c642c 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -13,6 +13,7 @@ # under the License. # +import argparse import logging import uuid @@ -22,6 +23,7 @@ from openstackclient.i18n import _ from tripleo_common import scale from tripleoclient import constants +from tripleoclient import utils as oooutils from tripleoclient.workflows import baremetal @@ -146,3 +148,66 @@ class IntrospectNode(command.Command): else: baremetal.provide_manageable_nodes(self.app.client_manager, queue_name=queue_name) + + +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('--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='local', + help=_('Whether to set instances for booting from ' + 'local hard drive (local) or network ' + '(netboot).')) + 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) + + queue_name = str(uuid.uuid4()) + + if parsed_args.no_deploy_image: + deploy_kernel = None + deploy_ramdisk = None + else: + deploy_kernel = 'bm-deploy-kernel' + deploy_ramdisk = 'bm-deploy-ramdisk' + + nodes = baremetal.register_or_update( + self.app.client_manager, + nodes_json=nodes_config, + queue_name=queue_name, + kernel_name=deploy_kernel, + ramdisk_name=deploy_ramdisk, + instance_boot_option=parsed_args.instance_boot_option + ) + + nodes_uuids = [node['uuid'] for node in nodes] + + if parsed_args.introspect: + baremetal.introspect(self.app.client_manager, + node_uuids=nodes_uuids, + queue_name=queue_name) + + if parsed_args.provide: + baremetal.provide(self.app.client_manager, + node_uuids=nodes_uuids, + queue_name=queue_name)