Jakob Meng 34b0abb4ca 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
2022-10-14 12:09:28 +02:00

503 lines
18 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2015, Hewlett-Packard Development Company, L.P.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
---
module: floating_ip
author: OpenStack Ansible SIG
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:
fixed_address:
description:
- To which fixed IP of server the floating IP address should be
attached to.
type: str
floating_ip_address:
description:
- A floating IP address to attach or to detach. When I(state) is
present can be used to specify a IP address to attach.
I(floating_ip_address) requires I(network) to be set.
type: str
nat_destination:
description:
- The name or id of a neutron private network that the fixed IP to
attach floating IP is on
aliases: ["fixed_network", "internal_network"]
type: str
network:
description:
- The name or ID of a neutron external network or a nova pool name.
type: str
purge:
description:
- When I(state) is absent, indicates whether or not to delete the
floating IP completely, or only detach it from the server.
Default is to detach only.
type: bool
default: 'no'
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.
- When I(reuse) is C(true), I(network) is defined and
I(floating_ip_address) is undefined, then C(nat_destination) and
C(fixed_address) will be ignored.
type: bool
default: 'no'
server:
description:
- The name or ID of the instance to which the IP address
should be assigned.
required: true
type: str
state:
description:
- Should the resource be present or absent.
choices: [present, absent]
default: present
type: str
requirements:
- "python >= 3.6"
- "openstacksdk"
extends_documentation_fragment:
- openstack.cloud.openstack
'''
EXAMPLES = '''
# Assign a floating IP to the first interface of `cattle001` from an existing
# external network or nova pool. A new floating IP from the first available
# external network is allocated to the project.
- openstack.cloud.floating_ip:
cloud: dguerri
server: cattle001
# Assign a new floating IP to the instance fixed ip `192.0.2.3` of
# `cattle001`. If a free floating IP is already allocated to the project, it is
# reused; if not, a new one is created.
- openstack.cloud.floating_ip:
cloud: dguerri
state: present
reuse: yes
server: cattle001
network: ext_net
fixed_address: 192.0.2.3
wait: true
timeout: 180
# Assign a new floating IP from the network `ext_net` to the instance fixed
# ip in network `private_net` of `cattle001`.
- openstack.cloud.floating_ip:
cloud: dguerri
state: present
server: cattle001
network: ext_net
nat_destination: private_net
wait: true
timeout: 180
# Detach a floating IP address from a server
- openstack.cloud.floating_ip:
cloud: dguerri
state: absent
floating_ip_address: 203.0.113.2
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
class NetworkingFloatingIPModule(OpenStackModule):
argument_spec = dict(
fixed_address=dict(),
floating_ip_address=dict(),
nat_destination=dict(aliases=['fixed_network', 'internal_network']),
network=dict(),
purge=dict(type='bool', default=False),
reuse=dict(type='bool', default=False),
server=dict(required=True),
state=dict(default='present', choices=['absent', 'present']),
)
module_kwargs = dict(
required_if=[
['state', 'absent', ['floating_ip_address']]
],
required_by={
'floating_ip_address': ('network'),
}
)
def run(self):
self._init()
if self.params['state'] == 'present':
self._create_and_attach()
else: # self.params['state'] == 'absent'
self._detach_and_delete()
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
ips = self._find_ips(
server=self.server,
floating_ip_address=floating_ip_address,
network_id=network_id,
fixed_address=fixed_address,
nat_destination_name_or_id=nat_destination_name_or_id)
# 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,
network_id,
fixed_address,
nat_destination_name_or_id):
# Check which floating ips matches our requirements.
# They might or might not be attached to our server.
if floating_ip_address:
# A specific floating ip address has been requested
ip = self.conn.network.find_ip(floating_ip_address)
return [ip] if ip else []
elif (not fixed_address and nat_destination_name_or_id):
# No specific floating ip and no specific fixed ip have been
# requested but a private network (nat_destination) has been
# given where the floating ip should be attached to.
return self._find_ips_by_nat_destination(
server, nat_destination_name_or_id)
else:
# not floating_ip_address
# and (fixed_address or not nat_destination_name_or_id)
# An analysis of all floating ips of server is required
return self._find_ips_by_network_id_and_fixed_address(
server, fixed_address, network_id)
def _find_ips_by_nat_destination(self,
server,
nat_destination_name_or_id):
if not server['addresses']:
return None
# Check if we have any floating ip on
# the given nat_destination network
nat_destination = self.conn.network.find_network(
nat_destination_name_or_id, ignore_missing=False)
fips_with_nat_destination = [
addr for addr
in server['addresses'].get(nat_destination['name'], [])
if addr['OS-EXT-IPS:type'] == 'floating']
if not fips_with_nat_destination:
return None
# One or more floating ip addresses have been assigned
# to the requested nat_destination; return the first.
return [self.conn.network.find_ip(fip['addr'], ignore_missing=False)
for fip in fips_with_nat_destination]
def _find_ips_by_network_id_and_fixed_address(self,
server,
fixed_address=None,
network_id=None):
# Get any of the floating ips that matches fixed_address and/or network
ips = [ip for ip in self.conn.network.ips()
if ip['floating_ip_address'] in self._filter_ips(server)]
matching_ips = []
for ip in ips:
if network_id and ip['floating_network_id'] != network_id:
# Requested network does not
# match network of floating ip
continue
if not fixed_address: # and not nat_destination_name_or_id
# Any floating ip will fullfil these requirements
matching_ips.append(ip)
if (fixed_address and ip['fixed_ip_address'] == fixed_address):
# A floating ip address has been assigned that
# points to the requested fixed_address
matching_ips.append(ip)
return matching_ips
def _init(self):
server_name_or_id = self.params['server']
server = self.conn.compute.find_server(server_name_or_id,
ignore_missing=False)
# fetch server details such as addresses
self.server = self.conn.compute.get_server(server)
network_name_or_id = self.params['network']
if network_name_or_id:
self.network = self.conn.network.find_network(
name_or_id=network_name_or_id, ignore_missing=False)
else:
self.network = None
def main():
module = NetworkingFloatingIPModule()
module()
if __name__ == '__main__':
main()