
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
503 lines
18 KiB
Python
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()
|