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] a6b0ece282/openstack/cloud/_floating_ip.py (L987)
[2] a6b0ece282/openstack/cloud/_floating_ip.py (L907)

Task: 40939
Story: 2008181
Change-Id: I1ada1be0994f526f72f81f7458782afbcca3c92c
This commit is contained in:
Jakob Meng 2020-09-22 15:22:25 +02:00
parent 0e370b2c51
commit 3a08a9c07c
3 changed files with 584 additions and 62 deletions

View File

@ -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

View File

@ -50,3 +50,4 @@
when: sdk_version is version("0.53.0", '>=')
- role: loadbalancer
tags: loadbalancer
- { role: floating_ip, tags: floating_ip }

View File

@ -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)