From 29253dab6ae7ae1c9803451c5a25ce69b3c56928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Mon, 22 Mar 2021 11:33:25 +0100 Subject: [PATCH] Add module to populate net VIPs environment Add module to populate environment for deployed network Virtual IPs. Related: blueprint network-data-v2-port Change-Id: I9c542aea654c3fcf4c62293b9d34e6790632006c --- .ansible-lint | 1 + ...cloud_network_vip_populate_environment.rst | 14 + ...rcloud_network_vip_populate_environment.py | 240 ++++++++++++++++++ ...pleo_overcloud_network_vip_populate_env.py | 183 +++++++++++++ 4 files changed, 438 insertions(+) create mode 100644 doc/source/modules/modules-tripleo_overcloud_network_vip_populate_environment.rst create mode 100644 tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_populate_environment.py create mode 100644 tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_populate_env.py diff --git a/.ansible-lint b/.ansible-lint index e63f796e2..ef2964ddb 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -66,3 +66,4 @@ mock_modules: - tripleo_generate_inventory_network_config - tripleo_overcloud_network_vip_extract - tripleo_overcloud_network_vip_provision + - tripleo_overcloud_network_vip_populate_environment diff --git a/doc/source/modules/modules-tripleo_overcloud_network_vip_populate_environment.rst b/doc/source/modules/modules-tripleo_overcloud_network_vip_populate_environment.rst new file mode 100644 index 000000000..449dd4ca9 --- /dev/null +++ b/doc/source/modules/modules-tripleo_overcloud_network_vip_populate_environment.rst @@ -0,0 +1,14 @@ +=========================================================== +Module - tripleo_overcloud_network_vip_populate_environment +=========================================================== + + +This module provides for the following ansible plugin: + + * tripleo_overcloud_network_vip_populate_environment + + +.. ansibleautoplugin:: + :module: tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_populate_environment.py + :documentation: true + :examples: true diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_populate_environment.py b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_populate_environment.py new file mode 100644 index 000000000..acac8ee46 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_populate_environment.py @@ -0,0 +1,240 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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. +# + +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright 2021 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. +# + +import os +import yaml + +try: + from ansible.module_utils import network_data_v2 as n_utils +except ImportError: + from tripleo_ansible.ansible_plugins.module_utils import network_data_v2 as n_utils # noqa +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.openstack import openstack_full_argument_spec +from ansible.module_utils.openstack import openstack_module_kwargs +from ansible.module_utils.openstack import openstack_cloud_from_module + + +ANSIBLE_METADATA = { + 'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community' +} + +DOCUMENTATION = ''' +--- +module: tripleo_overcloud_network_vip_populate_environment + +short_description: Extract information on provisioned overcloud Virtual IPs + +version_added: "2.8" + +description: + - Extract information about provisioned network Virtual IP resources in + overcloud heat stack. + +options: + stack_name: + description: + - Name of the overcloud heat stack + type: str + vip_data: + description: + - Dictionary of network Virtual IP definitions + type: list + elements: dict + suboptions: + name: + description: + - Virtual IP name (optional) + type: str + network: + description: + - Neutron Network name + type: str + required: True + ip_address: + description: + - IP address (Optional) + type: str + subnet: + description: + - Neutron Subnet name (Optional) + type: str + dns_name: + description: + - Dns Name (Optional) + type: str + required: True +author: + - Harald Jensås +''' + +RETURN = ''' +env: + +''' + +EXAMPLES = ''' +- name: Get Overcloud Virtual IPs data + tripleo_overcloud_network_vip_populate_environment: + stack_name: overcloud + register: overcloud_vip_env +- name: Write Virtual IPs environment to output file + copy: + content: "{{ overcloud_vip_env.vip_env | to_yaml }}" + dest: /path/overcloud_vip_env.yaml +''' + +DEFAULT_THT_DIR = '/usr/share/openstack-tripleo-heat-templates' +REGISTRY_KEY_TPL = 'OS::TripleO::Network::Ports::{net_name}VipPort' +PORT_PATH_TPL = 'network/ports/deployed_vip_{net_name_lower}.yaml' + + +def get_net_name_map(conn): + _map = {} + + networks = list(conn.network.networks()) + if not networks: + raise Exception('Unable to create vip environment. No networks found') + + for network in networks: + tags = n_utils.tags_to_dict(network.tags) + try: + _map[network.name] = tags['tripleo_network_name'] + except KeyError: + # Hard code the ControlPlane resource which is static in + # THT/overcloud-resource-registry-puppet.j2.yaml + if network.name == 'ctlplane': + _map[network.name] = 'ControlPlane' + + return _map + + +def add_ctlplane_vip_to_env(conn, ctlplane_vip_data, port): + network = conn.network.get_network(port.network_id) + subnet = conn.network.get_subnet(port.fixed_ips[0]['subnet_id']) + ctlplane_vip_data['network'] = dict() + ctlplane_vip_data['network']['tags'] = network.tags + ctlplane_vip_data['subnets'] = list() + ctlplane_vip_data['subnets'].append({'ip_version': subnet.ip_version}) + ctlplane_vip_data['fixed_ips'] = [{'ip_address': x['ip_address']} + for x in port.fixed_ips] + ctlplane_vip_data['name'] = port.name + + +def add_vip_to_env(conn, vip_port_map, port, net_name_lower): + subnet = conn.network.get_subnet(port.fixed_ips[0]['subnet_id']) + + vip_port = vip_port_map[net_name_lower] = {} + vip_port['ip_address'] = port.fixed_ips[0]['ip_address'] + vip_port['ip_address_uri'] = n_utils.wrap_ipv6( + port.fixed_ips[0]['ip_address']) + vip_port['ip_subnet'] = '/'.join([port.fixed_ips[0]['ip_address'], + subnet.cidr.split('/')[1]]) + + +def populate_net_vip_env(conn, stack, net_maps, vip_data, env): + low_up_map = get_net_name_map(conn) + + resource_reg = env['resource_registry'] = {} + param_defaults = env['parameter_defaults'] = {} + vip_port_map = param_defaults['VipPortMap'] = {} + ctlplane_vip_data = param_defaults['ControlPlaneVipData'] = {} + for vip_spec in vip_data: + net_name_lower = vip_spec['network'] + try: + port = next(conn.network.ports( + network_id=net_maps['by_name'][net_name_lower]['id'], + tags=['tripleo_stack_name={}'.format(stack), + 'tripleo_vip_net={}'.format(net_name_lower)])) + except StopIteration: + raise Exception('Neutron port for Virtual IP spec {} not ' + 'found'.format(vip_spec)) + + resource_reg[REGISTRY_KEY_TPL.format( + net_name=low_up_map[net_name_lower])] = os.path.join( + DEFAULT_THT_DIR, PORT_PATH_TPL.format( + net_name_lower=net_name_lower)) + + if net_name_lower == 'ctlplane': + add_ctlplane_vip_to_env(conn, ctlplane_vip_data, port) + else: + add_vip_to_env(conn, vip_port_map, port, net_name_lower) + + +def run_module(): + result = dict( + success=False, + changed=False, + error="", + env=dict() + ) + + argument_spec = openstack_full_argument_spec( + **yaml.safe_load(DOCUMENTATION)['options'] + ) + + module = AnsibleModule( + argument_spec, + supports_check_mode=False, + **openstack_module_kwargs() + ) + + stack = module.params['stack_name'] + vip_data = module.params['vip_data'] + + try: + _, conn = openstack_cloud_from_module(module) + net_maps = n_utils.create_name_id_maps(conn) + populate_net_vip_env(conn, stack, net_maps, vip_data, result['env']) + + result['changed'] = True if result['env'] else False + result['success'] = True if result['env'] else False + module.exit_json(**result) + except Exception as err: + result['error'] = err + result['msg'] = ("Error getting Virtual IPs data from overcloud stack " + "{stack_name}: %{error}".format(stack_name=stack, + error=err)) + module.fail_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_populate_env.py b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_populate_env.py new file mode 100644 index 000000000..8266d0be7 --- /dev/null +++ b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_populate_env.py @@ -0,0 +1,183 @@ +# Copyright 2021 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. + +import mock +import openstack + +from tripleo_ansible.ansible_plugins.modules import ( + tripleo_overcloud_network_vip_populate_environment as plugin) +from tripleo_ansible.tests import base as tests_base +from tripleo_ansible.tests import stubs + +BY_NAME_MAP = { + 'ctlplane': { + 'id': 'ctlplane_id', + 'subnets': { + 'ctlplane-subnet': 'ctlplane_subnet_id' + } + }, + 'internal_api': { + 'id': 'internal_api_id', + 'subnets': { + 'internal_api_subnet': 'internal_api_subnet_id', + } + }, + 'storage_mgmt': { + 'id': 'storage_mgmt_id', + 'subnets': { + 'storage_mgmt_subnet': 'storage_mgmt_subnet_id', + } + }, + 'external': { + 'id': 'external_id', + 'subnets': { + 'external_subnet': 'external_subnet_id', + } + }, +} + +NET_MAPS = {'by_name': BY_NAME_MAP} + +fake_internal_api = stubs.FakeNeutronNetwork( + id='internal_api_id', name='internal_api', + dns_domain='internalapi.localdomain.', + tags=['tripleo_network_name=InternalApi', 'tripleo_stack_name=stack']) +fake_storage_mgmt = stubs.FakeNeutronNetwork( + id='storage_mgmt_id', name='storage_mgmt', + dns_domain='storagemgmt.localdomain.', + tags=['tripleo_network_name=StorageMgmt', 'tripleo_stack_name=stack']) +fake_external = stubs.FakeNeutronNetwork( + id='external_id', name='external', + dns_domain='external.localdomain.', + tags=['tripleo_network_name=External', 'tripleo_stack_name=stack']) +fake_ctlplane = stubs.FakeNeutronNetwork( + id='ctlplane_id', name='ctlplane', dns_domain='ctlplane.localdomain.', + tags=['foo', 'bar']) +fake_ctlplane_subnet = stubs.FakeNeutronSubnet( + id='ctlplane_subnet_id', name='ctlplane-subnet', cidr='192.168.24.0/24', + ip_version=4) +fake_internal_api_subnet = stubs.FakeNeutronSubnet( + id='internal_api_subnet_id', name='internal_api_subnet', + cidr='10.0.1.0/24') +fake_storage_mgmt_subnet = stubs.FakeNeutronSubnet( + id='storage_mgmt_subnet_id', name='storage_mgmt_subnet', + cidr='10.0.3.0/24') +fake_external_subnet = stubs.FakeNeutronSubnet( + id='external_subnet_id', name='external_subnet', cidr='10.0.5.0/24') + +fake_ctlplane_port = stubs.FakeNeutronPort( + name='control_virtual_ip', + id='ctlplane_port_id', + dns_name='overcloud', + fixed_ips=[{'ip_address': '192.168.24.1', + 'subnet_id': 'ctlplane_subnet_id'}], + tags=['tripleo_stack_name=stack', 'tripleo_vip_net=ctlplane'] +) +fake_internal_api_port = stubs.FakeNeutronPort( + id='internal_api_port_id', + dns_name='overcloud', + fixed_ips=[{'ip_address': '10.0.1.1', + 'subnet_id': 'internal_api_subnet_id'}], + tags=['tripleo_stack_name=stack', 'tripleo_vip_net=internal_api'] +) +fake_storage_mgmt_port = stubs.FakeNeutronPort( + id='storage_mgmt_port_id', + dns_name='overcloud', + fixed_ips=[{'ip_address': '10.0.3.1', + 'subnet_id': 'storage_mgmt_subnet_id'}], + tags=['tripleo_stack_name=stack', 'tripleo_vip_net=storage_mgmt'] +) +fake_external_port = stubs.FakeNeutronPort( + id='external_port_id', + dns_name='overcloud', + fixed_ips=[{'ip_address': '10.0.5.1', + 'subnet_id': 'external_subnet_id'}], + tags=['tripleo_stack_name=stack', 'tripleo_vip_net=External'] +) + + +@mock.patch.object(openstack.connection, 'Connection', autospec=True) +class TestTripleoOvercloudVipProvision(tests_base.TestCase): + + def setUp(self): + super(TestTripleoOvercloudVipProvision, self).setUp() + + # Helper function to convert array to generator + self.a2g = lambda x: (n for n in x) + + def test_get_net_name_map(self, mock_conn): + mock_conn.network.networks.return_value = self.a2g( + [fake_ctlplane, fake_internal_api, fake_storage_mgmt, + fake_external]) + self.assertEqual({'ctlplane': 'ControlPlane', + 'external': 'External', + 'internal_api': 'InternalApi', + 'storage_mgmt': 'StorageMgmt'}, + plugin.get_net_name_map(mock_conn)) + + def test_populate_net_vip_env(self, mock_conn): + mock_conn.network.networks.return_value = self.a2g( + [fake_ctlplane, fake_internal_api, fake_storage_mgmt, + fake_external]) + mock_conn.network.ports.side_effect = [ + self.a2g([fake_ctlplane_port]), + self.a2g([fake_internal_api_port]), + self.a2g([fake_storage_mgmt_port]), + self.a2g([fake_external_port]) + ] + mock_conn.network.get_network.return_value = fake_ctlplane + mock_conn.network.get_subnet.side_effect = [fake_ctlplane_subnet, + fake_internal_api_subnet, + fake_storage_mgmt_subnet, + fake_external_subnet] + vip_data = [ + {'name': 'control_virtual_ip', 'network': 'ctlplane'}, + {'name': 'internal_api_virtual_ip', 'network': 'internal_api'}, + {'name': 'storage_mgmt_virtual_ip', 'network': 'storage_mgmt'}, + {'name': 'external_virtual_ip', 'network': 'external'}] + env = {} + plugin.populate_net_vip_env(mock_conn, 'stack', NET_MAPS, vip_data, + env) + self.assertEqual({ + 'ControlPlaneVipData': { + 'name': 'control_virtual_ip', + 'fixed_ips': [{'ip_address': '192.168.24.1'}], + 'network': {'tags': ['foo', 'bar']}, + 'subnets': [{'ip_version': 4}]}, + 'VipPortMap': { + 'external': {'ip_address': '10.0.5.1', + 'ip_address_uri': '10.0.5.1', + 'ip_subnet': '10.0.5.1/24'}, + 'internal_api': {'ip_address': '10.0.1.1', + 'ip_address_uri': '10.0.1.1', + 'ip_subnet': '10.0.1.1/24'}, + 'storage_mgmt': {'ip_address': '10.0.3.1', + 'ip_address_uri': '10.0.3.1', + 'ip_subnet': '10.0.3.1/24'}}}, + env['parameter_defaults']) + self.assertEqual( + {'OS::TripleO::Network::Ports::ControlPlaneVipPort': ( + '/usr/share/openstack-tripleo-heat-templates/network/ports' + '/deployed_vip_ctlplane.yaml'), + 'OS::TripleO::Network::Ports::ExternalVipPort': ( + '/usr/share/openstack-tripleo-heat-templates/network/ports/' + 'deployed_vip_external.yaml'), + 'OS::TripleO::Network::Ports::InternalApiVipPort': ( + '/usr/share/openstack-tripleo-heat-templates/network/ports/' + 'deployed_vip_internal_api.yaml'), + 'OS::TripleO::Network::Ports::StorageMgmtVipPort': ( + '/usr/share/openstack-tripleo-heat-templates/network/ports/' + 'deployed_vip_storage_mgmt.yaml')}, + env['resource_registry'])