From 72944f1a3a3bb2642fa86fa595881dc99409d91d Mon Sep 17 00:00:00 2001 From: Bram Verschueren Date: Thu, 23 Jan 2020 08:04:45 +0100 Subject: [PATCH] Add os_routers_info module Introduces a new os_routers_info module. original github PR: https://github.com/ansible/ansible/pull/63731 Change-Id: I7fe56cfe862b2b8209675acf0f87fbad99e09546 --- ci/roles/router/tasks/main.yml | 28 +++ plugins/modules/os_routers_info.py | 192 ++++++++++++++++ tests/sanity/ignore-2.10.txt | 2 + tests/sanity/ignore-2.9.txt | 2 + .../cloud/openstack/test_os_routers_info.py | 208 ++++++++++++++++++ tests/unit/requirements.txt | 3 + 6 files changed, 435 insertions(+) create mode 100644 plugins/modules/os_routers_info.py create mode 100644 tests/unit/modules/cloud/openstack/test_os_routers_info.py diff --git a/ci/roles/router/tasks/main.yml b/ci/roles/router/tasks/main.yml index e4f6d776..9a631309 100644 --- a/ci/roles/router/tasks/main.yml +++ b/ci/roles/router/tasks/main.yml @@ -29,6 +29,20 @@ interfaces: - shade_subnet1 +- name: Gather routers info + openstack.cloud.os_routers_info: + cloud: "{{ cloud }}" + name: "{{ router_name }}" + filters: + admin_state_up: true + register: result + +- name: Verify routers info + assert: + that: + - "result.openstack_routers.0.name == router_name" + - (result.openstack_routers.0.interfaces_info|length) == 1 + # Admin operation - name: Create external network openstack.cloud.os_network: @@ -60,6 +74,20 @@ when: - network_external +- name: Gather routers info + openstack.cloud.os_routers_info: + cloud: "{{ cloud }}" + name: "{{ router_name }}" + filters: + admin_state_up: true + register: result + +- name: Verify routers info + assert: + that: + - "result.openstack_routers.0.name == router_name" + - (result.openstack_routers.0.interfaces_info|length) == 1 + - name: Delete router openstack.cloud.os_router: cloud: "{{ cloud }}" diff --git a/plugins/modules/os_routers_info.py b/plugins/modules/os_routers_info.py new file mode 100644 index 00000000..c5fa30ef --- /dev/null +++ b/plugins/modules/os_routers_info.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Bram Verschueren +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: os_routers_info +short_description: Retrieve information about one or more OpenStack routers. +version_added: "2.10" +author: "Bram Verschueren (@bverschueren)" +description: + - Retrieve information about one or more routers from OpenStack. +requirements: + - "python >= 2.7" + - "openstacksdk" +options: + name: + description: + - Name or ID of the router + required: false + type: str + filters: + description: + - A dictionary of meta data to use for further filtering. Elements of + this dictionary may be additional dictionaries. + required: false + type: dict + suboptions: + project_id: + description: + - Filter the list result by the ID of the project that owns the resource. + type: str + aliases: + - tenant_id + name: + description: + - Filter the list result by the human-readable name of the resource. + type: str + description: + description: + - Filter the list result by the human-readable description of the resource. + type: str + admin_state_up: + description: + - Filter the list result by the administrative state of the resource, which is up (true) or down (false). + type: bool + revision_number: + description: + - Filter the list result by the revision number of the resource. + type: int + tags: + description: + - A list of tags to filter the list result by. Resources that match all tags in this list will be returned. + type: list +extends_documentation_fragment: openstack +''' + +EXAMPLES = ''' +- name: Gather information about routers + os_routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router by name + os_routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + name: router1 + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" + +- name: Gather information about a router with filter + os_routers_info: + auth: + auth_url: https://identity.example.com + username: user + password: password + project_name: someproject + filters: + tenant_id: bc3ea709c96849d6b81f54640400a19f + register: result + +- name: Show openstack routers + debug: + msg: "{{ result.openstack_routers }}" +''' + +RETURN = ''' +openstack_routers: + description: has all the openstack information about the routers + returned: always, but can be null + type: complex + contains: + id: + description: Unique UUID. + returned: success + type: str + name: + description: Name given to the router. + returned: success + type: str + status: + description: Router status. + returned: success + type: str + external_gateway_info: + description: The external gateway information of the router. + returned: success + type: dict + interfaces_info: + description: List of connected interfaces. + returned: success + type: list + distributed: + description: Indicates a distributed router. + returned: success + type: bool + ha: + description: Indicates a highly-available router. + returned: success + type: bool + project_id: + description: Project id associated with this router. + returned: success + type: str + routes: + description: The extra routes configuration for L3 router. + returned: success + type: list +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import openstack_full_argument_spec, openstack_cloud_from_module + + +def main(): + + argument_spec = openstack_full_argument_spec( + name=dict(required=False, default=None), + filters=dict(required=False, type='dict', default=None) + ) + module = AnsibleModule(argument_spec) + + sdk, cloud = openstack_cloud_from_module(module) + try: + routers = cloud.search_routers(module.params['name'], + module.params['filters']) + for router in routers: + interfaces_info = [] + for port in cloud.list_router_interfaces(router): + if port.device_owner != "network:router_gateway": + for ip_spec in port.fixed_ips: + int_info = { + 'port_id': port.id, + 'ip_address': ip_spec.get('ip_address'), + 'subnet_id': ip_spec.get('subnet_id') + } + interfaces_info.append(int_info) + router['interfaces_info'] = interfaces_info + + module.exit_json(changed=False, openstack_routers=routers) + + except sdk.exceptions.OpenStackCloudException as e: + module.fail_json(msg=str(e)) + + +if __name__ == '__main__': + main() diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index a8884ae7..12855a28 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -81,6 +81,8 @@ plugins/modules/os_recordset.py validate-modules:doc-required-mismatch plugins/modules/os_recordset.py validate-modules:parameter-type-not-in-doc plugins/modules/os_router.py validate-modules:doc-missing-type plugins/modules/os_router.py validate-modules:parameter-type-not-in-doc +plugins/modules/os_routers_info.py validate-modules:doc-missing-type +plugins/modules/os_routers_info.py validate-modules:undocumented-parameter plugins/modules/os_security_group.py validate-modules:doc-missing-type plugins/modules/os_security_group_rule.py validate-modules:doc-missing-type plugins/modules/os_security_group_rule.py validate-modules:parameter-type-not-in-doc diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 914cb75c..e00f46d7 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -73,6 +73,8 @@ plugins/modules/os_recordset.py validate-modules:doc-missing-type plugins/modules/os_recordset.py validate-modules:parameter-type-not-in-doc plugins/modules/os_router.py validate-modules:doc-missing-type plugins/modules/os_router.py validate-modules:parameter-type-not-in-doc +plugins/modules/os_routers_info.py validate-modules:doc-missing-type +plugins/modules/os_routers_info.py validate-modules:undocumented-parameter plugins/modules/os_security_group.py validate-modules:doc-missing-type plugins/modules/os_security_group_rule.py validate-modules:doc-missing-type plugins/modules/os_security_group_rule.py validate-modules:parameter-type-not-in-doc diff --git a/tests/unit/modules/cloud/openstack/test_os_routers_info.py b/tests/unit/modules/cloud/openstack/test_os_routers_info.py new file mode 100644 index 00000000..391a9c1c --- /dev/null +++ b/tests/unit/modules/cloud/openstack/test_os_routers_info.py @@ -0,0 +1,208 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import munch + +from mock import patch + +from ansible_collections.openstack.cloud.plugins.modules import os_routers_info +from ansible_collections.openstack.cloud.tests.unit.modules.utils import set_module_args, ModuleTestCase, AnsibleExitJson + + +def openstack_cloud_from_module(module, **kwargs): + return FakeSDK(), FakeCloud() + + +class FakeSDK(object): + class exceptions: + class OpenStackCloudException(Exception): + pass + + +class FakeCloud(object): + + def search_routers(self, name_or_id=None, filters=None): + test_routers = [ + { + "admin_state_up": True, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "created_at": "2019-12-19T20:16:18Z", + "description": "", + "distributed": False, + "external_gateway_info": None, + "flavor_id": None, + "ha": False, + "id": "d3f70ce4-7ab1-46a7-9bec-498c9d8a2483", + "name": "router1", + "project_id": "f48189aaee42429e8ed396e8b3f6a018", + "revision_number": 14, + "routes": [], + "status": "ACTIVE", + "tags": [], + "tenant_id": "f48189aaee42429e8ed396e8b3f6a018", + "updated_at": "2020-01-27T21:20:09Z" + }, + { + "admin_state_up": True, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "created_at": "2019-12-19T20:16:18Z", + "description": "", + "distributed": False, + "external_gateway_info": { + "enable_snat": True, + "external_fixed_ips": [ + { + "ip_address": "172.24.4.163", + "subnet_id": "b42b8057-5b3b-4aa3-949a-eaaee2032462" + }, + ], + "network_id": "fd6cc0f1-ed6f-426e-bb7b-a942b12633ad" + }, + "flavor_id": None, + "ha": False, + "id": "b869307c-a1f9-4956-a993-8a90fc7cc01d", + "name": "router2", + "project_id": "f48189aaee42429e8ed396e8b3f6a018", + "revision_number": 6, + "routes": [], + "status": "ACTIVE", + "tags": [], + "tenant_id": "f48189aaee42429e8ed396e8b3f6a018", + "updated_at": "2019-12-19T20:18:46Z" + }, + { + "admin_state_up": True, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "created_at": "2020-01-24T20:19:35Z", + "description": "", + "distributed": False, + "external_gateway_info": { + "enable_snat": True, + "external_fixed_ips": [ + { + "ip_address": "172.24.4.234", + "subnet_id": "b42b8057-5b3b-4aa3-949a-eaaee2032462" + }, + ], + "network_id": "fd6cc0f1-ed6f-426e-bb7b-a942b12633ad" + }, + "flavor_id": None, + "ha": False, + "id": "98bce30e-c912-4490-85eb-b22d650721e6", + "name": "router3", + "project_id": "f48189aaee42429e8ed396e8b3f6a018", + "revision_number": 4, + "routes": [], + "status": "ACTIVE", + "tags": [], + "tenant_id": "f48189aaee42429e8ed396e8b3f6a018", + "updated_at": "2020-01-26T10:21:31Z" + }, + ] + + if name_or_id is not None: + return [munch.Munch(router) for router in test_routers if router["name"] == name_or_id] + else: + return [munch.Munch(router) for router in test_routers] + + def list_router_interfaces(self, router): + test_ports = [ + { + "device_id": "d3f70ce4-7ab1-46a7-9bec-498c9d8a2483", + "device_owner": "network:router_interface", + "fixed_ips": [ + { + "ip_address": "192.168.1.254", + "subnet_id": "0624c75f-0574-41b5-a8d1-92e6e3a9e51d" + } + ], + "id": "92eeeca3-225d-46b8-a857-ede6c4f05484", + }, + { + "device_id": "b869307c-a1f9-4956-a993-8a90fc7cc01d", + "device_owner": "network:router_gateway", + "fixed_ips": [ + { + "ip_address": "172.24.4.10", + "subnet_id": "b42b8057-5b3b-4aa3-949a-eaaee2032462" + }, + ], + "id": "ab45060c-98fd-42a3-a1aa-8d5a03554bef", + }, + { + "device_id": "98bce30e-c912-4490-85eb-b22d650721e6", + "device_owner": "network:router_interface", + "fixed_ips": [ + { + "ip_address": "192.168.1.1", + "subnet_id": "0624c75f-0574-41b5-a8d1-92e6e3a9e51d" + } + ], + "id": "c9fb53f1-d43e-4588-a223-0e8bf8a79715", + }, + { + "device_id": "98bce30e-c912-4490-85eb-b22d650721e6", + "device_owner": "network:router_gateway", + "fixed_ips": [ + { + "ip_address": "172.24.4.234", + "subnet_id": "b42b8057-5b3b-4aa3-949a-eaaee2032462" + }, + ], + "id": "0271878e-4be8-433c-acdc-52823b41bcbf", + }, + ] + return [munch.Munch(port) for port in test_ports if port["device_id"] == router.id] + + +class TestRoutersInfo(ModuleTestCase): + '''This class calls the main function of the + os_routers_info module. + ''' + def setUp(self): + super(TestRoutersInfo, self).setUp() + self.module = os_routers_info + + def module_main(self, exit_exc): + with self.assertRaises(exit_exc) as exc: + self.module.main() + return exc.exception.args[0] + + @patch('ansible_collections.openstack.cloud.plugins.modules.os_routers_info.openstack_cloud_from_module', side_effect=openstack_cloud_from_module) + def test_main_with_router_interface(self, *args): + + set_module_args({'name': 'router1'}) + result = self.module_main(AnsibleExitJson) + self.assertIs(type(result.get('openstack_routers')[0].get('interfaces_info')), list) + self.assertEqual(len(result.get('openstack_routers')[0].get('interfaces_info')), 1) + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('port_id'), '92eeeca3-225d-46b8-a857-ede6c4f05484') + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('ip_address'), '192.168.1.254') + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('subnet_id'), '0624c75f-0574-41b5-a8d1-92e6e3a9e51d') + + @patch('ansible_collections.openstack.cloud.plugins.modules.os_routers_info.openstack_cloud_from_module', side_effect=openstack_cloud_from_module) + def test_main_with_router_gateway(self, *args): + + set_module_args({'name': 'router2'}) + result = self.module_main(AnsibleExitJson) + self.assertIs(type(result.get('openstack_routers')[0].get('interfaces_info')), list) + self.assertEqual(len(result.get('openstack_routers')[0].get('interfaces_info')), 0) + + @patch('ansible_collections.openstack.cloud.plugins.modules.os_routers_info.openstack_cloud_from_module', side_effect=openstack_cloud_from_module) + def test_main_with_router_interface_and_router_gateway(self, *args): + + set_module_args({'name': 'router3'}) + result = self.module_main(AnsibleExitJson) + self.assertIs(type(result.get('openstack_routers')[0].get('interfaces_info')), list) + self.assertEqual(len(result.get('openstack_routers')[0].get('interfaces_info')), 1) + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('port_id'), 'c9fb53f1-d43e-4588-a223-0e8bf8a79715') + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('ip_address'), '192.168.1.1') + self.assertEqual(result.get('openstack_routers')[0].get('interfaces_info')[0].get('subnet_id'), '0624c75f-0574-41b5-a8d1-92e6e3a9e51d') diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index a9772bea..69375f8e 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -40,3 +40,6 @@ httmock # requirment for kubevirt modules openshift ; python_version >= '2.7' + +# requirements for openstack collection +munch