From 51b0cc4c13f9960e087b34e9cd15fe09ecebc722 Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Thu, 1 Aug 2019 03:58:31 +0000 Subject: [PATCH] New command "overcloud node extract provisioned" As a first step to upgrade to a nova-less undercloud, the baremetal deployment yaml which represents the current overcloud needs to be built. This command builds the yaml for an existing overcloud using the heat stack output AnsibleHostVarsMap and a list of all baremetal nodes. When the generated yaml is used to run "openstack overcloud node provision" no changes will be made to any nodes. Blueprint: nova-less-deploy Change-Id: I47c885697bf36b999fd0ad9bd99a23990440ed62 --- setup.cfg | 1 + .../v1/overcloud_node/test_overcloud_node.py | 120 ++++++++++++++++++ tripleoclient/v1/overcloud_node.py | 86 +++++++++++++ 3 files changed, 207 insertions(+) diff --git a/setup.cfg b/setup.cfg index 3d97138ba..33d31d179 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,6 +76,7 @@ openstack.tripleoclient.v2 = overcloud_node_bios_reset = tripleoclient.v1.overcloud_bios:ResetBIOS overcloud_node_provision = tripleoclient.v2.overcloud_node:ProvisionNode overcloud_node_unprovision = tripleoclient.v2.overcloud_node:UnprovisionNode + overcloud_node_extract_provisioned = tripleoclient.v1.overcloud_node:ExtractProvisionedNode overcloud_parameters_set = tripleoclient.v1.overcloud_parameters:SetParameters overcloud_plan_create = tripleoclient.v1.overcloud_plan:CreatePlan overcloud_plan_delete = tripleoclient.v1.overcloud_plan:DeletePlan diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index 52aa52af7..8bc96c91f 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -749,3 +749,123 @@ class TestDiscoverNode(fakes.TestOvercloudNode): parsed_args = self.check_parser(self.cmd, argslist, verifylist) self.cmd.take_action(parsed_args) + + +class TestExtractProvisionedNode(test_utils.TestCommand): + + def setUp(self): + super(TestExtractProvisionedNode, self).setUp() + + self.orchestration = mock.Mock() + self.app.client_manager.orchestration = self.orchestration + + self.baremetal = mock.Mock() + self.app.client_manager.baremetal = self.baremetal + + self.cmd = overcloud_node.ExtractProvisionedNode(self.app, None) + + self.extract_file = tempfile.NamedTemporaryFile( + mode='w', delete=False, suffix='.yaml') + self.extract_file.close() + self.addCleanup(os.unlink, self.extract_file.name) + + def test_extract(self): + stack_dict = { + 'parameters': { + 'ComputeHostnameFormat': '%stackname%-novacompute-%index%', + 'ControllerHostnameFormat': '%stackname%-controller-%index%' + }, + 'outputs': [{ + 'output_key': 'AnsibleHostVarsMap', + 'output_value': { + 'Compute': [ + 'overcloud-novacompute-0' + ], + 'Controller': [ + 'overcloud-controller-0', + 'overcloud-controller-1', + 'overcloud-controller-2' + ], + } + }] + } + stack = mock.Mock() + stack.to_dict.return_value = stack_dict + self.orchestration.stacks.get.return_value = stack + + nodes = [ + mock.Mock(), + mock.Mock(), + mock.Mock(), + mock.Mock() + ] + nodes[0].name = 'bm-0' + nodes[1].name = 'bm-1' + nodes[2].name = 'bm-2' + nodes[3].name = 'bm-3' + + nodes[0].instance_info = {'display_name': 'overcloud-controller-0'} + nodes[1].instance_info = {'display_name': 'overcloud-controller-1'} + nodes[2].instance_info = {'display_name': 'overcloud-controller-2'} + nodes[3].instance_info = {'display_name': 'overcloud-novacompute-0'} + + self.baremetal.node.list.return_value = nodes + + argslist = ['--output', self.extract_file.name, '--yes'] + self.app.command_options = argslist + verifylist = [('output', self.extract_file.name), ('yes', True)] + + parsed_args = self.check_parser(self.cmd, + argslist, verifylist) + self.cmd.take_action(parsed_args) + + result = self.cmd.app.stdout.make_string() + self.assertEqual([{ + 'name': 'Compute', + 'count': 1, + 'hostname_format': '%stackname%-novacompute-%index%', + 'instances': [{ + 'hostname': 'overcloud-novacompute-0', + 'name': 'bm-3' + }], + }, { + 'name': 'Controller', + 'count': 3, + 'hostname_format': '%stackname%-controller-%index%', + 'instances': [{ + 'hostname': 'overcloud-controller-0', + 'name': 'bm-0' + }, { + 'hostname': 'overcloud-controller-1', + 'name': 'bm-1' + }, { + 'hostname': 'overcloud-controller-2', + 'name': 'bm-2' + }], + }], yaml.safe_load(result)) + + with open(self.extract_file.name) as f: + self.assertEqual(yaml.safe_load(result), yaml.safe_load(f)) + + def test_extract_empty(self): + stack_dict = { + 'parameters': {}, + 'outputs': [] + } + stack = mock.Mock() + stack.to_dict.return_value = stack_dict + self.orchestration.stacks.get.return_value = stack + + nodes = [] + + self.baremetal.node.list.return_value = nodes + + argslist = [] + self.app.command_options = argslist + verifylist = [] + + parsed_args = self.check_parser(self.cmd, + argslist, verifylist) + self.cmd.take_action(parsed_args) + result = self.cmd.app.stdout.make_string() + self.assertIsNone(yaml.safe_load(result)) diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index ffbc7eb52..2f60eedb7 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -14,9 +14,11 @@ # import collections +import datetime import json import logging import os +import sys from cliff.formatters import table from osc_lib import exceptions as oscexc @@ -439,3 +441,87 @@ class DiscoverNode(command.Command): baremetal.provide(self.app.client_manager, node_uuids=nodes_uuids ) + + +class ExtractProvisionedNode(command.Command): + + log = logging.getLogger(__name__ + ".ExtractProvisionedNode") + + def _setup_clients(self): + self.clients = self.app.client_manager + self.orchestration_client = self.clients.orchestration + self.baremetal_client = self.clients.baremetal + + def get_parser(self, prog_name): + parser = super(ExtractProvisionedNode, self).get_parser(prog_name) + parser.add_argument('--stack', dest='stack', + help=_('Name or ID of heat stack ' + '(default=Env: OVERCLOUD_STACK_NAME)'), + default=utils.env('OVERCLOUD_STACK_NAME', + default='overcloud')) + parser.add_argument('-o', '--output', + metavar='', + help=_('The output file path describing the ' + 'baremetal deployment')) + parser.add_argument('-y', '--yes', default=False, action='store_true', + help=_('Skip yes/no prompt for existing files ' + '(assume yes).')) + return parser + + def take_action(self, parsed_args): + self.log.debug("take_action(%s)" % parsed_args) + self._setup_clients() + stack = oooutils.get_stack(self.orchestration_client, + parsed_args.stack) + host_vars = oooutils.get_stack_output_item( + stack, 'AnsibleHostVarsMap') or {} + parameters = stack.to_dict().get('parameters', {}) + + # list all baremetal nodes and map hostname to node name + node_details = self.baremetal_client.node.list(detail=True) + hostname_node_map = {} + for node in node_details: + hostname = node.instance_info.get('display_name') + if hostname and node.name: + hostname_node_map[hostname] = node.name + + role_data = six.StringIO() + role_data.write('# Generated with the following on %s\n#\n' % + datetime.datetime.now().isoformat()) + role_data.write('# openstack %s\n#\n\n' % + ' '.join(self.app.command_options)) + for role, entries in host_vars.items(): + role_count = len(entries) + + # skip zero count roles + if not role_count: + continue + + role_data.write('- name: %s\n' % role) + role_data.write(' count: %s\n' % role_count) + + hostname_format = parameters.get('%sHostnameFormat' % role) + if hostname_format: + role_data.write(' hostname_format: "%s"\n' % hostname_format) + + role_data.write(' instances:\n') + + for entry in sorted(entries): + role_data.write(' - hostname: %s\n' % entry) + if entry in hostname_node_map: + role_data.write(' name: %s\n' % + hostname_node_map[entry]) + + if parsed_args.output: + if (os.path.exists(parsed_args.output) + and not parsed_args.yes and sys.stdin.isatty()): + prompt_response = six.moves.input( + ('Overwrite existing file %s [y/N]?' % parsed_args.output) + ).lower() + if not prompt_response.startswith('y'): + raise oscexc.CommandError( + "Will not overwrite existing file:" + " %s" % parsed_args.output) + with open(parsed_args.output, 'w+') as fp: + fp.write(role_data.getvalue()) + self.app.stdout.write(role_data.getvalue())