diff --git a/ci/roles/port_forwarding/defaults/main.yml b/ci/roles/port_forwarding/defaults/main.yml new file mode 100644 index 00000000..54796c7f --- /dev/null +++ b/ci/roles/port_forwarding/defaults/main.yml @@ -0,0 +1,10 @@ +expected_fields: + - description + - external_port + - floatingip_id + - id + - internal_ip_address + - internal_port + - internal_port_id + - name + - protocol diff --git a/ci/roles/port_forwarding/tasks/main.yml b/ci/roles/port_forwarding/tasks/main.yml new file mode 100644 index 00000000..c1fbadc6 --- /dev/null +++ b/ci/roles/port_forwarding/tasks/main.yml @@ -0,0 +1,272 @@ +--- +- name: Create test network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: present + name: test_internal_network + +- name: Create test subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + name: test_internal_subnet + network_name: test_internal_network + cidr: 192.168.100.0/24 + gateway_ip: 192.168.100.1 + +- name: Create test port + openstack.cloud.port: + cloud: "{{ cloud }}" + state: present + name: test_internal_port + network: test_internal_network + fixed_ips: + - ip_address: 192.168.100.10 + register: test_internal_port + +- name: Create test external network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: present + name: test_external_network + external: true + +- name: Create test external subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: present + network_name: test_external_network + name: test_external_subnet + cidr: 10.6.6.0/24 + +- name: Create router + openstack.cloud.router: + cloud: "{{ cloud }}" + state: present + name: test_router + network: test_external_network + external_fixed_ips: + - subnet: test_external_subnet + interfaces: + - test_internal_subnet + +- name: Create test floating IP + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: present + network: test_external_network + register: test_floating_ip + +- name: Test - Create port forwarding rule + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + internal_ip: 192.168.100.10 + external_protocol_port: 8080 + internal_protocol_port: 80 + protocol: tcp + register: pf_create + +- name: Get port forwarding info + openstack.cloud.port_forwarding_info: + cloud: "{{ cloud }}" + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + port_forwarding_id: "{{ pf_create.port_forwarding.id }}" + register: pf_create_info + +- name: Verify - Port forwarding created successfully + assert: + that: + - pf_create is changed + - pf_create.port_forwarding is defined + - pf_create.port_forwarding.external_port == 8080 + - pf_create.port_forwarding.internal_port == 80 + - pf_create.port_forwarding.protocol == "tcp" + - pf_create_info.port_forwardings | length == 1 + - pf_create_info.port_forwardings.0.id == pf_create.port_forwarding.id + +- name: Test - Create port forwarding rule again (idempotency) + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + internal_ip: 192.168.100.10 + external_protocol_port: 8080 + internal_protocol_port: 80 + protocol: tcp + register: pf_idempotent + +- name: Verify - No changes + assert: + that: + - pf_idempotent is not changed + +- name: Test - Update port forwarding internal port + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + internal_ip: 192.168.100.10 + external_protocol_port: 8080 + internal_protocol_port: 8080 # Changed from 80 to 8080 + protocol: tcp + register: pf_update + +- name: Get port forwarding info + openstack.cloud.port_forwarding_info: + cloud: "{{ cloud }}" + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + port_forwarding_id: "{{ pf_update.port_forwarding.id }}" + register: pf_update_info + +- name: Verify - Port forwarding updated successfully + assert: + that: + - pf_update is changed + - pf_update.port_forwarding.internal_port == 8080 + - pf_update_info.port_forwardings | length == 1 + - pf_update_info.port_forwardings.0.id == pf_update.port_forwarding.id + +- name: Test - Update with same values (idempotency) + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + internal_ip: 192.168.100.10 + external_protocol_port: 8080 + internal_protocol_port: 8080 + protocol: tcp + register: pf_update_idempotent + +- name: Verify - No changes + assert: + that: + - pf_update_idempotent is not changed + +- name: Test - Change just one attribute + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + port_forwarding_id: "{{ pf_create.port_forwarding.id }}" + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + internal_protocol_port: 9090 # Different internal port + register: pf_update_by_id + +- name: Verify - Port forwarding updated by ID + assert: + that: + - pf_update_by_id.changed == true + - pf_update_by_id.port_forwarding.id == pf_create.port_forwarding.id + - pf_update_by_id.port_forwarding.internal_port_id == test_internal_port.port.id + - pf_update_by_id.port_forwarding.internal_ip_address == "192.168.100.10" + - pf_update_by_id.port_forwarding.external_port == 8080 + - pf_update_by_id.port_forwarding.internal_port == 9090 + +- name: Test - Create port forwarding without specifying internal IP + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: present + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + external_protocol_port: 2222 + internal_protocol_port: 22 + protocol: tcp + register: pf_auto_internal_ip + +- name: Verify - Port forwarding created with auto internal IP + assert: + that: + - pf_auto_internal_ip.changed == true + - pf_auto_internal_ip.port_forwarding.internal_ip_address == "192.168.100.10" + +- name: Test - Delete port forwarding + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: absent + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + network_port: test_internal_port + external_protocol_port: 8080 + internal_protocol_port: 9090 + protocol: tcp + register: pf_delete + +- name: Verify - Port forwarding deleted successfully + assert: + that: + - pf_delete.changed == true + +- name: Test - Delete port forwarding by ID + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: absent + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + port_forwarding_id: "{{ pf_auto_internal_ip.port_forwarding.id }}" + register: pf_delete_by_id + +- name: Verify - Port forwarding deleted by ID + assert: + that: + - pf_delete_by_id.changed == true + +- name: Test - Delete already deleted port forwarding (idempotency) + openstack.cloud.port_forwarding: + cloud: "{{ cloud }}" + state: absent + port_forwarding_id: "{{ pf_auto_internal_ip.port_forwarding.id }}" + floating_ip: "{{ test_floating_ip.floating_ip.id }}" + register: pf_delete_idempotent + +- name: Verify - No errors on deleting non-existent rule (idempotency) + assert: + that: + - pf_delete_idempotent is not changed + - pf_delete_idempotent is not failed + +- name: Clean up - Delete test floating IP + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: absent + floating_ip_address: "{{ test_floating_ip.floating_ip.floating_ip_address }}" + network: test_external_network + purge: true + +- name: Clean up - Delete router + openstack.cloud.router: + cloud: "{{ cloud }}" + state: absent + name: test_router + +- name: Clean up - Delete test external subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: test_external_subnet + +- name: Clean up - Delete test external network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: test_external_network + +- name: Clean up - Delete test port + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: test_internal_port + +- name: Clean up - Delete test subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: test_internal_subnet + +- name: Clean up - Delete test network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: test_internal_network diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 50e424f3..8c4b156d 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -38,6 +38,7 @@ - { role: object_container, tags: object_container } - { role: object_containers_info, tags: object_containers_info } - { role: port, tags: port } + - { role: port_forwarding, tags: port_forwarding } - { role: trait, tags: trait } - { role: trunk, tags: trunk } - { role: project, tags: project } diff --git a/meta/runtime.yml b/meta/runtime.yml index 9356b226..76b99045 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -60,6 +60,8 @@ action_groups: - object_container - object_containers_info - port + - port_forwarding + - port_forwarding_info - port_info - project - project_info diff --git a/plugins/modules/port_forwarding.py b/plugins/modules/port_forwarding.py new file mode 100644 index 00000000..2de110f2 --- /dev/null +++ b/plugins/modules/port_forwarding.py @@ -0,0 +1,249 @@ +# Copyright: (c) 2018, Terry Jones +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: port_forwarding +short_description: Create/Update/Delete port forwarding resources from OpenStack +description: + - Create, Update and Remove Neutron floating IP port forwarding resources from OpenStack + - Port forwarding allows external traffic to reach instances behind a floating IP +author: OpenStack Ansible SIG +options: + external_protocol_port: + description: + - The external port number on the floating IP that will be forwarded + - Must be between 1 and 65535 + - Required if C(port_forwarding_id) is set + type: int + aliases: ['external_port'] + floating_ip: + description: + - The floating IP address or ID to create port forwarding on + type: str + required: true + aliases: ['floating_ip_address'] + internal_ip: + description: + - The internal IP address to forward traffic to + - Must be one of the fixed IPs on the specified port + - If not specified, uses the first fixed IP of the port + - Requires C(network_port) + type: str + aliases: ['internal_ip_address'] + internal_protocol_port: + description: + - The internal port number to forward traffic to + - Must be between 1 and 65535 + - Required if C(port_forwarding_id) is set + type: int + aliases: ['internal_port'] + network_port: + description: + - The Neutron port name or ID that contains the internal IP + - Required if C(port_forwarding_id) is set + type: str + port_forwarding_id: + description: + - ID of an existing port forwarding resource + - Used for updates and deletions when ID is known + type: str + protocol: + description: + - The IP protocol for the port forwarding resource + - Supports tcp and udp protocols + - Required if C(port_forwarding_id) is set + type: str + state: + description: + - Whether the port forwarding resource should exist or not + type: str + choices: ['present', 'absent'] + default: present + +extends_documentation_fragment: + - openstack.cloud.openstack +''' + +EXAMPLES = r''' +- name: Create new port fowarding + openstack.cloud.port_forwarding: + state: present + floating_ip: 192.168.150.67 + external_protocol_port: 80 + internal_protocol_port: 8080 + network_port: example_http_port + protocol: tcp + +- name: Update previously created port forwarding + openstack.cloud.port_forwarding: + state: present + port_forwarding_id: existing_port_forwarding + floating_ip: 192.168.150.67 + internal_protocol_port: 9090 + +- name: Delete port forwarding + openstack.cloud.port_forwarding: + state: absent + port_forwarding_id: "resource-id" + floating_ip: "203.0.113.100" +''' + +RETURN = r''' +port_forwarding: + description: Dictionary describing the port forwarding resource. + type: list + elements: dict + returned: success + contains: + description: + description: The description of the port forwarding. + type: str + external_port: + description: The external port number. + type: int + floatingip_id: + description: The floating IP id associated with the port forwarding. + type: str + id: + description: The id of the port forwarding. + type: str + internal_ip_address: + description: The internal IP address associated with the port forwarding. + type: str + internal_port: + description: The internal port number. + type: int + internal_port_id: + description: The ID of the network port associated with the port forwarding. + type: str + protocol: + description: The IP protocol used for port forwarding. + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule +) + + +class PortForwardingModule(OpenStackModule): + argument_spec = dict( + external_protocol_port=dict(type='int', aliases=['external_port']), + floating_ip=dict(required=True, aliases=['floating_ip_address']), + internal_ip=dict(aliases=['internal_ip_address']), + internal_protocol_port=dict(type='int', aliases=['internal_port']), + network_port=dict(), + port_forwarding_id=dict(), + protocol=dict(), + state=dict(default='present', choices=['present', 'absent']), + ) + + module_kwargs = dict( + required_if=[ + ['port_forwarding_id', None, ['external_protocol_port', + 'internal_protocol_port', + 'network_port', + 'protocol'], False], + ], + required_by={ + 'internal_ip': ['network_port'], + }, + ) + + def run(self): + port_forwarding_id = self.params['port_forwarding_id'] + floating_ip = self.conn.network.find_ip(self.params['floating_ip'], + ignore_missing=False) + port = self.conn.network.find_port(self.params['network_port']) \ + if self.params['network_port'] else None + internal_ip = self._find_internal_ip(port) if port else None + external_port = self.params['external_protocol_port'] + internal_port = self.params['internal_protocol_port'] + protocol = self.params['protocol'] + state = self.params['state'] + + attrs = {} + if port is not None: + attrs['internal_port_id'] = port.id + + if internal_ip is not None: + attrs['internal_ip_address'] = internal_ip + + if external_port is not None: + attrs['external_port'] = external_port + + if protocol is not None: + attrs['protocol'] = protocol + + port_forwarding = self._find_port_forwarding(floating_ip.id, + port_forwarding_id, + attrs) + + if internal_port is not None: + attrs['internal_port'] = internal_port + + changed = False + if state == 'present': + if port_forwarding: + # found valid pfwd_id or pfwd with matching attributes + new_attrs = {k: v for k, v in attrs.items() if port_forwarding[k] != v} + if new_attrs: + port_forwarding = self.conn.network.update_port_forwarding( + port_forwarding.id, floating_ip.id, **new_attrs) + changed = True + + elif not port_forwarding_id: + # pfwd_id not given, so create new pfwd + attrs['floatingip_id'] = floating_ip.id + port_forwarding = self.conn.network.create_port_forwarding(**attrs) + changed = True + + self.exit_json(changed=changed, port_forwarding=port_forwarding) + + else: + if port_forwarding: + self.conn.network.delete_port_forwarding(port_forwarding.id, floating_ip.id) + changed = True + + self.exit_json(changed=changed) + + def _find_internal_ip(self, port): + internal_ip = self.params['internal_ip'] + if internal_ip: + for fixed_ip in port.fixed_ips: + if fixed_ip['ip_address'] == internal_ip: + return internal_ip + + self.fail_json( + msg='Internal IP %s not found in port %s fixed IPs' % (internal_ip, port.id)) + + else: + if port.fixed_ips: + return port.fixed_ips[0]['ip_address'] + + else: + self.fail_json(msg='Port %s has no fixed IPs available' % port.id) + + def _find_port_forwarding(self, fip_id, pf_id, attrs): + try: + if pf_id: + return self.conn.network.find_port_forwarding(pf_id, fip_id, ignore_missing=False) + + port_forwardings = list(self.conn.network.port_forwardings(fip_id, **attrs)) + if len(port_forwardings) > 1: + self.fail_json( + msg='Found more than one port forwarding resources with matching attributes') + + return port_forwardings[0] if len(port_forwardings) == 1 else None + except self.sdk.exceptions.NotFoundException: + return None + + +def main(): + module = PortForwardingModule() + module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/port_forwarding_info.py b/plugins/modules/port_forwarding_info.py new file mode 100644 index 00000000..aa41c3ef --- /dev/null +++ b/plugins/modules/port_forwarding_info.py @@ -0,0 +1,148 @@ +# Copyright: (c) 2018, Terry Jones +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: port_forwarding_info +short_description: Retrieve port forwarding resources from OpenStack. +description: + - Retrieve Neutron floating IP port forwarding resources from OpenStack. +author: OpenStack Ansible SIG +options: + external_port: + description: + - The external port number on the floating IP that will be forwarded. + type: int + floating_ip: + description: + - The address or ID of a floating IP that contains a port forwarding. + type: str + internal_port_id: + description: + - The Neutron port ID. + type: str + port_forwarding_id: + description: + - ID of an existing port forwarding resource. + type: str + protocol: + description: + - The IP protocol for the port forwarding resource. + type: str + +extends_documentation_fragment: + - openstack.cloud.openstack + +''' + +EXAMPLES = r''' +# Getting all port forwardings +- openstack.cloud.port_forwarding_info: + register: pfwds + +# Getting port forwardings by associated floating ip +- openstack.cloud.port_forwarding_info: + floating_ip: 192.168.42.67 + register: pfwds + +# Getting port forwarding by port forwarding id +- openstack.cloud.port_forwarding_info: + port_forwarding_id: d09f88d6-bb20-4268-9139-27c1b82c51d0 + register: pfwd +''' + +RETURN = r''' +port_forwardings: + description: The port forwarding objects list. + type: list + elements: dict + returned: success + contains: + description: + description: The description of the port forwarding. + type: str + external_port: + description: The external port number. + type: int + floatingip_id: + description: The floating IP id associated with the port forwarding. + type: str + id: + description: The id of the port forwarding. + type: str + internal_ip_address: + description: The internal IP address associated with the port forwarding. + type: str + internal_port: + description: The internal port number. + type: int + internal_port_id: + description: The ID of the network port associated with the port forwarding. + type: str + protocol: + description: The IP protocol used for port forwarding. + type: str +''' + +from ansible_collections.openstack.cloud.plugins.module_utils.openstack import ( + OpenStackModule +) + + +class PortForwardingInfoModule(OpenStackModule): + argument_spec = dict( + external_port=dict(type='int'), + floating_ip=dict(), + internal_port_id=dict(), + port_forwarding_id=dict(), + protocol=dict(), + ) + module_kwargs = dict( + supports_check_mode=True + ) + + def _find_port_forwardings(self): + port_forwarding_id = self.params['port_forwarding_id'] + floating_ip = self.params['floating_ip'] + query_kwargs = {k: self.params[k] + for k in ['external_port', + 'internal_port_id', + 'protocol'] + if self.params[k] is not None} + + floating_ips = None + if floating_ip: + fip = self.conn.network.find_ip(floating_ip) + floating_ips = [fip] if fip else [] + else: + floating_ips = self.conn.network.ips() + + port_forwardings = [] + if port_forwarding_id is None: + for fip in floating_ips: + pfwds = self.conn.network.port_forwardings(fip.id, **query_kwargs) + port_forwardings.extend(list(pfwds)) + + else: + for fip in floating_ips: + pfwd = self.conn.network.find_port_forwarding( + port_forwarding_id, fip.id, query_kwargs) + if pfwd: + return [pfwd] + + return port_forwardings + + def run(self): + port_forwardings = [pfwd.to_dict(computed=False) + for pfwd in self._find_port_forwardings()] + + self.exit(changed=False, port_forwardings=port_forwardings) + + +def main(): + module = PortForwardingInfoModule() + module() + + +if __name__ == '__main__': + main()