From 6bd7eeb7cccc52863e33d0b33adb5deb05816862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Wed, 2 Sep 2020 20:57:41 +0200 Subject: [PATCH] Add os-net-config mappings support Adds an ansible plug-in that can process the input in THT parameter 'NetConfigDataLookup' used for the OsNetConfigMappings resource in tripleo-heat-templates. Add tasks in the tripleo_network_config role to call the plug-in and write the result into the default mapping file for os-net-config. Change-Id: I968771376cdb3cd2e8e576a4de226b1eccebb23a Blueprint: bp/nova-less-deploy --- .../modules/tripleo_os_net_config_mappings.py | 161 ++++++++++++++++++ .../tripleo_network_config/defaults/main.yml | 1 + .../molecule/default/prepare.yml | 2 + .../tripleo_network_config/tasks/main.yml | 20 +++ .../test_tripleo_os_net_config_mappings.py | 83 +++++++++ 5 files changed, 267 insertions(+) create mode 100644 tripleo_ansible/ansible_plugins/modules/tripleo_os_net_config_mappings.py create mode 100644 tripleo_ansible/tests/modules/test_tripleo_os_net_config_mappings.py diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_os_net_config_mappings.py b/tripleo_ansible/ansible_plugins/modules/tripleo_os_net_config_mappings.py new file mode 100644 index 000000000..402f21271 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_os_net_config_mappings.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2020 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule + +import copy +import os +import subprocess +import yaml + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: tripleo_os_net_config_mappings +author: + - Harald Jensås (hjensas@redhat.com) +version_added: '2.8' +short_description: Configure os-net-config mappings for nodes or node groups +notes: [] +description: + - This module creates os-net-config mapping for nodes or node groups based on + the input data provided. MAC addresses or DMI table strings can be used + to identify specific nodes or node groups. See manual page for DMIDECODE(8) + for a list of DMI table strings that can be used. +options: + net_config_data_lookup: + description: + - Per node and/or per node group configuration map + type: dict +''' + +EXAMPLES = ''' +- name: Map os-net-config nicX abstraction using MAC address + tripleo_os_net_config_mappings: + net_config_data_lookup: + overcloud-controller-0: + nic1: "00:c8:7c:e6:f0:2e" + overcloud-compute-13: + nic1: "00:18:7d:99:0c:b6" +- name: Interface name to os-net-config nicX abstraction using system-uuid + tripleo_os_net_config_mappings: + net_config_data_lookup: + overcloud-controller-0: + dmiString: 'system-uuid' + id: 'A8C85861-1B16-4803-8689-AFC62984F8F6' + nic1: em3 + nic2: em1 + nic3: em2 + nic4: em4 +- name: Interface name to os-net-config nicX abstraction for node groups using system-product-name + tripleo_os_net_config_mappings: + net_config_data_lookup: + nodegroup-dell-poweredge-r630: + dmiString: "system-product-name" + id: "PowerEdge R630" + nic1: em3 + nic2: em1 + nic3: em2 + nodegroup-cisco-ucsb-b200-m4: + dmiString: "system-product-name" + id: "UCSB-B200-M4" + nic1: enp7s0 + nic2: enp6s0 +''' + +RETURN = ''' +mapping: + description: + - Dictionary with os-net-config mapping data that can be written to the + os-net-config mapping file. + returned: when mapping match present in net_config_data_lookup + type: dict +''' + + +def _get_interfaces(): + eth_addr = [ + # cast to lower case for MAC address match + open('/sys/class/net/{}/address'.format(x)).read().strip().lower() + for x in os.listdir('/sys/class/net/')] + eth_addr = list(filter(None, eth_addr)) + + return eth_addr + + +def _get_mappings(data): + eth_addr = _get_interfaces() + + for node in data: + iface_mapping = copy.deepcopy(data[node]) + if 'dmiString' in iface_mapping: + del iface_mapping['dmiString'] + if 'id' in iface_mapping: + del iface_mapping['id'] + + # Match on mac addresses first - cast all to lower case + lc_iface_mapping = copy.deepcopy(iface_mapping) + for key, x in lc_iface_mapping.items(): + lc_iface_mapping[key] = x.lower() + + if any(x in eth_addr for x in + lc_iface_mapping.values()): + return {'interface_mapping': lc_iface_mapping} + + # If data contain dmiString and id keys, try to match node(group) + if 'dmiString' in data[node] and 'id' in data[node]: + ps = subprocess.Popen( + ['dmidecode', '--string', data[node]['dmiString']], + stdout=subprocess.PIPE, universal_newlines=True) + out, err = ps.communicate() + + # See LP#1816652 + if data[node].get('id').lower() == out.rstrip().lower(): + return {'interface_mapping': lc_iface_mapping} + + +def run(module): + results = dict( + changed=False, + mapping=None, + ) + + data = module.params['net_config_data_lookup'] + if isinstance(data, dict) and data: + results['mapping'] = _get_mappings(data) + + results['changed'] = True if results['mapping'] else False + + module.exit_json(**results) + + +def main(): + module = AnsibleModule( + argument_spec=yaml.safe_load(DOCUMENTATION)['options'], + supports_check_mode=False, + ) + run(module) + +if __name__ == '__main__': + main() diff --git a/tripleo_ansible/roles/tripleo_network_config/defaults/main.yml b/tripleo_ansible/roles/tripleo_network_config/defaults/main.yml index af024bdf7..34ef69595 100644 --- a/tripleo_ansible/roles/tripleo_network_config/defaults/main.yml +++ b/tripleo_ansible/roles/tripleo_network_config/defaults/main.yml @@ -27,3 +27,4 @@ tripleo_network_config_hide_sensitive_logs: true tripleo_network_config_interface_name: nic1 tripleo_network_config_manage_service: true tripleo_network_config_network_deployment_actions: [] +tripleo_network_config_os_net_config_mappings: {} diff --git a/tripleo_ansible/roles/tripleo_network_config/molecule/default/prepare.yml b/tripleo_ansible/roles/tripleo_network_config/molecule/default/prepare.yml index 4f6b8383d..9f34d9253 100644 --- a/tripleo_ansible/roles/tripleo_network_config/molecule/default/prepare.yml +++ b/tripleo_ansible/roles/tripleo_network_config/molecule/default/prepare.yml @@ -20,6 +20,8 @@ tasks: - import_role: name: test_deps + vars: + test_deps_setup_tripleo: true - name: Ensure legacy scripts installed package: name: network-scripts diff --git a/tripleo_ansible/roles/tripleo_network_config/tasks/main.yml b/tripleo_ansible/roles/tripleo_network_config/tasks/main.yml index da0400042..1dca834c1 100644 --- a/tripleo_ansible/roles/tripleo_network_config/tasks/main.yml +++ b/tripleo_ansible/roles/tripleo_network_config/tasks/main.yml @@ -53,6 +53,26 @@ mode: 0755 when: not ansible_check_mode|bool + - name: Create /etc/os-net-config directory + become: true + file: + path: /etc/os-net-config + state: directory + recurse: true + + - name: Create os-net-config mappings from lookup data + tripleo_os_net_config_mappings: + net_config_data_lookup: + "{{ tripleo_network_config_os_net_config_mappings }}" + when: not ansible_check_mode|bool + register: os_net_config_mappings_result + + - name: Write os-net-config mappings file /etc/os-net-config/mapping.yaml + copy: + content: "{{ os_net_config_mappings_result.mapping | to_nice_yaml }}" + dest: /etc/os-net-config/mapping.yaml + when: os_net_config_mappings_result.changed|bool + - name: Run NetworkConfig script shell: /var/lib/tripleo-config/scripts/run_os_net_config.sh async: "{{ tripleo_network_config_async_timeout }}" diff --git a/tripleo_ansible/tests/modules/test_tripleo_os_net_config_mappings.py b/tripleo_ansible/tests/modules/test_tripleo_os_net_config_mappings.py new file mode 100644 index 000000000..d6a051635 --- /dev/null +++ b/tripleo_ansible/tests/modules/test_tripleo_os_net_config_mappings.py @@ -0,0 +1,83 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from tripleo_ansible.ansible_plugins.modules import ( + tripleo_os_net_config_mappings) +from tripleo_ansible.tests import base as tests_base + +import mock + + +@mock.patch('tripleo_ansible.ansible_plugins.modules.' + 'tripleo_os_net_config_mappings._get_interfaces', autospec=True) +@mock.patch('subprocess.Popen', autospec=True) +class TestTripleoOsNetConfigMappings(tests_base.TestCase): + + def test_mac_mappings_match(self, mock_Popen, mock_get_ifaces): + module = mock.MagicMock() + module.params = { + 'net_config_data_lookup': { + 'node0': {'nic1': 'aa:bb:cc:dd:ee:ff', + 'nic2': 'ff:ee:dd:cc:bb:aa'}, + 'node1': {'nic1': '0a:0b:0c:0d:0e:0f', + 'nic2': 'f0:e0:d0:c0:b0:a0'} + } + } + mock_exit = mock.MagicMock() + module.exit_json = mock_exit + mock_get_ifaces.side_effect = ['aa:bb:cc:dd:ee:ff', 'ff:ee:dd:cc:bb:aa'] + expected = module.params['net_config_data_lookup']['node0'] + tripleo_os_net_config_mappings.run(module) + mock_exit.assert_called_once_with( + changed=True, mapping={'interface_mapping': expected}) + + def test_mac_mappings_no_match(self, mock_Popen, mock_get_ifaces): + module = mock.MagicMock() + module.params = { + 'net_config_data_lookup': { + 'node0': {'nic1': 'aa:bb:cc:dd:ee:ff', + 'nic2': 'ff:ee:dd:cc:bb:aa'}, + 'node1': {'nic1': '0a:0b:0c:0d:0e:0f', + 'nic2': 'f0:e0:d0:c0:b0:a0'} + } + } + mock_exit = mock.MagicMock() + module.exit_json = mock_exit + mock_get_ifaces.side_effect = ['01:02:03:04:05:06', '10:20:30:40:50:60'] + tripleo_os_net_config_mappings.run(module) + mock_exit.assert_called_once_with(changed=False, mapping=None) + + def test_dmi_type_string_match(self, mock_Popen, mock_get_ifaces): + module = mock.MagicMock() + module.params = { + 'net_config_data_lookup': { + 'node2': {'dmiString': 'foo-dmi-type', + 'id': 'bar-dmi-id', + 'nic1': 'em3', + 'nic2': 'em4'}, + 'node3': {'nic1': '0a:0b:0c:0d:0e:0f', + 'nic2': 'f0:e0:d0:c0:b0:a0'} + } + } + mock_exit = mock.MagicMock() + module.exit_json = mock_exit + expected = {'nic1': 'em3', + 'nic2': 'em4'} + mock_return = mock.MagicMock() + mock_return.return_value.communicate.return_value = ('bar-dmi-id', '') + mock_Popen.side_effect = mock_return + tripleo_os_net_config_mappings.run(module) + mock_exit.assert_called_once_with( + changed=True, mapping={'interface_mapping': expected})