diff --git a/.zuul.yaml b/.zuul.yaml index 1479e9ba..6d13ea50 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -71,7 +71,7 @@ dns dns_zone_info endpoint - floating_ip_info + floating_ip host_aggregate identity_domain_info identity_group @@ -112,7 +112,6 @@ user_role volume # failing tags - # floating_ip # neutron_rbac - job: diff --git a/ci/roles/floating_ip/defaults/main.yml b/ci/roles/floating_ip/defaults/main.yml new file mode 100644 index 00000000..638b179f --- /dev/null +++ b/ci/roles/floating_ip/defaults/main.yml @@ -0,0 +1,21 @@ +--- +expected_fields: + - created_at + - description + - dns_domain + - dns_name + - fixed_ip_address + - floating_ip_address + - floating_network_id + - id + - name + - port_details + - port_id + - project_id + - qos_policy_id + - revision_number + - router_id + - status + - subnet_id + - tags + - updated_at diff --git a/ci/roles/floating_ip/tasks/main.yml b/ci/roles/floating_ip/tasks/main.yml index ebb41ec7..d846e28e 100644 --- a/ci/roles/floating_ip/tasks/main.yml +++ b/ci/roles/floating_ip/tasks/main.yml @@ -1,5 +1,4 @@ --- -# Prepare environment - name: Gather information about public network openstack.cloud.networks_info: cloud: "{{ cloud }}" @@ -12,109 +11,112 @@ - name: Create external network openstack.cloud.network: - cloud: "{{ cloud }}" - state: present - name: ansible_external - external: true + cloud: "{{ cloud }}" + state: present + name: ansible_external + external: true - name: Create external subnet openstack.cloud.subnet: - cloud: "{{ cloud }}" - state: present - network_name: ansible_external - name: ansible_external_subnet - cidr: 10.6.6.0/24 + cloud: "{{ cloud }}" + state: present + network_name: ansible_external + name: ansible_external_subnet + cidr: 10.6.6.0/24 - name: Create external port 1 openstack.cloud.port: - cloud: "{{ cloud }}" - state: present - name: ansible_external_port1 - network: ansible_external - fixed_ips: - - ip_address: 10.6.6.50 + cloud: "{{ cloud }}" + state: present + name: ansible_external_port1 + network: ansible_external + fixed_ips: + - ip_address: 10.6.6.50 - name: Create external port 2 openstack.cloud.port: - cloud: "{{ cloud }}" - state: present - name: ansible_external_port2 - network: ansible_external - fixed_ips: - - ip_address: 10.6.6.51 + cloud: "{{ cloud }}" + state: present + name: ansible_external_port2 + network: ansible_external + fixed_ips: + - ip_address: 10.6.6.51 - name: Create internal network openstack.cloud.network: - cloud: "{{ cloud }}" - state: present - name: ansible_internal - external: false + cloud: "{{ cloud }}" + state: present + name: ansible_internal + external: false - name: Create internal subnet openstack.cloud.subnet: - cloud: "{{ cloud }}" - state: present - network_name: ansible_internal - name: ansible_internal_subnet - cidr: 10.7.7.0/24 + cloud: "{{ cloud }}" + state: present + network_name: ansible_internal + name: ansible_internal_subnet + cidr: 10.7.7.0/24 - name: Create internal port 1 openstack.cloud.port: - cloud: "{{ cloud }}" - state: present - name: ansible_internal_port1 - network: ansible_internal - fixed_ips: - - ip_address: 10.7.7.100 + cloud: "{{ cloud }}" + state: present + name: ansible_internal_port1 + network: ansible_internal + fixed_ips: + - ip_address: 10.7.7.100 + register: port1 - name: Create internal port 2 openstack.cloud.port: - cloud: "{{ cloud }}" - state: present - name: ansible_internal_port2 - network: ansible_internal - fixed_ips: - - ip_address: 10.7.7.101 + cloud: "{{ cloud }}" + state: present + name: ansible_internal_port2 + network: ansible_internal + fixed_ips: + - ip_address: 10.7.7.101 + register: port2 - name: Create internal port 3 openstack.cloud.port: - cloud: "{{ cloud }}" - state: present - name: ansible_internal_port3 - network: ansible_internal - fixed_ips: - - ip_address: 10.7.7.102 + cloud: "{{ cloud }}" + state: present + name: ansible_internal_port3 + network: ansible_internal + fixed_ips: + - ip_address: 10.7.7.102 + register: port3 - name: Create router 1 openstack.cloud.router: - cloud: "{{ cloud }}" - state: present - name: ansible_router1 - network: ansible_external - external_fixed_ips: - - subnet: ansible_external_subnet - ip: 10.6.6.10 - interfaces: - - net: ansible_internal - subnet: ansible_internal_subnet - portip: 10.7.7.1 + cloud: "{{ cloud }}" + state: present + name: ansible_router1 + network: ansible_external + external_fixed_ips: + - subnet: ansible_external_subnet + ip: 10.6.6.10 + interfaces: + - net: ansible_internal + subnet: ansible_internal_subnet + portip: 10.7.7.1 # Router 2 is required for the simplest, first test that assigns a new floating IP to server # from first available external network or nova pool which is DevStack's public network - name: Create router 2 openstack.cloud.router: - cloud: "{{ cloud }}" - state: present - name: ansible_router2 - network: public - interfaces: - - net: ansible_internal - subnet: ansible_internal_subnet - portip: 10.7.7.10 + cloud: "{{ cloud }}" + state: present + name: ansible_router2 + network: public + interfaces: + - net: ansible_internal + subnet: ansible_internal_subnet + portip: 10.7.7.10 - name: Get all floating ips openstack.cloud.floating_ip_info: - cloud: "{{ cloud }}" + cloud: "{{ cloud }}" register: fips - name: Check if public network has any floating ips @@ -138,232 +140,286 @@ when: fips.floating_ips|length == 0 or "10.6.6.150" not in fips.floating_ips|map(attribute="floating_ip_address")|list -- name: Create server with one nic +- name: Create server 1 with one nic openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: ansible_server1 - image: "{{ image }}" - flavor: m1.tiny - nics: - # one nic only else simple, first floating ip test does not work - - port-name: ansible_internal_port1 - auto_ip: false - wait: true - -- name: Get info about server - openstack.cloud.server_info: cloud: "{{ cloud }}" - server: ansible_server1 - register: info + state: present + name: ansible_server1 + image: "{{ image }}" + flavor: m1.tiny + nics: + # one nic only else simple, first floating ip test does not work + - port-name: ansible_internal_port1 + auto_ip: false + wait: true + register: server1 -- name: Assert one internal port and no floating ips on server 1 +- name: Get server 1 ports + openstack.cloud.port_info: + cloud: "{{ cloud }}" + filters: + device_id: "{{ server1.server.id }}" + register: server1_ports + +- name: Assert one fixed ip on server 1 # If this assertion fails because server has an public ipv4 address (public_v4) then make sure # that no floating ip on public network is associated with "10.7.7.100" before running this role assert: that: - - info.servers|length == 1 - - info.servers.0.public_v4|length == 0 - - info.servers.0.public_v6|length == 0 - - info.servers.0.addresses.ansible_internal|length == 1 - - info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.100"] + - server1_ports.ports|length == 1 + - server1_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list == + ["10.7.7.100"] -- name: Create server with two nics +- name: Create server 2 with two nics openstack.cloud.server: - cloud: "{{ cloud }}" - state: present - name: ansible_server2 - image: "{{ image }}" - flavor: m1.tiny - nics: - - port-name: ansible_internal_port2 - - port-name: ansible_internal_port3 - auto_ip: false - wait: true - -- name: Get info about server - openstack.cloud.server_info: cloud: "{{ cloud }}" - server: ansible_server2 - register: info + state: present + name: ansible_server2 + image: "{{ image }}" + flavor: m1.tiny + nics: + - port-name: ansible_internal_port2 + - port-name: ansible_internal_port3 + auto_ip: false + wait: true + register: server2 -- name: Assert two internal ports and no floating ips on server 2 +- name: Get server 2 ports + openstack.cloud.port_info: + cloud: "{{ cloud }}" + filters: + device_id: "{{ server2.server.id }}" + register: server2_ports + +- name: Assert two fixed ips on server 2 assert: that: - - info.servers|length == 1 - - info.servers.0.public_v4|length == 0 - - info.servers.0.public_v6|length == 0 - - info.servers.0.addresses.ansible_internal|length == 2 - - info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == + - server2_ports.ports|length == 2 + - server2_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list == ["10.7.7.101", "10.7.7.102"] -# Tests - name: Assign new floating IP to server from first available external network or nova pool openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: present - server: ansible_server1 - wait: true - -- name: Get info about server - openstack.cloud.server_info: cloud: "{{ cloud }}" + state: present server: ansible_server1 - register: info + wait: yes -- name: Assert one internal port and one floating ip on server 1 +- name: Get floating ip attached to server 1 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port1.port.id }}" + register: server1_fips + # openstacksdk has issues with waiting hence we simply retry + retries: 10 + delay: 3 + until: server1_fips.floating_ips|length == 1 + +- name: Assert fixed ip and floating ip attached to server 1 assert: that: - - info.servers.0.addresses.ansible_internal|length == 2 - - info.servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == - ["fixed", "floating"] + - server1_ports.ports|length == 1 + - server1_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list == + ["10.7.7.100"] + - server1_fips.floating_ips|length == 1 + - server1_fips.floating_ips|map(attribute='fixed_ip_address')|sort|list == + ["10.7.7.100"] -- name: Detach floating IP from server +- name: Assert return values of floating_ip_info module + assert: + that: + - server1_fips is success + - server1_fips is not changed + - server1_fips.floating_ips + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(server1_fips.floating_ips[0].keys())|length == 0 + +- name: Assign floating ip to server 1 again openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: absent - server: ansible_server1 - network: public - floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| - selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}" - -- name: Get info about server - openstack.cloud.server_info: cloud: "{{ cloud }}" + state: present server: ansible_server1 - register: info - # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. + wait: true + register: floating_ip + +- name: Assert floating ip on server 1 has not changed + assert: + that: floating_ip is not changed + +- name: Assert return values of floating_ip module + assert: + that: + - floating_ip.floating_ip + # allow new fields to be introduced but prevent fields from being removed + - expected_fields|difference(floating_ip.floating_ip.keys())|length == 0 + +- name: Detach floating ip from server 1 + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: absent + server: ansible_server1 + network: public + floating_ip_address: "{{ server1_fips.floating_ips.0.floating_ip_address }}" + +- name: Wait until floating ip is detached from server 1 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port1.port.id }}" + register: server1_fips + # When detaching a floating ip from an instance there might be a delay until it is not listed anymore retries: 10 delay: 3 - until: info.servers.0.addresses.ansible_internal|length == 1 + until: server1_fips.floating_ips|length == 0 -- name: Assert one internal port on server 1 - assert: - that: - - info.servers.0.addresses.ansible_internal|length == 1 - - info.servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.100"] +- name: Find all floating ips for debugging + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + register: fips -- name: Assign floating IP to server - openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: present - reuse: yes - server: ansible_server2 - network: public - fixed_address: 10.7.7.101 - wait: true +- name: Print all floating ips for debugging + debug: var=fips -- name: Get info about server +- name: Find all servers for debugging openstack.cloud.server_info: cloud: "{{ cloud }}" - server: ansible_server2 - register: info + register: servers -- name: Assert two internal ports and one floating ip on server 2 +- name: Print all servers for debugging + debug: var=servers + +- name: Assign floating ip to server 2 + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: present + reuse: no # else fixed_address will be ignored + server: ansible_server2 + network: public + fixed_address: "{{ port2.port.fixed_ips[0].ip_address }}" + wait: true + register: server2_fip + +- name: Assert floating ip attached to server 2 assert: that: - - info.servers.0.addresses.ansible_internal|length == 3 - - info.servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == - ["fixed", "fixed", "floating"] + - server2_fip.floating_ip -- name: Assign a second, specific floating IP to server - openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: present - reuse: yes - server: ansible_server2 - network: ansible_external - fixed_address: 10.7.7.102 - floating_ip_address: "10.6.6.150" +- name: Find all floating ips for debugging + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + register: fips -# We cannot wait for second floating ip to be attached because OpenStackSDK checks only for first floating ip -# Ref.: https://github.com/openstack/openstacksdk/blob/e0372b72af8c5f471fc17e53434d7a814ca958bd/openstack/cloud/_floating_ip.py#L733 +- name: Print all floating ips for debugging + debug: var=fips -- name: Get info about server +- name: Find all servers for debugging openstack.cloud.server_info: cloud: "{{ cloud }}" + register: servers + +- name: Print all servers for debugging + debug: var=servers + +- name: Get floating ip attached to server 2 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port2.port.id }}" + register: server2_fips + +- name: Assert floating ip attached to server 2 + assert: + that: + - server2_fips.floating_ips|length == 1 + - server2_fips.floating_ips|map(attribute='fixed_ip_address')|sort|list == + ["10.7.7.101"] + +- name: Assign a second, specific floating ip to server 2 + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: present + reuse: no # else fixed_address will be ignored server: ansible_server2 - register: info - # retry because we cannot wait for second floating ip + network: ansible_external + fixed_address: "{{ port3.port.fixed_ips[0].ip_address }}" + floating_ip_address: "10.6.6.150" + wait: no # does not work anyway and causes issues in local testing + +- name: Get floating ip attached to server 2 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port3.port.id }}" + register: server2_fips + # We cannot wait for second floating ip to be attached because OpenStackSDK checks only for first floating ip + # Ref.: https://github.com/openstack/openstacksdk/blob/e0372b72af8c5f471fc17e53434d7a814ca958bd/openstack/cloud/_floating_ip.py#L733 retries: 10 delay: 3 - until: info.servers.0.addresses.ansible_internal|length == 4 + until: server2_fips.floating_ips|length == 1 -- name: Assert two internal ports and two floating ips on server 2 +- name: Assert second floating ip attached to server 2 assert: that: - - info.servers.0.addresses.ansible_internal|length == 4 - - ("10.6.6.150" in info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list) + - server2_fips.floating_ips|length == 1 + - server2_fips.floating_ips|map(attribute='fixed_ip_address')|sort|list == + ["10.7.7.102"] -- name: Detach second floating IP from server +- name: Detach second floating ip from server 2 openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: absent - server: ansible_server2 - network: ansible_external - floating_ip_address: "10.6.6.150" - -- name: Get info about server - openstack.cloud.server_info: cloud: "{{ cloud }}" + state: absent server: ansible_server2 - register: info - # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. + network: ansible_external + floating_ip_address: "10.6.6.150" + +- name: Wait until second floating ip is detached from server 2 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port3.port.id }}" + register: server2_fips + # When detaching a floating ip from an instance there might be a delay until it is not listed anymore retries: 10 delay: 3 - until: info.servers.0.addresses.ansible_internal|length == 3 + until: server2_fips.floating_ips|length == 0 -- name: Assert two internal ports and one floating ip on server 2 - assert: - that: - - info.servers.0.addresses.ansible_internal|length == 3 - -- name: Detach remaining floating IP from server - openstack.cloud.floating_ip: - cloud: "{{ cloud }}" - state: absent - server: ansible_server2 - network: public - floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| - selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}" - -- name: Get info about server - openstack.cloud.server_info: +- name: Get first floating ip attached to server 2 + openstack.cloud.floating_ip_info: cloud: "{{ cloud }}" + port: "{{ port2.port.id }}" + register: server2_fips + +- name: Detach remaining floating ip from server 2 + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: absent server: ansible_server2 - register: info - # When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info - # does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary. + network: public + floating_ip_address: "{{ server2_fips.floating_ips.0.floating_ip_address }}" + +- name: Wait until first floating ip is detached from server 2 + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + port: "{{ port2.port.id }}" + register: server2_fips + # When detaching a floating ip from an instance there might be a delay until it is not listed anymore retries: 10 delay: 3 - until: info.servers.0.addresses.ansible_internal|length == 2 + until: server2_fips.floating_ips|length == 0 -- name: Assert two internal ports on server 2 - assert: - that: - - info.servers.0.addresses.ansible_internal|length == 2 - - info.servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.101", "10.7.7.102"] - -# Clean environment - name: Delete server with two nics openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: ansible_server2 - wait: true + cloud: "{{ cloud }}" + state: absent + name: ansible_server2 + wait: true - name: Delete server with one nic openstack.cloud.server: - cloud: "{{ cloud }}" - state: absent - name: ansible_server1 - wait: true + cloud: "{{ cloud }}" + state: absent + name: ansible_server1 + wait: true - name: Get all floating ips openstack.cloud.floating_ip_info: - cloud: "{{ cloud }}" + cloud: "{{ cloud }}" register: fips # TODO: Replace with appropriate Ansible module once available @@ -381,8 +437,8 @@ - name: Get remaining floating ips on external network openstack.cloud.floating_ip_info: - cloud: "{{ cloud }}" - floating_network: ansible_external + cloud: "{{ cloud }}" + floating_network: ansible_external register: fips # TODO: Replace with appropriate Ansible module once available @@ -396,71 +452,71 @@ # Remove routers after floating ips have been detached and disassociated else removal fails with # Error detaching interface from router ***: Client Error for url: ***, -# Router interface for subnet *** on router *** cannot be deleted, +# Router interface for subnet *** on router *** cannot be deleted, # as it is required by one or more floating IPs. - name: Delete router 2 openstack.cloud.router: - cloud: "{{ cloud }}" - state: absent - name: ansible_router2 + cloud: "{{ cloud }}" + state: absent + name: ansible_router2 - name: Delete router 1 openstack.cloud.router: - cloud: "{{ cloud }}" - state: absent - name: ansible_router1 + cloud: "{{ cloud }}" + state: absent + name: ansible_router1 - name: Delete internal port 3 openstack.cloud.port: - cloud: "{{ cloud }}" - state: absent - name: ansible_internal_port3 + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port3 - name: Delete internal port 2 openstack.cloud.port: - cloud: "{{ cloud }}" - state: absent - name: ansible_internal_port2 + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port2 - name: Delete internal port 1 openstack.cloud.port: - cloud: "{{ cloud }}" - state: absent - name: ansible_internal_port1 + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port1 - name: Delete internal subnet openstack.cloud.subnet: - cloud: "{{ cloud }}" - state: absent - name: ansible_internal_subnet + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_subnet - name: Delete internal network openstack.cloud.network: - cloud: "{{ cloud }}" - state: absent - name: ansible_internal + cloud: "{{ cloud }}" + state: absent + name: ansible_internal - name: Delete external port 2 openstack.cloud.port: - cloud: "{{ cloud }}" - state: absent - name: ansible_external_port2 + cloud: "{{ cloud }}" + state: absent + name: ansible_external_port2 - name: Delete external port 1 openstack.cloud.port: - cloud: "{{ cloud }}" - state: absent - name: ansible_external_port1 + cloud: "{{ cloud }}" + state: absent + name: ansible_external_port1 - name: Delete external subnet openstack.cloud.subnet: - cloud: "{{ cloud }}" - state: absent - name: ansible_external_subnet + cloud: "{{ cloud }}" + state: absent + name: ansible_external_subnet - name: Delete external network openstack.cloud.network: - cloud: "{{ cloud }}" - state: absent - name: ansible_external + cloud: "{{ cloud }}" + state: absent + name: ansible_external diff --git a/ci/roles/floating_ip_info/tasks/main.yml b/ci/roles/floating_ip_info/tasks/main.yml deleted file mode 100644 index 916c2cb2..00000000 --- a/ci/roles/floating_ip_info/tasks/main.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -- name: Getting info about allocated ips - openstack.cloud.floating_ip_info: - cloud: "{{ cloud }}" - register: fips - -- name: assert result - assert: - that: - - fips is success - - fips is not changed - -- name: assert fields - when: fips.floating_ips|length > 0 - assert: - that: - # allow new fields to be introduced but prevent fields from being removed - - '["created_at", "description", "dns_domain", "dns_name", "fixed_ip_address", "floating_ip_address", - "floating_network_id", "id", "name", "port_details", "port_id", "project_id", "qos_policy_id", - "revision_number", "router_id", "status", "subnet_id", "tags", "updated_at"]| - difference(fips.floating_ips.0.keys())|length == 0' diff --git a/ci/run-collection.yml b/ci/run-collection.yml index 31b8e99a..70c37784 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -17,7 +17,7 @@ tags: dns when: sdk_version is version(0.28, '>=') - { role: endpoint, tags: endpoint } - - { role: floating_ip_info, tags: floating_ip_info } + - { role: floating_ip, tags: floating_ip } - { role: host_aggregate, tags: host_aggregate } - { role: identity_domain_info, tags: identity_domain_info } - { role: identity_group, tags: identity_group } @@ -67,5 +67,4 @@ - { role: volume, tags: volume } - role: loadbalancer tags: loadbalancer - - { role: floating_ip, tags: floating_ip } - { role: quota, tags: quota } diff --git a/plugins/modules/floating_ip.py b/plugins/modules/floating_ip.py index 29e18a78..fbfda7f1 100644 --- a/plugins/modules/floating_ip.py +++ b/plugins/modules/floating_ip.py @@ -12,66 +12,59 @@ short_description: Add/Remove floating IP from an instance description: - Add or Remove a floating IP to an instance. - Returns the floating IP when attaching only if I(wait=true). - - When detaching a floating IP there might be a delay until an instance does not list the floating IP any more. + - When detaching a floating IP there might be a delay until an instance + does not list the floating IP any more. options: - server: - description: - - The name or ID of the instance to which the IP address - should be assigned. - required: true - type: str - network: - description: - - The name or ID of a neutron external network or a nova pool name. - type: str - floating_ip_address: - description: - - A floating IP address to attach or to detach. When I(state) is present - can be used to specify a IP address to attach. I(floating_ip_address) - requires I(network) to be set. - type: str - reuse: - description: - - When I(state) is present, and I(floating_ip_address) is not present, - this parameter can be used to specify whether we should try to reuse - a floating IP address already allocated to the project. - type: bool - default: 'no' fixed_address: description: - To which fixed IP of server the floating IP address should be attached to. type: str + floating_ip_address: + description: + - A floating IP address to attach or to detach. When I(state) is + present can be used to specify a IP address to attach. + I(floating_ip_address) requires I(network) to be set. + type: str nat_destination: description: - The name or id of a neutron private network that the fixed IP to attach floating IP is on aliases: ["fixed_network", "internal_network"] type: str - wait: + network: description: - - When attaching a floating IP address, specify whether to wait for it to appear as attached. - - Must be set to C(yes) for the module to return the value of the floating IP when attaching. + - The name or ID of a neutron external network or a nova pool name. + type: str + purge: + description: + - When I(state) is absent, indicates whether or not to delete the + floating IP completely, or only detach it from the server. + Default is to detach only. type: bool default: 'no' - timeout: + reuse: description: - - Time to wait for an IP address to appear as attached. See wait. - required: false - default: 60 - type: int + - When I(state) is present, and I(floating_ip_address) is not present, + this parameter can be used to specify whether we should try to reuse + a floating IP address already allocated to the project. + - When I(reuse) is C(true), I(network) is defined and + I(floating_ip_address) is undefined, then C(nat_destination) and + C(fixed_address) will be ignored. + type: bool + default: 'no' + server: + description: + - The name or ID of the instance to which the IP address + should be assigned. + required: true + type: str state: description: - Should the resource be present or absent. choices: [present, absent] default: present type: str - purge: - description: - - When I(state) is absent, indicates whether or not to delete the floating - IP completely, or only detach it from the server. Default is to detach only. - type: bool - default: 'no' requirements: - "python >= 3.6" - "openstacksdk" @@ -120,182 +113,384 @@ EXAMPLES = ''' server: cattle001 ''' +RETURN = ''' +floating_ip: + description: Dictionary describing the floating ip address. + type: dict + returned: success + contains: + created_at: + description: Timestamp at which the floating IP was assigned. + type: str + description: + description: The description of a floating IP. + type: str + dns_domain: + description: The DNS domain. + type: str + dns_name: + description: The DNS name. + type: str + fixed_ip_address: + description: The fixed IP address associated with a floating IP address. + type: str + floating_ip_address: + description: The IP address of a floating IP. + type: str + floating_network_id: + description: The id of the network associated with a floating IP. + type: str + id: + description: Id of the floating ip. + type: str + name: + description: Name of the floating ip. + type: str + port_details: + description: | + The details of the port that this floating IP associates + with. Present if C(fip-port-details) extension is loaded. + type: dict + port_id: + description: The port ID floating ip associated with. + type: str + project_id: + description: The ID of the project this floating IP is associated with. + type: str + qos_policy_id: + description: The ID of the QoS policy attached to the floating IP. + type: str + revision_number: + description: Revision number. + type: str + router_id: + description: The id of the router floating ip associated with. + type: str + status: + description: | + The status of a floating IP, which can be 'ACTIVE' or 'DOWN'. + type: str + subnet_id: + description: The id of the subnet the floating ip associated with. + type: str + tags: + description: List of tags. + type: list + elements: str + updated_at: + description: Timestamp at which the floating IP was last updated. + type: str +''' + from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule -import itertools class NetworkingFloatingIPModule(OpenStackModule): argument_spec = dict( + fixed_address=dict(), + floating_ip_address=dict(), + nat_destination=dict(aliases=['fixed_network', 'internal_network']), + network=dict(), + purge=dict(type='bool', default=False), + reuse=dict(type='bool', default=False), server=dict(required=True), state=dict(default='present', choices=['absent', 'present']), - network=dict(), - floating_ip_address=dict(), - reuse=dict(type='bool', default=False), - fixed_address=dict(), - nat_destination=dict(aliases=['fixed_network', 'internal_network']), - wait=dict(type='bool', default=False), - timeout=dict(type='int', default=60), - purge=dict(type='bool', default=False), ) module_kwargs = dict( required_if=[ ['state', 'absent', ['floating_ip_address']] ], - required_by=dict( - floating_ip_address=('network',) - ) + required_by={ + 'floating_ip_address': ('network'), + } ) - def _get_floating_ip(self, floating_ip_address): - f_ips = self.conn.search_floating_ips( - filters={'floating_ip_address': floating_ip_address}) + def run(self): + self._init() + if self.params['state'] == 'present': + self._create_and_attach() - if not f_ips: - return None + else: # self.params['state'] == 'absent' + self._detach_and_delete() - return f_ips[0] + def _create_and_attach(self): + changed = False + fixed_address = self.params['fixed_address'] + floating_ip_address = self.params['floating_ip_address'] + nat_destination_name_or_id = self.params['nat_destination'] + network_id = self.network['id'] if self.network else None - def _list_floating_ips(self, server): - return itertools.chain.from_iterable([ - (addr['addr'] for addr in server.addresses[net] if addr['OS-EXT-IPS:type'] == 'floating') - for net in server.addresses - ]) + ips = self._find_ips( + server=self.server, + floating_ip_address=floating_ip_address, + network_id=network_id, + fixed_address=fixed_address, + nat_destination_name_or_id=nat_destination_name_or_id) - def _match_floating_ip(self, server, - floating_ip_address, - network_id, - fixed_address, - nat_destination): + # First floating ip satisfies our requirements + ip = ips[0] if ips else None if floating_ip_address: - return self._get_floating_ip(floating_ip_address) - elif not fixed_address and nat_destination: - nat_destination_name = self.conn.get_network(nat_destination)['name'] - return next( - (self._get_floating_ip(addr['addr']) - for addr in server.addresses.get(nat_destination_name, []) - if addr['OS-EXT-IPS:type'] == 'floating'), - None) - else: - # not floating_ip_address and (fixed_address or not nat_destination) + # A specific floating ip address has been requested - # get any of the floating ips that matches fixed_address and/or network - f_ip_addrs = self._list_floating_ips(server) - f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] - return next( - (f_ip for f_ip in f_ips - if ((fixed_address and f_ip.fixed_ip_address == fixed_address) or not fixed_address) - and ((network_id and f_ip.network == network_id) or not network_id)), - None) + if not ip: + # If a specific floating ip address has been requested + # and it does not exist yet then create it - def run(self): - server_name_or_id = self.params['server'] - state = self.params['state'] - network = self.params['network'] - floating_ip_address = self.params['floating_ip_address'] - reuse = self.params['reuse'] - fixed_address = self.params['fixed_address'] - nat_destination = self.params['nat_destination'] - wait = self.params['wait'] - timeout = self.params['timeout'] - purge = self.params['purge'] - - server = self.conn.get_server(server_name_or_id) - if not server: - self.fail_json( - msg="server {0} not found".format(server_name_or_id)) - - # Extract floating ips from server - f_ip_addrs = self._list_floating_ips(server) - - # Get details about requested floating ip - f_ip = self._get_floating_ip(floating_ip_address) if floating_ip_address else None - - if network: - network_id = self.conn.get_network(name_or_id=network)["id"] - else: - network_id = None - - if state == 'present': - if floating_ip_address and f_ip and floating_ip_address in f_ip_addrs: - # Floating ip address has been assigned to server - self.exit_json(changed=False, floating_ip=f_ip) - - if f_ip and f_ip['attached'] and floating_ip_address not in f_ip_addrs: - # Requested floating ip has been attached to different server - self.fail_json(msg="floating-ip {floating_ip_address} already has been attached to different server" - .format(floating_ip_address=floating_ip_address)) - - if not floating_ip_address: - # No specific floating ip requested, i.e. if any floating ip is already assigned to server, - # check that it matches requirements. - - if not fixed_address and nat_destination: - # Check if we have any floating ip on the given nat_destination network - nat_destination_name = self.conn.get_network(nat_destination)['name'] - for addr in server.addresses.get(nat_destination_name, []): - if addr['OS-EXT-IPS:type'] == 'floating': - # A floating ip address has been assigned to the requested nat_destination - f_ip = self._get_floating_ip(addr['addr']) - self.exit_json(changed=False, floating_ip=f_ip) - # else fixed_address or not nat_destination, hence an - # analysis of all floating ips of server is required - f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] - for f_ip in f_ips: - if network_id and f_ip.network != network_id: - # requested network does not match network of floating ip - continue - - if not fixed_address and not nat_destination: - # any floating ip will fullfil these requirements - self.exit_json(changed=False, floating_ip=f_ip) - - if fixed_address and f_ip.fixed_ip_address == fixed_address: - # a floating ip address has been assigned that points to the requested fixed_address - self.exit_json(changed=False, floating_ip=f_ip) - - if floating_ip_address and not f_ip: - # openstacksdk's create_ip requires floating_ip_address and floating_network_id to be set - self.conn.network.create_ip(floating_ip_address=floating_ip_address, floating_network_id=network_id) - # Else floating ip either does not exist or has not been attached yet - - # Both floating_ip_address and network are mutually exclusive in add_ips_to_server, i.e. - # add_ips_to_server will ignore floating_ip_address if network is set - # Ref.: https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L987 - server = self.conn.add_ips_to_server( - server=server, - ips=floating_ip_address, - ip_pool=network if not floating_ip_address else None, - reuse=reuse, - fixed_address=fixed_address, - wait=wait, - timeout=timeout, nat_destination=nat_destination) - - # Update the floating ip status - f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) - self.exit_json(changed=True, floating_ip=f_ip) - - elif state == 'absent': - f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) - if not f_ip: - # Nothing to detach - self.exit_json(changed=False) - changed = False - - if f_ip["fixed_ip_address"]: - self.conn.detach_ip_from_server(server_id=server['id'], floating_ip_id=f_ip['id']) - # OpenStackSDK sets {"port_id": None} to detach a floating ip from an instance, - # but there might be a delay until a server does not list it in addresses any more. - - # Update the floating IP status - f_ip = self.conn.get_floating_ip(id=f_ip['id']) + # openstacksdk's create_ip requires floating_ip_address + # and floating_network_id to be set + self.conn.network.create_ip( + floating_ip_address=floating_ip_address, + floating_network_id=network_id) changed = True - if purge: - self.conn.delete_floating_ip(f_ip['id']) - self.exit_json(changed=True) - self.exit_json(changed=changed, floating_ip=f_ip) + else: # ip + # Requested floating ip address exists already + + if ip.port_details and (ip.port_details.status == 'ACTIVE') \ + and (floating_ip_address not in self._filter_ips( + self.server)): + # Floating ip address exists and has been attached + # but to a different server + + # Requested ip has been attached to different server + self.fail_json( + msg="Floating ip {0} has been attached to different " + "server".format(floating_ip_address)) + + if not ip \ + or floating_ip_address not in self._filter_ips(self.server): + # Requested floating ip address does not exist or has not been + # assigned to server + + self.conn.add_ip_list( + server=self.server, + ips=[floating_ip_address], + wait=self.params['wait'], + timeout=self.params['timeout'], + fixed_address=fixed_address) + changed = True + else: + # Requested floating ip address has been assigned to server + pass + + elif not ips: # and not floating_ip_address + # No specific floating ip has been requested and none of the + # floating ips which have been assigned to the server matches + # requirements + + # add_ips_to_server() will handle several scenarios: + # + # If a specific floating ip address has been requested then it + # will be attached to the server. The floating ip address has + # either been created in previous steps or it already existed. + # Ref.: https://github.com/openstack/openstacksdk/blob/ + # 9d3ee1d32149ba2a8bb3dc894295e180746cdddc/openstack/cloud + # /_floating_ip.py#L985 + # + # If no specific floating ip address has been requested, reuse + # is allowed and a network has been given (with ip_pool) from + # which floating ip addresses will be drawn, then any existing + # floating ip address from ip_pool=network which is not + # attached to any other server will be attached to the server. + # If no such floating ip address exists or if reuse is not + # allowed, then a new floating ip address will be created + # within ip_pool=network and attached to the server. + # Ref.: https://github.com/openstack/openstacksdk/blob/ + # 9d3ee1d32149ba2a8bb3dc894295e180746cdddc/openstack/cloud/ + # _floating_ip.py#L981 + # + # If no specific floating ip address has been requested and no + # network has been given (with ip_pool) from which floating ip + # addresses will be taken, then a floating ip address might be + # added to the server, refer to _needs_floating_ip() for + # details. + # Ref.: + # * https://github.com/openstack/openstacksdk/blob/ + # 9d3ee1d32149ba2a8bb3dc894295e180746cdddc/openstack/cloud/\ + # _floating_ip.py#L989 + # * https://github.com/openstack/openstacksdk/blob/ + # 9d3ee1d32149ba2a8bb3dc894295e180746cdddc/openstack/cloud/ + # _floating_ip.py#L995 + # + # Both floating_ip_address and network are mutually exclusive + # in add_ips_to_server(), i.e.add_ips_to_server will ignore + # floating_ip_address if network is not None. To prefer + # attaching a specific floating ip address over assigning any + # fip, ip_pool is only defined if floating_ip_address is None. + # Ref.: https://github.com/openstack/openstacksdk/blob/ + # a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/ + # _floating_ip.py#L987 + self.conn.add_ips_to_server( + server=self.server, + ip_pool=network_id, + ips=None, # No specific floating ip requested + reuse=self.params['reuse'], + fixed_address=fixed_address, + wait=self.params['wait'], + timeout=self.params['timeout'], + nat_destination=nat_destination_name_or_id) + changed = True + else: + # Found one or more floating ips which satisfy requirements + pass + + if changed: + # update server details such as addresses + self.server = self.conn.compute.get_server(self.server) + + # Update the floating ip resource + ips = self._find_ips( + self.server, floating_ip_address, network_id, + fixed_address, nat_destination_name_or_id) + + # ips can be empty, e.g. when server has no private ipv4 + # address to which a floating ip address can be attached + + self.exit_json( + changed=changed, + floating_ip=ips[0].to_dict(computed=False) if ips else None) + + def _detach_and_delete(self): + ips = self._find_ips( + server=self.server, + floating_ip_address=self.params['floating_ip_address'], + network_id=self.network['id'] if self.network else None, + fixed_address=self.params['fixed_address'], + nat_destination_name_or_id=self.params['nat_destination']) + + if not ips: + # Nothing to detach + self.exit_json(changed=False) + + changed = False + for ip in ips: + if ip['fixed_ip_address']: + # Silently ignore that ip might not be attached to server + self.conn.compute.remove_floating_ip_from_server( + self.server, ip['floating_ip_address']) + + # OpenStackSDK sets {"port_id": None} to detach a floating + # ip from an instance, but there might be a delay until a + # server does not list it in addresses any more. + changed = True + + if self.params['purge']: + self.conn.network.delete_ip(ip['id']) + changed = True + + self.exit_json(changed=changed) + + def _filter_ips(self, server): + # Extract floating ips from server + + def _flatten(lists): + return [item for sublist in lists for item in sublist] + + if server['addresses'] is None: + # fetch server with details + server = self.conn.compute.get_server(server) + + if not server['addresses']: + return [] + + # Returns a list not an iterator here because + # it is iterated several times below + return [address['addr'] + for address in _flatten(server['addresses'].values()) + if address['OS-EXT-IPS:type'] == 'floating'] + + def _find_ips(self, + server, + floating_ip_address, + network_id, + fixed_address, + nat_destination_name_or_id): + # Check which floating ips matches our requirements. + # They might or might not be attached to our server. + if floating_ip_address: + # A specific floating ip address has been requested + ip = self.conn.network.find_ip(floating_ip_address) + return [ip] if ip else [] + elif (not fixed_address and nat_destination_name_or_id): + # No specific floating ip and no specific fixed ip have been + # requested but a private network (nat_destination) has been + # given where the floating ip should be attached to. + return self._find_ips_by_nat_destination( + server, nat_destination_name_or_id) + else: + # not floating_ip_address + # and (fixed_address or not nat_destination_name_or_id) + + # An analysis of all floating ips of server is required + return self._find_ips_by_network_id_and_fixed_address( + server, fixed_address, network_id) + + def _find_ips_by_nat_destination(self, + server, + nat_destination_name_or_id): + + if not server['addresses']: + return None + + # Check if we have any floating ip on + # the given nat_destination network + nat_destination = self.conn.network.find_network( + nat_destination_name_or_id, ignore_missing=False) + + fips_with_nat_destination = [ + addr for addr + in server['addresses'].get(nat_destination['name'], []) + if addr['OS-EXT-IPS:type'] == 'floating'] + + if not fips_with_nat_destination: + return None + + # One or more floating ip addresses have been assigned + # to the requested nat_destination; return the first. + return [self.conn.network.find_ip(fip['addr'], ignore_missing=False) + for fip in fips_with_nat_destination] + + def _find_ips_by_network_id_and_fixed_address(self, + server, + fixed_address=None, + network_id=None): + # Get any of the floating ips that matches fixed_address and/or network + ips = [ip for ip in self.conn.network.ips() + if ip['floating_ip_address'] in self._filter_ips(server)] + + matching_ips = [] + for ip in ips: + if network_id and ip['floating_network_id'] != network_id: + # Requested network does not + # match network of floating ip + continue + + if not fixed_address: # and not nat_destination_name_or_id + # Any floating ip will fullfil these requirements + matching_ips.append(ip) + + if (fixed_address and ip['fixed_ip_address'] == fixed_address): + # A floating ip address has been assigned that + # points to the requested fixed_address + matching_ips.append(ip) + + return matching_ips + + def _init(self): + server_name_or_id = self.params['server'] + server = self.conn.compute.find_server(server_name_or_id, + ignore_missing=False) + # fetch server details such as addresses + self.server = self.conn.compute.get_server(server) + + network_name_or_id = self.params['network'] + if network_name_or_id: + self.network = self.conn.network.find_network( + name_or_id=network_name_or_id, ignore_missing=False) + else: + self.network = None def main(): diff --git a/plugins/modules/floating_ip_info.py b/plugins/modules/floating_ip_info.py index e108a814..f15eaef1 100644 --- a/plugins/modules/floating_ip_info.py +++ b/plugins/modules/floating_ip_info.py @@ -32,17 +32,18 @@ options: description: - The name or id of the port to which a floating IP is associated. type: str - project_id: + project: description: - - The ID of the project a floating IP is associated with. + - The name or ID of the project a floating IP is associated with. type: str + aliases: ['project_id'] router: description: - The name or id of an associated router. type: str status: description: - - The status of a floating IP, which can be ``ACTIVE``or ``DOWN``. + - The status of a floating IP. choices: ['active', 'down'] type: str requirements: @@ -56,8 +57,9 @@ extends_documentation_fragment: RETURN = ''' floating_ips: description: The floating ip objects list. - type: complex - returned: On Success. + type: list + elements: dict + returned: success contains: created_at: description: Timestamp at which the floating IP was assigned. @@ -87,9 +89,10 @@ floating_ips: description: Name of the floating ip. type: str port_details: - description: The details of the port that this floating IP associates \ - with. Present if ``fip-port-details`` extension is loaded. - type: str + description: | + The details of the port that this floating IP associates + with. Present if C(fip-port-details) extension is loaded. + type: dict port_id: description: The port ID floating ip associated with. type: str @@ -106,15 +109,16 @@ floating_ips: description: The id of the router floating ip associated with. type: str status: - description: The status of a floating IP, which can be ``ACTIVE``or ``DOWN``.\ - Can be 'ACTIVE' and 'DOWN'. + description: | + The status of a floating IP, which can be 'ACTIVE' or 'DOWN'. type: str subnet_id: description: The id of the subnet the floating ip associated with. type: str tags: description: List of tags. - type: str + type: list + elements: str updated_at: description: Timestamp at which the floating IP was last updated. type: str @@ -146,7 +150,7 @@ class FloatingIPInfoModule(OpenStackModule): floating_ip_address=dict(), floating_network=dict(), port=dict(), - project_id=dict(), + project=dict(aliases=['project_id']), router=dict(), status=dict(choices=['active', 'down']), ) @@ -155,46 +159,42 @@ class FloatingIPInfoModule(OpenStackModule): ) def run(self): + query = dict((k, self.params[k]) + for k in ['description', 'fixed_ip_address', + 'floating_ip_address'] + if self.params[k] is not None) + + for k in ['port', 'router']: + if self.params[k]: + k_id = '{0}_id'.format(k) + find_name = 'find_{0}'.format(k) + query[k_id] = getattr(self.conn.network, find_name)( + name_or_id=self.params[k], ignore_missing=False)['id'] + + floating_network_name_or_id = self.params['floating_network'] + if floating_network_name_or_id: + query['floating_network_id'] = self.conn.network.find_network( + name_or_id=floating_network_name_or_id, + ignore_missing=False)['id'] + + project_name_or_id = self.params['project'] + if project_name_or_id: + project = self.conn.identity.find_project(project_name_or_id) + if project: + query['project_id'] = project['id'] + else: + # caller might not have permission to query projects + # so assume she gave a project id + query['project_id'] = project_name_or_id - description = self.params['description'] - fixed_ip_address = self.params['fixed_ip_address'] - floating_ip_address = self.params['floating_ip_address'] - floating_network = self.params['floating_network'] - port = self.params['port'] - project_id = self.params['project_id'] - router = self.params['router'] status = self.params['status'] - - query = {} - if description: - query['description'] = description - if fixed_ip_address: - query['fixed_ip_address'] = fixed_ip_address - if floating_ip_address: - query['floating_ip_address'] = floating_ip_address - if floating_network: - try: - query['floating_network_id'] = self.conn.network.find_network(name_or_id=floating_network, - ignore_missing=False).id - except self.sdk.exceptions.ResourceNotFound: - self.fail_json(msg="floating_network not found") - if port: - try: - query['port_id'] = self.conn.network.find_port(name_or_id=port, ignore_missing=False).id - except self.sdk.exceptions.ResourceNotFound: - self.fail_json(msg="port not found") - if project_id: - query['project_id'] = project_id - if router: - try: - query['router_id'] = self.conn.network.find_router(name_or_id=router, ignore_missing=False).id - except self.sdk.exceptions.ResourceNotFound: - self.fail_json(msg="router not found") if status: query['status'] = status.upper() - ips = [ip.to_dict(computed=False) for ip in self.conn.network.ips(**query)] - self.exit_json(changed=False, floating_ips=ips) + self.exit_json( + changed=False, + floating_ips=[ip.to_dict(computed=False) + for ip in self.conn.network.ips(**query)]) def main():