diff --git a/.ansible-lint b/.ansible-lint index 827b7e480..aadd294a1 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -64,3 +64,4 @@ mock_modules: - tripleo_templates_upload - tripleo_unmanaged_populate_environment - tripleo_generate_inventory_network_config + - tripleo_overcloud_network_vip_extract diff --git a/doc/source/modules/modules-tripleo_overcloud_network_vip_extract.rst b/doc/source/modules/modules-tripleo_overcloud_network_vip_extract.rst new file mode 100644 index 000000000..1ce95ac14 --- /dev/null +++ b/doc/source/modules/modules-tripleo_overcloud_network_vip_extract.rst @@ -0,0 +1,14 @@ +============================================== +Module - tripleo_overcloud_network_vip_extract +============================================== + + +This module provides for the following ansible plugin: + + * tripleo_overcloud_network_vip_extract + + +.. ansibleautoplugin:: + :module: tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_extract.py + :documentation: true + :examples: true diff --git a/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_extract.py b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_extract.py new file mode 100644 index 000000000..39635ea38 --- /dev/null +++ b/tripleo_ansible/ansible_plugins/modules/tripleo_overcloud_network_vip_extract.py @@ -0,0 +1,182 @@ +#!/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 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_extract + +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 +author: + - Harald Jensås +''' + +RETURN = ''' +vip_data: +- dns_name: overcloud + ip_address: 172.19.0.36 + name: storage_mgmt_virtual_ip + network: storage_mgmt + subnet: storage_mgmt_subnet +- dns_name: overcloud + ip_address: 172.17.0.167 + name: internal_api_virtual_ip + network: internal_api + subnet: internal_api_subnet +- dns_name: overcloud + ip_address: 172.18.0.83 + name: storage_virtual_ip + network: storage + subnet: storage_subnet +- dns_name: overcloud + ip_address: 10.0.0.82 + name: external_virtual_ip + network: external + subnet: external_subnet +- dns_name: overcloud + ip_address: 192.168.25.13 + name: control_virtual_ip + network: ctlplane + subnet: ctlplane-leaf1 +''' + +EXAMPLES = ''' +- name: Get Overcloud Virtual IPs data + tripleo_overcloud_network_vip_extract: + stack_name: overcloud + register: overcloud_vip_data +- name: Write Virtual IPs data to output file + copy: + content: "{{ overcloud_vip_data.network_data | to_yaml }}" + dest: /path/exported-vip-data.yaml +''' + + +def update_vip_data(conn, network, vip_ports, vip_data): + try: + vip = next(vip_ports) + except StopIteration: + return + + if not vip.fixed_ips: + return + + subnet = conn.network.get_subnet(vip.fixed_ips[0]['subnet_id']) + vip_data.append(dict(name=vip.name, + network=network.name, + subnet=subnet.name, + ip_address=vip.fixed_ips[0]['ip_address'], + dns_name=vip.dns_name)) + + +def find_net_vips(conn, net_resrcs, vip_data): + for net in net_resrcs: + for res in net_resrcs[net]: + if not net_resrcs[net][res]['resource_type'] == n_utils.TYPE_NET: + continue + + network = conn.network.get_network( + net_resrcs[net][res][n_utils.RES_ID]) + vip_ports = conn.network.ports( + network_id=network.id, + name='{}{}'.format(network.name, n_utils.NET_VIP_SUFFIX)) + + update_vip_data(conn, network, vip_ports, vip_data) + + +def find_ctlplane_vip(conn, vip_data): + network = conn.network.find_network('ctlplane') + vip_ports = conn.network.ports( + network_id=network.id, + name='control{}'.format(n_utils.NET_VIP_SUFFIX)) + + update_vip_data(conn, network, vip_ports, vip_data) + + +def run_module(): + result = dict( + success=False, + changed=False, + error="", + vip_data=list() + ) + + 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'] + + try: + _, conn = openstack_cloud_from_module(module) + net_resources = n_utils.get_overcloud_network_resources(conn, stack) + find_net_vips(conn, net_resources, result['vip_data']) + find_ctlplane_vip(conn, result['vip_data']) + + result['changed'] = True if result['vip_data'] else False + result['success'] = True if result['vip_data'] else False + module.exit_json(**result) + except Exception as err: + result['error'] = str(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/playbooks/cli-overcloud-network-vip-extract.yaml b/tripleo_ansible/playbooks/cli-overcloud-network-vip-extract.yaml new file mode 100644 index 000000000..923024fe4 --- /dev/null +++ b/tripleo_ansible/playbooks/cli-overcloud-network-vip-extract.yaml @@ -0,0 +1,54 @@ +--- +# 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. +# +- name: Overcloud Virtuap IPs Extract + connection: "{{ (tripleo_target_host is defined) | ternary('ssh', 'local') }}" + hosts: "{{ tripleo_target_host | default('localhost') }}" + remote_user: "{{ tripleo_target_user | default(lookup('env', 'USER')) }}" + gather_facts: "{{ (tripleo_target_host is defined) | ternary(true, false) }}" + any_errors_fatal: true + pre_tasks: + - name: Validate that stack name is provided + fail: + msg: stack_name is a required input + when: + - stack_name is undefined + - name: Validate that output is provided + fail: + msg: output is a required input + when: + - output is undefined + - name: Check if output file exists + stat: + path: "{{ output }}" + register: stat_output_file + - name: Fail if output file already exists and overwrite not set + fail: + msg: Output file exists + when: + - stat_output_file.stat.exists and not overwrite|bool + + tasks: + + - name: Get Virtual IPs data from overcloud stack + tripleo_overcloud_network_vip_extract: + stack_name: "{{ stack_name }}" + register: overcloud_vip_data + + - name: Write Virtual IPs data to output file + copy: + content: "{{ overcloud_vip_data.vip_data | to_nice_yaml(indent=2) }}" + dest: "{{ output }}" diff --git a/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_extract.py b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_extract.py new file mode 100644 index 000000000..e2d3fc342 --- /dev/null +++ b/tripleo_ansible/tests/modules/test_tripleo_overcloud_network_vip_extract.py @@ -0,0 +1,101 @@ +# 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 + +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 tripleo_ansible.ansible_plugins.modules import ( + tripleo_overcloud_network_vip_extract as plugin) +from tripleo_ansible.tests import base as tests_base +from tripleo_ansible.tests import stubs + + +@mock.patch.object(openstack.connection, 'Connection', autospec=True) +class TestTripleoOvercloudVipExtract(tests_base.TestCase): + + def setUp(self): + super(TestTripleoOvercloudVipExtract, self).setUp() + + # Helper function to convert array to generator + self.a2g = lambda x: (n for n in x) + + def test_find_net_vips(self, mock_conn): + fake_net_resources = { + 'StorageNetwork': { + 'InternalApiNetwork': {'physical_resource_id': 'fake-id', + 'resource_type': n_utils.TYPE_NET}, + 'StorageSubnet': {'physical_resource_id': 'fake-id', + 'resource_type': n_utils.TYPE_SUBNET}, + 'StorageSubnet_leaf1': {'physical_resource_id': 'fake-id', + 'resource_type': n_utils.TYPE_SUBNET} + } + } + fake_network = stubs.FakeNeutronNetwork( + id='internal_api_id', + name='internal_api') + fake_subnet = stubs.FakeNeutronSubnet( + id='internal_api_subnet_id', + name='internal_api_subnet') + fake_vip_port = stubs.FakeNeutronPort( + id='internal_api_vip_id', + name='internal_api_virtual_ip', + fixed_ips=[{'subnet_id': 'internal_api_subnet_id', + 'ip_address': '1.2.3.4'}], + dns_name='internalapi.localdomain' + ) + mock_conn.network.get_network.return_value = fake_network + mock_conn.network.get_subnet.return_value = fake_subnet + mock_conn.network.ports.return_value = self.a2g([fake_vip_port]) + + vip_data = list() + plugin.find_net_vips(mock_conn, fake_net_resources, vip_data) + self.assertEqual([{'name': 'internal_api_virtual_ip', + 'network': 'internal_api', + 'subnet': 'internal_api_subnet', + 'ip_address': '1.2.3.4', + 'dns_name': 'internalapi.localdomain'}], + vip_data) + + def test_find_ctlplane_vip(self, mock_conn): + fake_network = stubs.FakeNeutronNetwork( + id='ctlplane_id', + name='ctlplane') + fake_subnet = stubs.FakeNeutronSubnet( + id='ctlplane_subnet_id', + name='ctlplane-subnet') + fake_vip_port = stubs.FakeNeutronPort( + id='ctlplane_vip_id', + name='control_virtual_ip', + fixed_ips=[{'subnet_id': 'ctlplane_subnet_id', + 'ip_address': '4.3.2.1'}], + dns_name='ctlplane.localdomain' + ) + mock_conn.network.find_network.return_value = fake_network + mock_conn.network.get_subnet.return_value = fake_subnet + mock_conn.network.ports.return_value = self.a2g([fake_vip_port]) + + vip_data = list() + plugin.find_ctlplane_vip(mock_conn, vip_data) + self.assertEqual([{'name': 'control_virtual_ip', + 'network': 'ctlplane', + 'subnet': 'ctlplane-subnet', + 'ip_address': '4.3.2.1', + 'dns_name': 'ctlplane.localdomain'}], + vip_data)