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