From 4864bdda8529a1f2f125adfdba3f59220474dc56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Fri, 21 Jan 2022 22:23:28 +0100 Subject: [PATCH] baremetal node export - auto convert Heat nic-conf If no NetworkConfigTemplate parameter is set when exporting overcloud nodes to baremetal_deployment.yaml an attempt to look for a legacy Heat nic-config template is made. In case a heat nic-config template is found it will be downloaded from the stack to a temporary file. The tool $THT/tools/convert_heat_nic_config_to_ansible_j2.py is then used to convert the template to an ansible j2 nic-config template. The template created by the conversion tool is placed in the working-dir and the baremetal_deployment.yaml network_config -> template parameter will point to the converted template. Co-Authored-By: Sergii Golovatiuk Change-Id: Idbb2345c2b94370bbdc455a96869fcf85c5edfc7 (cherry picked from commit f965bc4f54bc8eb33be519fc9d814e2ffa27fce6) --- .../v1/overcloud_node/test_overcloud_node.py | 129 +++++++++++++++++- tripleoclient/v1/overcloud_node.py | 95 ++++++++++++- 2 files changed, 217 insertions(+), 7 deletions(-) diff --git a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py index 7a913d0b3..fdfd62a78 100644 --- a/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py +++ b/tripleoclient/tests/v1/overcloud_node/test_overcloud_node.py @@ -754,6 +754,8 @@ class TestExtractProvisionedNode(test_utils.TestCommand): {'name': 'Compute'} ] + networks_data = [] + self.stack_dict = { 'parameters': { 'ComputeHostnameFormat': '%stackname%-novacompute-%index%', @@ -764,7 +766,7 @@ class TestExtractProvisionedNode(test_utils.TestCommand): 'output_key': 'TripleoHeatTemplatesJinja2RenderingDataSources', 'output_value': { 'roles_data': roles_data, - 'networks_data': {} + 'networks_data': networks_data, } }, { 'output_key': 'AnsibleHostVarsMap', @@ -905,8 +907,17 @@ class TestExtractProvisionedNode(test_utils.TestCommand): mode='w', delete=False, suffix='.yaml') self.roles_file.write(yaml.safe_dump(roles_data)) self.roles_file.close() + + self.networks_file = tempfile.NamedTemporaryFile( + mode='w', delete=False, suffix='.yaml') + self.networks_file.write(yaml.safe_dump(networks_data)) + self.networks_file.close() + + self.working_dir = tempfile.TemporaryDirectory() + self.addCleanup(os.unlink, self.extract_file.name) self.addCleanup(os.unlink, self.roles_file.name) + self.addCleanup(os.unlink, self.networks_file.name) def test_extract(self): stack = mock.Mock() @@ -999,10 +1010,12 @@ class TestExtractProvisionedNode(test_utils.TestCommand): self.baremetal.node.list.return_value = self.nodes argslist = ['--roles-file', self.roles_file.name, + '--networks-file', self.networks_file.name, '--output', self.extract_file.name, '--yes'] self.app.command_options = argslist verifylist = [('roles_file', self.roles_file.name), + ('networks_file', self.networks_file.name), ('output', self.extract_file.name), ('yes', True)] @@ -1098,6 +1111,114 @@ class TestExtractProvisionedNode(test_utils.TestCommand): with open(self.extract_file.name) as f: self.assertEqual(yaml.safe_load(result), yaml.safe_load(f)) + @mock.patch('tripleoclient.utils.run_command_and_log', autospec=True) + def test_extract_convert_nic_configs(self, mock_run_cmd): + stack = mock.Mock() + stack.stack_name = 'overcloud' + stack.to_dict.return_value = self.stack_dict + stack.files.return_value = { + 'https://1.1.1.1:13808/v1/AUTH_xx/overcloud/user-files/' + 'home/stack/overcloud/compute-net-config.yaml': 'FAKE_CONTENT'} + stack.environment.return_value = { + 'resource_registry': { + 'OS::TripleO::Compute::Net::SoftwareConfig': + 'https://1.1.1.1:13808/v1/AUTH_xx/overcloud/user-files/' + 'home/stack/overcloud/compute-net-config.yaml' + } + } + self.orchestration.stacks.get.return_value = stack + + self.baremetal.node.list.return_value = self.nodes + + mock_run_cmd.return_value = 0 + + argslist = ['--output', self.extract_file.name, + '--working-dir', self.working_dir.name, + '--yes'] + self.app.command_options = argslist + verifylist = [('output', self.extract_file.name), + ('working_dir', self.working_dir.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() + heat_nic_conf_path = os.path.join(self.working_dir.name, + 'nic-configs', + 'compute-net-config.yaml') + cmd = ['/usr/share/openstack-tripleo-heat-templates/tools/' + 'convert_heat_nic_config_to_ansible_j2.py', + '--yes', + '--stack', stack.stack_name, + '--networks_file', mock.ANY, + heat_nic_conf_path] + mock_run_cmd.assert_called_once_with(mock.ANY, cmd) + self.assertEqual([{ + 'name': 'Compute', + 'count': 1, + 'hostname_format': '%stackname%-novacompute-%index%', + 'defaults': { + 'network_config': {'network_config_update': False, + 'physical_bridge_name': 'br-ex', + 'public_interface_name': 'nic1', + 'template': + os.path.join(self.working_dir.name, + 'nic-configs', + 'compute-net-config.j2')}, + 'networks': [{'network': 'ctlplane', + 'vif': True}, + {'network': 'internal_api', + 'subnet': 'internal_api_b'}] + }, + 'instances': [{ + 'hostname': 'overcloud-novacompute-0', + 'resource_class': 'compute', + 'name': 'bm-3' + }], + }, { + 'name': 'Controller', + 'count': 3, + 'hostname_format': '%stackname%-controller-%index%', + 'defaults': { + 'network_config': {'default_route_network': ['External'], + 'network_config_update': False, + 'networks_skip_config': ['Tenant'], + 'physical_bridge_name': 'br-ex', + 'public_interface_name': 'nic1', + 'template': 'templates/controller.j2'}, + 'networks': [{'network': 'ctlplane', + 'vif': True}, + {'network': 'external', + 'subnet': 'external_a'}, + {'network': 'internal_api', + 'subnet': 'internal_api_a'}] + }, + 'instances': [{ + 'hostname': 'overcloud-controller-0', + 'resource_class': 'controller', + 'name': 'bm-0' + }, { + 'hostname': 'overcloud-controller-1', + 'resource_class': 'controller', + 'name': 'bm-1' + }, { + 'hostname': 'overcloud-controller-2', + 'name': 'bm-2' + }], + }], yaml.safe_load(result)) + + with open(self.extract_file.name) as f: + file_content = f.read() + self.assertEqual(yaml.safe_load(result), yaml.safe_load(file_content)) + self.assertIn('WARNING: Network config for role Compute was ' + 'automatically converted from Heat template to Ansible ' + 'Jinja2 template. Please review the file: {}\n' + .format(os.path.join(self.working_dir.name, + 'nic-configs', + 'compute-net-config.j2')), + file_content) + def test_extract_empty(self): stack_dict = { 'parameters': {}, @@ -1111,9 +1232,11 @@ class TestExtractProvisionedNode(test_utils.TestCommand): self.baremetal.node.list.return_value = nodes - argslist = ['--roles-file', self.roles_file.name] + argslist = ['--roles-file', self.roles_file.name, + '--networks-file', self.networks_file.name] self.app.command_options = argslist - verifylist = [('roles_file', self.roles_file.name)] + verifylist = [('roles_file', self.roles_file.name), + ('networks_file', self.networks_file.name)] parsed_args = self.check_parser(self.cmd, argslist, verifylist) diff --git a/tripleoclient/v1/overcloud_node.py b/tripleoclient/v1/overcloud_node.py index 023b9ec29..30a62e512 100644 --- a/tripleoclient/v1/overcloud_node.py +++ b/tripleoclient/v1/overcloud_node.py @@ -28,6 +28,7 @@ from openstack import exceptions as openstack_exc from osc_lib import exceptions as oscexc from osc_lib.i18n import _ from osc_lib import utils +import tempfile import yaml from tripleoclient import command @@ -496,6 +497,15 @@ class ExtractProvisionedNode(command.Command): parser.add_argument('--roles-file', '-r', dest='roles_file', required=False, help=_('Role data definition file')) + parser.add_argument('--networks-file', '-n', dest='networks_file', + required=False, + help=_('Network data definition file')) + parser.add_argument('--working-dir', + action='store', + help=_('The working directory for the deployment ' + 'where all input, output, and generated ' + 'files will be stored.\nDefaults to ' + '"$HOME/overcloud-deploy/"')) return parser def _get_subnet_from_net_name_and_ip(self, net_name, ip_addr): @@ -522,9 +532,61 @@ class ExtractProvisionedNode(command.Command): "network %(net)s." % {'ip': ip_addr, 'net': net_name}) + def _convert_heat_nic_conf_to_j2(self, stack, role_name, network_data, + resource_registry, parsed_args): + heat_nic_conf = resource_registry.get( + 'OS::TripleO::{}::Net::SoftwareConfig'.format(role_name)) + if heat_nic_conf is None or heat_nic_conf == 'OS::Heat::None': + return None + + j2_nic_conf_dir = os.path.join(self.working_dir, 'nic-configs') + oooutils.makedirs(j2_nic_conf_dir) + + heat_nic_conf_basename = os.path.basename(heat_nic_conf) + tmp_heat_nic_conf_path = os.path.join(j2_nic_conf_dir, + heat_nic_conf_basename) + heat_nic_conf_content = stack.files().get(heat_nic_conf) + + j2_nic_conf_basename = (heat_nic_conf_basename.rstrip('.yaml') + '.j2') + j2_nic_conf_path = os.path.join(j2_nic_conf_dir, j2_nic_conf_basename) + + tmp_net_data_fd, tmp_net_data_path = tempfile.mkstemp(suffix=".yaml") + try: + with open(tmp_net_data_path, 'w') as tmp_net_data: + tmp_net_data.write(yaml.safe_dump(network_data)) + with open(tmp_heat_nic_conf_path, 'w') as tmp_heat_nic_conf: + tmp_heat_nic_conf.write(heat_nic_conf_content) + + cmd = ['/usr/share/openstack-tripleo-heat-templates/tools/' + 'convert_heat_nic_config_to_ansible_j2.py'] + if parsed_args.yes: + cmd.extend(['--yes']) + cmd.extend(['--stack', stack.stack_name, + '--networks_file', tmp_net_data_path, + tmp_heat_nic_conf_path]) + retcode = oooutils.run_command_and_log(self.log, cmd) + finally: + try: + os.remove(tmp_net_data_path) + except (IsADirectoryError, FileNotFoundError, PermissionError): + pass + try: + os.remove(tmp_heat_nic_conf_path) + except (IsADirectoryError, FileNotFoundError, PermissionError): + pass + + return j2_nic_conf_path if retcode == 0 else None + def take_action(self, parsed_args): self.log.debug("take_action(%s)" % parsed_args) + if not parsed_args.working_dir: + self.working_dir = oooutils.get_default_working_dir( + parsed_args.stack) + else: + self.working_dir = parsed_args.working_dir + oooutils.makedirs(self.working_dir) + self._setup_clients() stack = oooutils.get_stack(self.orchestration_client, parsed_args.stack) @@ -544,6 +606,19 @@ class ExtractProvisionedNode(command.Command): "stack by setting the --roles-data argument.".format( parsed_args.stack)) + if parsed_args.networks_file: + networks_file = os.path.abspath(parsed_args.networks_file) + with open(networks_file, 'r') as fd: + network_data = yaml.safe_load(fd.read()) + else: + network_data = tht_j2_sources.get('networks_data') + if network_data is None: + raise oscexc.CommandError( + "Unable to extract. Network data not available in {} " + "stack output. Please provide the networks data for the " + "deployed stack by setting the --networks-data argument." + .format(parsed_args.stack)) + # Convert role_data to a dict role_data = {x['name']: x for x in role_data} @@ -553,6 +628,7 @@ class ExtractProvisionedNode(command.Command): stack, 'RoleNetIpMap') or {} parameters = stack.to_dict().get('parameters', {}) parameter_defaults = stack.environment().get('parameter_defaults', {}) + resource_registry = stack.environment().get('resource_registry', {}) # list all baremetal nodes and map hostname to node name node_details = self.baremetal_client.node.list(detail=True) @@ -606,10 +682,21 @@ class ExtractProvisionedNode(command.Command): net_conf['template'] = parameters.get( role_name + 'NetworkConfigTemplate') if net_conf['template'] is None: - warnings.append( - 'WARNING: No network config found for role {}. Please ' - 'edit the file and set the path to the correct network ' - 'config template.'.format(role_name)) + net_conf['template'] = self._convert_heat_nic_conf_to_j2( + stack, role_name, network_data, resource_registry, + parsed_args) + + if net_conf['template'] is None: + warnings.append( + 'WARNING: No network config found for role {}. ' + 'Please edit the file and set the path to the correct ' + 'network config template.'.format(role_name)) + else: + warnings.append( + 'WARNING: Network config for role {} was ' + 'automatically converted from Heat template to ' + 'Ansible Jinja2 template. Please review the file: {}' + .format(role_name, net_conf['template'])) if parameters.get(role_name + 'NetworkDeploymentActions'): network_deployment_actions = parameters.get(