Refactored floating_ip and floating_ip_info modules

Use service proxies from openstacksdk wherever reasonable in order to
reduce calls to OpenStack API.

Renamed floating_ip_info's attribute 'project_id' to 'project' to be
consistent with other attributes and added the former as an alias to
keep backward compatibility. The latter can now also be used to search
for floating ips by project names, not only project ids.

Sorted argument specs and documentation of both modules.

Reworked integration tests, e.g. replaced references to server's
'addresses' attribute with calls to our port_info and floating_ip_info
modules. Also reformatted tests and added assertion on return values.

Merged integration tests of floating_ip_info module into floating_ip
module, because the former does not create any floating ips and
assumes that they have been created earlier.

For Zuul CI job ansible-collections-openstack-functional-devstack-\
releases to pass, the minimum required openstacksdk release must be
0.102.0 because [1],[2],[3],[4] are available since that release
only.

[1] https://review.opendev.org/c/openstack/openstacksdk/+/851976
[2] 0ded7ac398
[3] https://review.opendev.org/c/openstack/openstacksdk/+/859672
[4] 2535ba7a28

Change-Id: I129f866e7ed8d5c0499c93e78ebbe2c424e09423
This commit is contained in:
Jakob Meng 2022-09-28 15:49:04 +02:00 committed by Jakob Meng
parent 0ade33eb6f
commit 34b0abb4ca
7 changed files with 774 additions and 525 deletions

View File

@ -71,7 +71,7 @@
dns dns
dns_zone_info dns_zone_info
endpoint endpoint
floating_ip_info floating_ip
host_aggregate host_aggregate
identity_domain_info identity_domain_info
identity_group identity_group
@ -112,7 +112,6 @@
user_role user_role
volume volume
# failing tags # failing tags
# floating_ip
# neutron_rbac # neutron_rbac
- job: - job:

View File

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

View File

@ -1,5 +1,4 @@
--- ---
# Prepare environment
- name: Gather information about public network - name: Gather information about public network
openstack.cloud.networks_info: openstack.cloud.networks_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
@ -66,6 +65,7 @@
network: ansible_internal network: ansible_internal
fixed_ips: fixed_ips:
- ip_address: 10.7.7.100 - ip_address: 10.7.7.100
register: port1
- name: Create internal port 2 - name: Create internal port 2
openstack.cloud.port: openstack.cloud.port:
@ -75,6 +75,7 @@
network: ansible_internal network: ansible_internal
fixed_ips: fixed_ips:
- ip_address: 10.7.7.101 - ip_address: 10.7.7.101
register: port2
- name: Create internal port 3 - name: Create internal port 3
openstack.cloud.port: openstack.cloud.port:
@ -84,6 +85,7 @@
network: ansible_internal network: ansible_internal
fixed_ips: fixed_ips:
- ip_address: 10.7.7.102 - ip_address: 10.7.7.102
register: port3
- name: Create router 1 - name: Create router 1
openstack.cloud.router: openstack.cloud.router:
@ -138,7 +140,7 @@
when: fips.floating_ips|length == 0 or when: fips.floating_ips|length == 0 or
"10.6.6.150" not in fips.floating_ips|map(attribute="floating_ip_address")|list "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: openstack.cloud.server:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
@ -150,25 +152,25 @@
- port-name: ansible_internal_port1 - port-name: ansible_internal_port1
auto_ip: false auto_ip: false
wait: true wait: true
register: server1
- name: Get info about server - name: Get server 1 ports
openstack.cloud.server_info: openstack.cloud.port_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server1 filters:
register: info device_id: "{{ server1.server.id }}"
register: server1_ports
- name: Assert one internal port and no floating ips on server 1 - name: Assert one fixed ip on server 1
# If this assertion fails because server has an public ipv4 address (public_v4) then make sure # 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 # that no floating ip on public network is associated with "10.7.7.100" before running this role
assert: assert:
that: that:
- info.servers|length == 1 - server1_ports.ports|length == 1
- info.servers.0.public_v4|length == 0 - server1_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list ==
- info.servers.0.public_v6|length == 0 ["10.7.7.100"]
- info.servers.0.addresses.ansible_internal|length == 1
- info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list == ["10.7.7.100"]
- name: Create server with two nics - name: Create server 2 with two nics
openstack.cloud.server: openstack.cloud.server:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
@ -180,123 +182,186 @@
- port-name: ansible_internal_port3 - port-name: ansible_internal_port3
auto_ip: false auto_ip: false
wait: true wait: true
register: server2
- name: Get info about server - name: Get server 2 ports
openstack.cloud.server_info: openstack.cloud.port_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server2 filters:
register: info device_id: "{{ server2.server.id }}"
register: server2_ports
- name: Assert two internal ports and no floating ips on server 2 - name: Assert two fixed ips on server 2
assert: assert:
that: that:
- info.servers|length == 1 - server2_ports.ports|length == 2
- info.servers.0.public_v4|length == 0 - server2_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list ==
- 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 ==
["10.7.7.101", "10.7.7.102"] ["10.7.7.101", "10.7.7.102"]
# Tests
- name: Assign new floating IP to server from first available external network or nova pool - name: Assign new floating IP to server from first available external network or nova pool
openstack.cloud.floating_ip: openstack.cloud.floating_ip:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
server: ansible_server1 server: ansible_server1
wait: true wait: yes
- name: Get info about server - name: Get floating ip attached to server 1
openstack.cloud.server_info: openstack.cloud.floating_ip_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server1 port: "{{ port1.port.id }}"
register: info 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 one internal port and one floating ip on server 1 - name: Assert fixed ip and floating ip attached to server 1
assert: assert:
that: that:
- info.servers.0.addresses.ansible_internal|length == 2 - server1_ports.ports|length == 1
- info.servers.0.addresses.ansible_internal|map(attribute="OS-EXT-IPS:type")|sort|list == - server1_ports.ports|sum(attribute='fixed_ips', start=[])|map(attribute='ip_address')|sort|list ==
["fixed", "floating"] ["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: present
server: ansible_server1
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: openstack.cloud.floating_ip:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: absent state: absent
server: ansible_server1 server: ansible_server1
network: public network: public
floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| floating_ip_address: "{{ server1_fips.floating_ips.0.floating_ip_address }}"
selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}"
- name: Get info about server - name: Wait until floating ip is detached from server 1
openstack.cloud.server_info: openstack.cloud.floating_ip_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server1 port: "{{ port1.port.id }}"
register: info register: server1_fips
# When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info # When detaching a floating ip from an instance there might be a delay until it is not listed anymore
# does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary.
retries: 10 retries: 10
delay: 3 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 - name: Find all floating ips for debugging
assert: openstack.cloud.floating_ip_info:
that:
- info.servers.0.addresses.ansible_internal|length == 1
- info.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 }}" cloud: "{{ cloud }}"
state: present register: fips
reuse: yes
server: ansible_server2
network: public
fixed_address: 10.7.7.101
wait: true
- name: Get info about server - name: Print all floating ips for debugging
debug: var=fips
- name: Find all servers for debugging
openstack.cloud.server_info: openstack.cloud.server_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server2 register: servers
register: info
- name: Assert two internal ports and one floating ip on server 2 - name: Print all servers for debugging
assert: debug: var=servers
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"]
- name: Assign a second, specific floating IP to server - name: Assign floating ip to server 2
openstack.cloud.floating_ip: openstack.cloud.floating_ip:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: present state: present
reuse: yes 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:
- server2_fip.floating_ip
- name: Find all floating ips for debugging
openstack.cloud.floating_ip_info:
cloud: "{{ cloud }}"
register: fips
- name: Print all floating ips for debugging
debug: var=fips
- 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 server: ansible_server2
network: ansible_external network: ansible_external
fixed_address: 10.7.7.102 fixed_address: "{{ port3.port.fixed_ips[0].ip_address }}"
floating_ip_address: "10.6.6.150" 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 # 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 # 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 retries: 10
delay: 3 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: assert:
that: that:
- info.servers.0.addresses.ansible_internal|length == 4 - server2_fips.floating_ips|length == 1
- ("10.6.6.150" in info.servers.0.addresses.ansible_internal|map(attribute="addr")|sort|list) - 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: openstack.cloud.floating_ip:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: absent state: absent
@ -304,49 +369,40 @@
network: ansible_external network: ansible_external
floating_ip_address: "10.6.6.150" floating_ip_address: "10.6.6.150"
- name: Get info about server - name: Wait until second floating ip is detached from server 2
openstack.cloud.server_info: openstack.cloud.floating_ip_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server2 port: "{{ port3.port.id }}"
register: info register: server2_fips
# When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info # When detaching a floating ip from an instance there might be a delay until it is not listed anymore
# does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary.
retries: 10 retries: 10
delay: 3 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 - name: Get first floating ip attached to server 2
assert: openstack.cloud.floating_ip_info:
that: cloud: "{{ cloud }}"
- info.servers.0.addresses.ansible_internal|length == 3 port: "{{ port2.port.id }}"
register: server2_fips
- name: Detach remaining floating IP from server - name: Detach remaining floating ip from server 2
openstack.cloud.floating_ip: openstack.cloud.floating_ip:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
state: absent state: absent
server: ansible_server2 server: ansible_server2
network: public network: public
floating_ip_address: "{{ (info.servers.0.addresses.ansible_internal| floating_ip_address: "{{ server2_fips.floating_ips.0.floating_ip_address }}"
selectattr('OS-EXT-IPS:type', '==', 'floating')|map(attribute='addr')|list)[0] }}"
- name: Get info about server - name: Wait until first floating ip is detached from server 2
openstack.cloud.server_info: openstack.cloud.floating_ip_info:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"
server: ansible_server2 port: "{{ port2.port.id }}"
register: info register: server2_fips
# When detaching a floating ip from an instance there might be a delay until openstack.cloud.server_info # When detaching a floating ip from an instance there might be a delay until it is not listed anymore
# does not list it any more in info.servers.0.addresses.ansible_internal, so retry if necessary.
retries: 10 retries: 10
delay: 3 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 - name: Delete server with two nics
openstack.cloud.server: openstack.cloud.server:
cloud: "{{ cloud }}" cloud: "{{ cloud }}"

View File

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

View File

@ -17,7 +17,7 @@
tags: dns tags: dns
when: sdk_version is version(0.28, '>=') when: sdk_version is version(0.28, '>=')
- { role: endpoint, tags: endpoint } - { 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: host_aggregate, tags: host_aggregate }
- { role: identity_domain_info, tags: identity_domain_info } - { role: identity_domain_info, tags: identity_domain_info }
- { role: identity_group, tags: identity_group } - { role: identity_group, tags: identity_group }
@ -67,5 +67,4 @@
- { role: volume, tags: volume } - { role: volume, tags: volume }
- role: loadbalancer - role: loadbalancer
tags: loadbalancer tags: loadbalancer
- { role: floating_ip, tags: floating_ip }
- { role: quota, tags: quota } - { role: quota, tags: quota }

View File

@ -12,66 +12,59 @@ short_description: Add/Remove floating IP from an instance
description: description:
- Add or Remove a floating IP to an instance. - Add or Remove a floating IP to an instance.
- Returns the floating IP when attaching only if I(wait=true). - 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: 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: fixed_address:
description: description:
- To which fixed IP of server the floating IP address should be - To which fixed IP of server the floating IP address should be
attached to. attached to.
type: str 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: nat_destination:
description: description:
- The name or id of a neutron private network that the fixed IP to - The name or id of a neutron private network that the fixed IP to
attach floating IP is on attach floating IP is on
aliases: ["fixed_network", "internal_network"] aliases: ["fixed_network", "internal_network"]
type: str type: str
wait: network:
description: description:
- When attaching a floating IP address, specify whether to wait for it to appear as attached. - The name or ID of a neutron external network or a nova pool name.
- Must be set to C(yes) for the module to return the value of the floating IP when attaching. 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 type: bool
default: 'no' default: 'no'
timeout: reuse:
description: description:
- Time to wait for an IP address to appear as attached. See wait. - When I(state) is present, and I(floating_ip_address) is not present,
required: false this parameter can be used to specify whether we should try to reuse
default: 60 a floating IP address already allocated to the project.
type: int - 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: state:
description: description:
- Should the resource be present or absent. - Should the resource be present or absent.
choices: [present, absent] choices: [present, absent]
default: present default: present
type: str 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: requirements:
- "python >= 3.6" - "python >= 3.6"
- "openstacksdk" - "openstacksdk"
@ -120,182 +113,384 @@ EXAMPLES = '''
server: cattle001 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 from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
import itertools
class NetworkingFloatingIPModule(OpenStackModule): class NetworkingFloatingIPModule(OpenStackModule):
argument_spec = dict( 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), server=dict(required=True),
state=dict(default='present', choices=['absent', 'present']), 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( module_kwargs = dict(
required_if=[ required_if=[
['state', 'absent', ['floating_ip_address']] ['state', 'absent', ['floating_ip_address']]
], ],
required_by=dict( required_by={
floating_ip_address=('network',) 'floating_ip_address': ('network'),
) }
) )
def _get_floating_ip(self, floating_ip_address): def run(self):
f_ips = self.conn.search_floating_ips( self._init()
filters={'floating_ip_address': floating_ip_address}) if self.params['state'] == 'present':
self._create_and_attach()
if not f_ips: else: # self.params['state'] == 'absent'
return None 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): ips = self._find_ips(
return itertools.chain.from_iterable([ server=self.server,
(addr['addr'] for addr in server.addresses[net] if addr['OS-EXT-IPS:type'] == 'floating') floating_ip_address=floating_ip_address,
for net in server.addresses network_id=network_id,
]) fixed_address=fixed_address,
nat_destination_name_or_id=nat_destination_name_or_id)
def _match_floating_ip(self, server, # First floating ip satisfies our requirements
ip = ips[0] if ips else None
if floating_ip_address:
# A specific floating ip address has been requested
if not ip:
# If a specific floating ip address has been requested
# and it does not exist yet then create it
# 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
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, floating_ip_address,
network_id, network_id,
fixed_address, fixed_address,
nat_destination): 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: if floating_ip_address:
return self._get_floating_ip(floating_ip_address) # A specific floating ip address has been requested
elif not fixed_address and nat_destination: ip = self.conn.network.find_ip(floating_ip_address)
nat_destination_name = self.conn.get_network(nat_destination)['name'] return [ip] if ip else []
return next( elif (not fixed_address and nat_destination_name_or_id):
(self._get_floating_ip(addr['addr']) # No specific floating ip and no specific fixed ip have been
for addr in server.addresses.get(nat_destination_name, []) # requested but a private network (nat_destination) has been
if addr['OS-EXT-IPS:type'] == 'floating'), # given where the floating ip should be attached to.
None) return self._find_ips_by_nat_destination(
server, nat_destination_name_or_id)
else: else:
# not floating_ip_address and (fixed_address or not nat_destination) # not floating_ip_address
# and (fixed_address or not nat_destination_name_or_id)
# get any of the floating ips that matches fixed_address and/or network # An analysis of all floating ips of server is required
f_ip_addrs = self._list_floating_ips(server) return self._find_ips_by_network_id_and_fixed_address(
f_ips = [f_ip for f_ip in self.conn.list_floating_ips() if f_ip['floating_ip_address'] in f_ip_addrs] server, fixed_address, network_id)
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): def _find_ips_by_nat_destination(self,
server_name_or_id = self.params['server'] server,
state = self.params['state'] nat_destination_name_or_id):
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['addresses']:
if not server: return None
self.fail_json(
msg="server {0} not found".format(server_name_or_id))
# Extract floating ips from server # Check if we have any floating ip on
f_ip_addrs = self._list_floating_ips(server) # the given nat_destination network
nat_destination = self.conn.network.find_network(
nat_destination_name_or_id, ignore_missing=False)
# Get details about requested floating ip fips_with_nat_destination = [
f_ip = self._get_floating_ip(floating_ip_address) if floating_ip_address else None addr for addr
in server['addresses'].get(nat_destination['name'], [])
if addr['OS-EXT-IPS:type'] == 'floating']
if network: if not fips_with_nat_destination:
network_id = self.conn.get_network(name_or_id=network)["id"] return None
else:
network_id = None
if state == 'present': # One or more floating ip addresses have been assigned
if floating_ip_address and f_ip and floating_ip_address in f_ip_addrs: # to the requested nat_destination; return the first.
# Floating ip address has been assigned to server return [self.conn.network.find_ip(fip['addr'], ignore_missing=False)
self.exit_json(changed=False, floating_ip=f_ip) for fip in fips_with_nat_destination]
if f_ip and f_ip['attached'] and floating_ip_address not in f_ip_addrs: def _find_ips_by_network_id_and_fixed_address(self,
# Requested floating ip has been attached to different server server,
self.fail_json(msg="floating-ip {floating_ip_address} already has been attached to different server" fixed_address=None,
.format(floating_ip_address=floating_ip_address)) 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)]
if not floating_ip_address: matching_ips = []
# No specific floating ip requested, i.e. if any floating ip is already assigned to server, for ip in ips:
# check that it matches requirements. if network_id and ip['floating_network_id'] != network_id:
# Requested network does not
if not fixed_address and nat_destination: # match network of floating ip
# 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 continue
if not fixed_address and not nat_destination: if not fixed_address: # and not nat_destination_name_or_id
# any floating ip will fullfil these requirements # Any floating ip will fullfil these requirements
self.exit_json(changed=False, floating_ip=f_ip) matching_ips.append(ip)
if fixed_address and f_ip.fixed_ip_address == fixed_address: if (fixed_address and ip['fixed_ip_address'] == fixed_address):
# a floating ip address has been assigned that points to the requested fixed_address # A floating ip address has been assigned that
self.exit_json(changed=False, floating_ip=f_ip) # points to the requested fixed_address
matching_ips.append(ip)
if floating_ip_address and not f_ip: return matching_ips
# 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. def _init(self):
# add_ips_to_server will ignore floating_ip_address if network is set server_name_or_id = self.params['server']
# Ref.: https://github.com/openstack/openstacksdk/blob/a6b0ece2821ea79330c4067100295f6bdcbe456e/openstack/cloud/_floating_ip.py#L987 server = self.conn.compute.find_server(server_name_or_id,
server = self.conn.add_ips_to_server( ignore_missing=False)
server=server, # fetch server details such as addresses
ips=floating_ip_address, self.server = self.conn.compute.get_server(server)
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 network_name_or_id = self.params['network']
f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) if network_name_or_id:
self.exit_json(changed=True, floating_ip=f_ip) self.network = self.conn.network.find_network(
name_or_id=network_name_or_id, ignore_missing=False)
elif state == 'absent': else:
f_ip = self._match_floating_ip(server, floating_ip_address, network_id, fixed_address, nat_destination) self.network = None
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'])
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)
def main(): def main():

View File

@ -32,17 +32,18 @@ options:
description: description:
- The name or id of the port to which a floating IP is associated. - The name or id of the port to which a floating IP is associated.
type: str type: str
project_id: project:
description: 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 type: str
aliases: ['project_id']
router: router:
description: description:
- The name or id of an associated router. - The name or id of an associated router.
type: str type: str
status: status:
description: description:
- The status of a floating IP, which can be ``ACTIVE``or ``DOWN``. - The status of a floating IP.
choices: ['active', 'down'] choices: ['active', 'down']
type: str type: str
requirements: requirements:
@ -56,8 +57,9 @@ extends_documentation_fragment:
RETURN = ''' RETURN = '''
floating_ips: floating_ips:
description: The floating ip objects list. description: The floating ip objects list.
type: complex type: list
returned: On Success. elements: dict
returned: success
contains: contains:
created_at: created_at:
description: Timestamp at which the floating IP was assigned. description: Timestamp at which the floating IP was assigned.
@ -87,9 +89,10 @@ floating_ips:
description: Name of the floating ip. description: Name of the floating ip.
type: str type: str
port_details: port_details:
description: The details of the port that this floating IP associates \ description: |
with. Present if ``fip-port-details`` extension is loaded. The details of the port that this floating IP associates
type: str with. Present if C(fip-port-details) extension is loaded.
type: dict
port_id: port_id:
description: The port ID floating ip associated with. description: The port ID floating ip associated with.
type: str type: str
@ -106,15 +109,16 @@ floating_ips:
description: The id of the router floating ip associated with. description: The id of the router floating ip associated with.
type: str type: str
status: status:
description: The status of a floating IP, which can be ``ACTIVE``or ``DOWN``.\ description: |
Can be 'ACTIVE' and 'DOWN'. The status of a floating IP, which can be 'ACTIVE' or 'DOWN'.
type: str type: str
subnet_id: subnet_id:
description: The id of the subnet the floating ip associated with. description: The id of the subnet the floating ip associated with.
type: str type: str
tags: tags:
description: List of tags. description: List of tags.
type: str type: list
elements: str
updated_at: updated_at:
description: Timestamp at which the floating IP was last updated. description: Timestamp at which the floating IP was last updated.
type: str type: str
@ -146,7 +150,7 @@ class FloatingIPInfoModule(OpenStackModule):
floating_ip_address=dict(), floating_ip_address=dict(),
floating_network=dict(), floating_network=dict(),
port=dict(), port=dict(),
project_id=dict(), project=dict(aliases=['project_id']),
router=dict(), router=dict(),
status=dict(choices=['active', 'down']), status=dict(choices=['active', 'down']),
) )
@ -155,46 +159,42 @@ class FloatingIPInfoModule(OpenStackModule):
) )
def run(self): 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'] 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: if status:
query['status'] = status.upper() query['status'] = status.upper()
ips = [ip.to_dict(computed=False) for ip in self.conn.network.ips(**query)] self.exit_json(
self.exit_json(changed=False, floating_ips=ips) changed=False,
floating_ips=[ip.to_dict(computed=False)
for ip in self.conn.network.ips(**query)])
def main(): def main():