From 3a08a9c07c4abcb1c772b97ae047bec6f18cff47 Mon Sep 17 00:00:00 2001 From: Jakob Meng Date: Tue, 22 Sep 2020 15:22:25 +0200 Subject: [PATCH] Allow to attach multiple floating ips to a server OpenStack allows to attach multiple floating ips to a single server. Previously, only one floating ip was supported by this module. It would call openstacksdk's get_server_public_ip(), which in turn would return just one of the attached floating ips. If this floating ip would not point to the right nat_destination or fixed_address, then the module would fail. If no floating ip had been attached to a server, then this module would call openstacksdk's add_ips_to_server() with both parameters "floating_ip_address" and "network" to attach a floating ip to the server. But both parameters are mutually exclusive [1], i.e. add_ips_to_server() will ignore "floating_ip_address" if "network" is set and then choose any non-attached floating ip from "network". If "floating_ip_address" has not been created in OpenStack and "network" is not given, then this module would not create this floating ip [2]. The new module code allows to create and add more than one floating ip to a server. It priorizes more specific parameters over generic ones, i.e. if both "floating_ip_address" and "network" are given, then "floating_ip_address" precedes "network". Parameter "network" is now required if "floating_ip_address" is specified, because both are necessary when creating floating ips. Module documentation and args have been updated accordingly. Ref.: [1] https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L987 [2] https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L907 Task: 40939 Story: 2008181 Change-Id: I1ada1be0994f526f72f81f7458782afbcca3c92c --- ci/roles/floating_ip/tasks/main.yml | 466 ++++++++++++++++++++++++++++ ci/run-collection.yml | 1 + plugins/modules/floating_ip.py | 179 +++++++---- 3 files changed, 584 insertions(+), 62 deletions(-) create mode 100644 ci/roles/floating_ip/tasks/main.yml diff --git a/ci/roles/floating_ip/tasks/main.yml b/ci/roles/floating_ip/tasks/main.yml new file mode 100644 index 00000000..e724a8cb --- /dev/null +++ b/ci/roles/floating_ip/tasks/main.yml @@ -0,0 +1,466 @@ +--- +# Prepare environment +- name: Gather information about public network + openstack.cloud.networks_info: + cloud: "{{ cloud }}" + name: public + register: public_network + +- name: Assert that public network exists + assert: + that: public_network.openstack_networks|length == 1 + +- name: Create external network + openstack.cloud.network: + 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 + +- 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 + +- 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 + +- name: Create internal network + openstack.cloud.network: + 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 + +- 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 + +- 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 + +- 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 + +- 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 + +# 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 + +- name: Get all floating ips + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + register: fips + +- name: Check if public network has any floating ips + set_fact: + public_network_had_fips: "{{ fips.floating_ips| + selectattr('floating_network_id', '==', public_network.openstack_networks.0.id)| + list|length > 0 }}" + +# TODO: Replace with appropriate Ansible module once available +- name: Create a floating ip on public network (required for simplest, first floating ip test) + command: openstack --os-cloud={{ cloud }} floating ip create public + when: not public_network_had_fips + +# TODO: Replace with appropriate Ansible module once available +- name: Create floating ip 1 on external network + command: > + openstack --os-cloud={{ cloud }} floating ip create + --subnet ansible_external_subnet + --floating-ip-address 10.6.6.150 + ansible_external + 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 + 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 + +- name: Assert one internal port and no floating ips 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.openstack_servers|length == 1 + - info.openstack_servers.0.public_v4|length == 0 + - info.openstack_servers.0.public_v6|length == 0 + - info.openstack_servers.0.addresses.ansible_internal|length == 1 + - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.100"] + +- name: Create server 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 + +- name: Assert two internal ports and no floating ips on server 2 + assert: + that: + - info.openstack_servers|length == 1 + - info.openstack_servers.0.public_v4|length == 0 + - info.openstack_servers.0.public_v6|length == 0 + - info.openstack_servers.0.addresses.ansible_internal|length == 2 + - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|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 }}" + server: ansible_server1 + register: info + +- name: Assert one internal port and one floating ip on server 1 + assert: + that: + - info.openstack_servers.0.addresses.ansible_internal|length == 2 + - info.openstack_servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == + ["fixed", "floating"] + +- name: Detach floating IP from server + openstack.cloud.floating_ip: + cloud: "{{ cloud }}" + state: absent + server: ansible_server1 + network: public + floating_ip_address: "{{ (info.openstack_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 }}" + 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.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + retries: 10 + delay: 3 + until: info.openstack_servers.0.addresses.ansible_internal|length == 1 + +- name: Assert one internal port on server 1 + assert: + that: + - info.openstack_servers.0.addresses.ansible_internal|length == 1 + - info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|list == ["10.7.7.100"] + +- 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: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: ansible_server2 + register: info + +- name: Assert two internal ports and one floating ip on server 2 + assert: + that: + - info.openstack_servers.0.addresses.ansible_internal|length == 3 + - info.openstack_servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == + ["fixed", "fixed", "floating"] + +- 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" + +# 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: Get info about server + openstack.cloud.server_info: + cloud: "{{ cloud }}" + server: ansible_server2 + register: info + # retry because we cannot wait for second floating ip + retries: 10 + delay: 3 + until: info.openstack_servers.0.addresses.ansible_internal|length == 4 + +- name: Assert two internal ports and two floating ips on server 2 + assert: + that: + - info.openstack_servers.0.addresses.ansible_internal|length == 4 + - ("10.6.6.150" in info.openstack_servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list) + +- name: Detach second floating IP from server + 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 }}" + 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.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + retries: 10 + delay: 3 + until: info.openstack_servers.0.addresses.ansible_internal|length == 3 + +- name: Assert two internal ports and one floating ip on server 2 + assert: + that: + - info.openstack_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.openstack_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 }}" + 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.openstack_servers.0.addresses.ansible_internal, so retry if necessary. + retries: 10 + delay: 3 + until: info.openstack_servers.0.addresses.ansible_internal|length == 2 + +- name: Assert two internal ports on server 2 + assert: + that: + - info.openstack_servers.0.addresses.ansible_internal|length == 2 + - info.openstack_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 + +- name: Delete server with one nic + openstack.cloud.server: + cloud: "{{ cloud }}" + state: absent + name: ansible_server1 + wait: true + +- name: Get all floating ips + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + register: fips + +# TODO: Replace with appropriate Ansible module once available +- name: Delete floating ip on public network if we created it + when: not public_network_had_fips + command: > + openstack --os-cloud={{ cloud }} floating ip delete + {{ fips.floating_ips|selectattr('floating_network_id', '==', public_network.openstack_networks.0.id)| + map(attribute="floating_ip_address")|list|join(' ') }} + +# TODO: Replace with appropriate Ansible module once available +- name: Delete floating ip 1 + command: openstack --os-cloud={{ cloud }} floating ip delete 10.6.6.150 + when: fips.floating_ips|length > 0 and "10.6.6.150" in fips.floating_ips|map(attribute="floating_ip_address")|list + +- name: Get remaining floating ips on external network + openstack.cloud.floating_ip_info: + cloud: "{{ cloud }}" + floating_network: ansible_external + register: fips + +# TODO: Replace with appropriate Ansible module once available +# The first, simple floating ip test might have allocated a floating ip on the external network. +# This floating ip must be removed before external network can be deleted. +- name: Delete remaining floating ips on external network + when: fips.floating_ips|length > 0 + command: > + openstack --os-cloud={{ cloud }} floating ip delete + {{ fips.floating_ips|map(attribute="floating_ip_address")|list|join(' ') }} + +# 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, +# as it is required by one or more floating IPs. + +- name: Delete router 2 + openstack.cloud.router: + cloud: "{{ cloud }}" + state: absent + name: ansible_router2 + +- name: Delete router 1 + openstack.cloud.router: + cloud: "{{ cloud }}" + state: absent + name: ansible_router1 + +- name: Delete internal port 3 + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port3 + +- name: Delete internal port 2 + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port2 + +- name: Delete internal port 1 + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_port1 + +- name: Delete internal subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal_subnet + +- name: Delete internal network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: ansible_internal + +- name: Delete external port 2 + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: ansible_external_port2 + +- name: Delete external port 1 + openstack.cloud.port: + cloud: "{{ cloud }}" + state: absent + name: ansible_external_port1 + +- name: Delete external subnet + openstack.cloud.subnet: + cloud: "{{ cloud }}" + state: absent + name: ansible_external_subnet + +- name: Delete external network + openstack.cloud.network: + cloud: "{{ cloud }}" + state: absent + name: ansible_external diff --git a/ci/run-collection.yml b/ci/run-collection.yml index a217ff16..064b24b0 100644 --- a/ci/run-collection.yml +++ b/ci/run-collection.yml @@ -50,3 +50,4 @@ when: sdk_version is version("0.53.0", '>=') - role: loadbalancer tags: loadbalancer + - { role: floating_ip, tags: floating_ip } diff --git a/plugins/modules/floating_ip.py b/plugins/modules/floating_ip.py index 16222445..6b5fb0d6 100644 --- a/plugins/modules/floating_ip.py +++ b/plugins/modules/floating_ip.py @@ -11,6 +11,7 @@ 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. options: server: description: @@ -24,9 +25,9 @@ options: type: str floating_ip_address: description: - - A floating IP address to attach or to detach. Required only if I(state) - is absent. When I(state) is present can be used to specify a IP address - to attach. + - 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: @@ -49,7 +50,7 @@ options: wait: 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. + - Must be set to C(yes) for the module to return the value of the floating IP when attaching. type: bool default: 'no' timeout: @@ -118,8 +119,8 @@ EXAMPLES = ''' server: cattle001 ''' -from ansible.module_utils.basic import remove_values from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule +import itertools class NetworkingFloatingIPModule(OpenStackModule): @@ -137,16 +138,57 @@ class NetworkingFloatingIPModule(OpenStackModule): purge=dict(required=False, type='bool', default=False), ) - module_kwargs = dict() + module_kwargs = dict( + required_if=[ + ['state', 'absent', ['floating_ip_address']] + ], + required_by=dict( + 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}) + if not f_ips: return None return f_ips[0] + 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 + ]) + + def _match_floating_ip(self, server, + floating_ip_address, + network_id, + fixed_address, + nat_destination): + + 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) + + # 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) + def run(self): server_name_or_id = self.params['server'] state = self.params['state'] @@ -160,83 +202,96 @@ class NetworkingFloatingIPModule(OpenStackModule): purge = self.params['purge'] server = self.conn.get_server(server_name_or_id) - if server is None: + 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 f_ip already assigned to server, check that it matches - # requirements. - public_ip = self.conn.get_server_public_ip(server) - f_ip = self._get_floating_ip(public_ip) if public_ip else public_ip - if f_ip: - if network: - network_id = self.conn.get_network(name_or_id=network)["id"] - else: - network_id = None - # check if we have floating ip on given nat_destination network - if nat_destination: - nat_floating_addrs = [ - addr for addr in server.addresses.get( - self.conn.get_network(nat_destination)['name'], []) - if addr['addr'] == public_ip - and addr['OS-EXT-IPS:type'] == 'floating' - ] + 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 len(nat_floating_addrs) == 0: - self.fail_json( - msg="server {server} already has a " - "floating-ip on a different " - "nat-destination than '{nat_destination}'" - .format(server=server_name_or_id, - nat_destination=nat_destination)) + 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 all([fixed_address, f_ip.fixed_ip_address == fixed_address, - network, f_ip.network != network_id]): - # Current state definitely conflicts with requirements - self.fail_json( - msg="server {server} already has a " - "floating-ip on requested " - "interface but it doesn't match " - "requested network {network}: {fip}" - .format(server=server_name_or_id, - network=network, - fip=remove_values(f_ip, self.no_log_values))) - if not network or f_ip.network == network_id: - # Requirements are met - self.exit_json(changed=False, floating_ip=f_ip) + 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. - # Requirements are vague enough to ignore existing f_ip and try - # to create a new f_ip to the server. + 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, - reuse=reuse, fixed_address=fixed_address, wait=wait, + 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) - fip_address = self.conn.get_server_public_ip(server) - # Update the floating IP status - f_ip = self._get_floating_ip(fip_address) + + # 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': - if floating_ip_address is None: - if not server_name_or_id: - self.fail_json(msg="either server or floating_ip_address are required") - server = self.conn.get_server(server_name_or_id) - floating_ip_address = self.conn.get_server_public_ip(server) - - f_ip = self._get_floating_ip(floating_ip_address) - + 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']) + 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']) changed = True + if purge: self.conn.delete_floating_ip(f_ip['id']) self.exit_json(changed=True)