ansible-collections-openstack/plugins/modules/loadbalancer.py
Jakob Meng f73a0e385e Use true and false instead of yes and no for boolean values
Story: 2010586
Task: 47380
Change-Id: I1b88aa925d823d74b2d012153dfe26d35c93dfd5
2023-02-21 13:07:29 +01:00

648 lines
23 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2018 Catalyst Cloud Ltd.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
DOCUMENTATION = '''
---
module: loadbalancer
short_description: Manage Octavia load-balancer in an OpenStack cloud
author: OpenStack Ansible SIG
description:
- Add, update or remove Octavia load-balancer from OpenStack cloud.
options:
assign_floating_ip:
description:
- Allocate floating ip address and associate with the VIP automatically.
- Deprecated, use M(openstack.cloud.floating_ip) instead.
type: bool
default: false
aliases: ['auto_public_ip']
delete_floating_ip:
description:
- When I(state) is C(present) and I(delete_floating_ip) is C(true), then
any floating ip address associated with the VIP will be deleted.
- When I(state) is C(absent) and I(delete_floating_ip) is C(true), then
any floating ip address associated with the VIP will be deleted along
with the load balancer.
- Deprecated, use M(openstack.cloud.floating_ip) instead.
type: bool
default: false
aliases: ['delete_public_ip']
description:
description:
- A human-readable description for the load-balancer.
type: str
flavor:
description:
- The flavor of the load balancer.
- This attribute cannot be updated.
type: str
floating_ip_address:
description:
- Floating ip address aka public ip address associated with the VIP.
- Deprecated, use M(openstack.cloud.floating_ip) instead.
type: str
aliases: ['public_ip_address']
floating_ip_network:
description:
- Name or ID of a Neutron external network where floating ip address will
be created on.
- Deprecated, use M(openstack.cloud.floating_ip) instead.
type: str
aliases: ['public_network']
name:
description:
- The name of the load balancer.
- This attribute cannot be updated.
required: true
type: str
state:
description:
- Should the resource be present or absent.
choices: [present, absent]
default: present
type: str
vip_address:
description:
- IP address of the load balancer virtual IP.
- This attribute cannot be updated.
type: str
vip_network:
description:
- The name or id of the network for the virtual IP of the load balancer.
- One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified
for creation.
- This attribute cannot be updated.
type: str
vip_port:
description:
- The name or id of the load balancer virtual IP port. One of
- One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified
for creation.
- This attribute cannot be updated.
type: str
vip_subnet:
description:
- The name or id of the subnet for the virtual IP of the load balancer.
- One of I(vip_network), I(vip_subnet), or I(vip_port) must be specified
for creation.
- This attribute cannot be updated.
type: str
extends_documentation_fragment:
- openstack.cloud.openstack
'''
RETURN = r'''
floating_ip:
description: Dictionary describing the floating ip address attached to the
load-balancer.
type: dict
returned: On success when I(state) is C(present) and I(assign_floating_ip) is
C(true).
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
load_balancer:
description: Dictionary describing the load-balancer.
returned: On success when I(state) is C(present).
type: dict
contains:
additional_vips:
description: Additional VIPs.
type: str
availability_zone:
description: Name of the target Octavia availability zone.
type: str
created_at:
description: Timestamp when the load balancer was created.
type: str
description:
description: The load balancer description.
type: str
flavor_id:
description: The load balancer flavor ID.
type: str
id:
description: Unique UUID.
type: str
is_admin_state_up:
description: The administrative state of the load balancer.
type: bool
listeners:
description: The associated listener IDs, if any.
type: list
name:
description: Name given to the load balancer.
type: str
operating_status:
description: The operating status of the load balancer.
type: str
pools:
description: The associated pool IDs, if any.
type: list
project_id:
description: The ID of the project this load balancer is associated with.
type: str
provider:
description: Provider name for the load balancer.
type: str
provisioning_status:
description: The provisioning status of the load balancer.
type: str
tags:
description: A list of associated tags.
type: str
updated_at:
description: Timestamp when the load balancer was last updated.
type: str
vip_address:
description: The load balancer virtual IP address.
type: str
vip_network_id:
description: Network ID the load balancer virtual IP port belongs in.
type: str
vip_port_id:
description: The load balancer virtual IP port ID.
type: str
vip_qos_policy_id:
description: VIP qos policy id.
type: str
vip_subnet_id:
description: Subnet ID the load balancer virtual IP port belongs in.
type: str
'''
EXAMPLES = r'''
- name: Create a load balancer
openstack.cloud.loadbalancer:
cloud: devstack
name: my_lb
state: present
vip_subnet: my_subnet
- name: Create another load balancer
openstack.cloud.loadbalancer:
cloud: devstack
name: my_lb
state: present
vip_address: 192.168.0.11
vip_network: my_network
- name: Delete a load balancer and all its related resources
openstack.cloud.loadbalancer:
cloud: devstack
name: my_lb
state: absent
- name: Delete a load balancer, its related resources and its floating ip
openstack.cloud.loadbalancer:
cloud: devstack
delete_floating_ip: true
name: my_lb
state: absent
'''
from ansible_collections.openstack.cloud.plugins.module_utils.openstack import OpenStackModule
class LoadBalancerModule(OpenStackModule):
argument_spec = dict(
assign_floating_ip=dict(default=False, type='bool',
aliases=['auto_public_ip']),
delete_floating_ip=dict(default=False, type='bool',
aliases=['delete_public_ip']),
description=dict(),
flavor=dict(),
floating_ip_address=dict(aliases=['public_ip_address']),
floating_ip_network=dict(aliases=['public_network']),
name=dict(required=True),
state=dict(default='present', choices=['absent', 'present']),
vip_address=dict(),
vip_network=dict(),
vip_port=dict(),
vip_subnet=dict(),
)
module_kwargs = dict(
required_if=[
('state', 'present', ('vip_network', 'vip_subnet', 'vip_port'),
True)
],
mutually_exclusive=[
('assign_floating_ip', 'delete_floating_ip'),
],
supports_check_mode=True,
)
def run(self):
state = self.params['state']
load_balancer = self._find()
if self.ansible.check_mode:
self.exit_json(changed=self._will_change(state, load_balancer))
if state == 'present' and not load_balancer:
# Create load_balancer
load_balancer, floating_ip = self._create()
self.exit_json(
changed=True,
load_balancer=load_balancer.to_dict(computed=False),
**(dict(floating_ip=floating_ip.to_dict(computed=False))
if floating_ip is not None else dict()))
elif state == 'present' and load_balancer:
# Update load_balancer
update, floating_ip = self._build_update(load_balancer)
if update:
load_balancer, floating_ip = self._update(load_balancer,
update)
self.exit_json(
changed=bool(update),
load_balancer=load_balancer.to_dict(computed=False),
**(dict(floating_ip=floating_ip.to_dict(computed=False))
if floating_ip is not None else dict()))
elif state == 'absent' and load_balancer:
# Delete load_balancer
self._delete(load_balancer)
self.exit_json(changed=True)
elif state == 'absent' and not load_balancer:
# Do nothing
self.exit_json(changed=False)
def _build_update(self, load_balancer):
update = {}
non_updateable_keys = [k for k in ['name', 'vip_address']
if self.params[k] is not None
and self.params[k] != load_balancer[k]]
flavor_name_or_id = self.params['flavor']
if flavor_name_or_id is not None:
flavor = self.conn.load_balancer.find_flavor(
flavor_name_or_id, ignore_missing=False)
if load_balancer['flavor_id'] != flavor.id:
non_updateable_keys.append('flavor_id')
vip_network_name_or_id = self.params['vip_network']
if vip_network_name_or_id is not None:
network = self.conn.network.find_network(
vip_network_name_or_id, ignore_missing=False)
if load_balancer['vip_network_id'] != network.id:
non_updateable_keys.append('vip_network_id')
vip_subnet_name_or_id = self.params['vip_subnet']
if vip_subnet_name_or_id is not None:
subnet = self.conn.network.find_subnet(
vip_subnet_name_or_id, ignore_missing=False)
if load_balancer['vip_subnet_id'] != subnet.id:
non_updateable_keys.append('vip_subnet_id')
vip_port_name_or_id = self.params['vip_port']
if vip_port_name_or_id is not None:
port = self.conn.network.find_port(
vip_port_name_or_id, ignore_missing=False)
if load_balancer['vip_port_id'] != port.id:
non_updateable_keys.append('vip_port_id')
if non_updateable_keys:
self.fail_json(msg='Cannot update parameters {0}'
.format(non_updateable_keys))
attributes = dict((k, self.params[k])
for k in ['description']
if self.params[k] is not None
and self.params[k] != load_balancer[k])
if attributes:
update['attributes'] = attributes
floating_ip, floating_ip_update = \
self._build_update_floating_ip(load_balancer)
return {**update, **floating_ip_update}, floating_ip
def _build_update_floating_ip(self, load_balancer):
assign_floating_ip = self.params['assign_floating_ip']
delete_floating_ip = self.params['delete_floating_ip']
floating_ip_address = self.params['floating_ip_address']
if floating_ip_address is not None \
and (not assign_floating_ip and not delete_floating_ip):
self.fail_json(msg="assign_floating_ip or delete_floating_ip must"
" be true when floating_ip_address is set")
floating_ip_network = self.params['floating_ip_network']
if floating_ip_network is not None \
and (not assign_floating_ip and not delete_floating_ip):
self.fail_json(msg="assign_floating_ip or delete_floating_ip must"
" be true when floating_ip_network is set")
ips = list(self.conn.network.ips(
port_id=load_balancer.vip_port_id,
fixed_ip_address=load_balancer.vip_address))
if len(ips) > 1:
self.fail_json(msg="Only a single floating ip address"
" per load-balancer is supported")
if delete_floating_ip or not assign_floating_ip:
if not ips:
return None, {}
if len(ips) != 1:
raise AssertionError("A single floating ip is expected")
ip = ips[0]
return ip, {'delete_floating_ip': ip}
# else assign_floating_ip
if not ips:
return None, dict(
assign_floating_ip=dict(
floating_ip_address=floating_ip_address,
floating_ip_network=floating_ip_network))
if len(ips) != 1:
raise AssertionError("A single floating ip is expected")
ip = ips[0]
if floating_ip_network is not None:
network = self.conn.network.find_network(floating_ip_network,
ignore_missing=False)
if ip.floating_network_id != network.id:
return ip, dict(
assign_floating_ip=dict(
floating_ip_address=floating_ip_address,
floating_ip_network=floating_ip_network),
delete_floating_ip=ip)
if floating_ip_address is not None \
and floating_ip_address != ip.floating_ip_address:
return ip, dict(
assign_floating_ip=dict(
floating_ip_address=floating_ip_address,
floating_ip_network=floating_ip_network),
delete_floating_ip=ip)
return ip, {}
def _create(self):
kwargs = dict((k, self.params[k])
for k in ['description', 'name', 'vip_address']
if self.params[k] is not None)
flavor_name_or_id = self.params['flavor']
if flavor_name_or_id is not None:
flavor = self.conn.load_balancer.find_flavor(
flavor_name_or_id, ignore_missing=False)
kwargs['flavor_id'] = flavor.id
vip_network_name_or_id = self.params['vip_network']
if vip_network_name_or_id is not None:
network = self.conn.network.find_network(
vip_network_name_or_id, ignore_missing=False)
kwargs['vip_network_id'] = network.id
vip_subnet_name_or_id = self.params['vip_subnet']
if vip_subnet_name_or_id is not None:
subnet = self.conn.network.find_subnet(
vip_subnet_name_or_id, ignore_missing=False)
kwargs['vip_subnet_id'] = subnet.id
vip_port_name_or_id = self.params['vip_port']
if vip_port_name_or_id is not None:
port = self.conn.network.find_port(
vip_port_name_or_id, ignore_missing=False)
kwargs['vip_port_id'] = port.id
load_balancer = self.conn.load_balancer.create_load_balancer(**kwargs)
if self.params['wait']:
load_balancer = self.conn.load_balancer.wait_for_load_balancer(
load_balancer.id,
wait=self.params['timeout'])
floating_ip, update = self._build_update_floating_ip(load_balancer)
if update:
load_balancer, floating_ip = \
self._update_floating_ip(load_balancer, update)
return load_balancer, floating_ip
def _delete(self, load_balancer):
if self.params['delete_floating_ip']:
ips = list(self.conn.network.ips(
port_id=load_balancer.vip_port_id,
fixed_ip_address=load_balancer.vip_address))
else:
ips = []
# With cascade=False the deletion of load-balancer
# would always fail if there are sub-resources.
self.conn.load_balancer.delete_load_balancer(load_balancer.id,
cascade=True)
if self.params['wait']:
for count in self.sdk.utils.iterate_timeout(
timeout=self.params['timeout'],
message="Timeout waiting for load-balancer to be absent"
):
if self.conn.load_balancer.\
find_load_balancer(load_balancer.id) is None:
break
for ip in ips:
self.conn.network.delete_ip(ip)
def _find(self):
name = self.params['name']
return self.conn.load_balancer.find_load_balancer(name_or_id=name)
def _update(self, load_balancer, update):
attributes = update.get('attributes')
if attributes:
load_balancer = \
self.conn.load_balancer.update_load_balancer(load_balancer.id,
**attributes)
if self.params['wait']:
load_balancer = self.conn.load_balancer.wait_for_load_balancer(
load_balancer.id,
wait=self.params['timeout'])
load_balancer, floating_ip = \
self._update_floating_ip(load_balancer, update)
return load_balancer, floating_ip
def _update_floating_ip(self, load_balancer, update):
floating_ip = None
delete_floating_ip = update.get('delete_floating_ip')
if delete_floating_ip:
self.conn.network.delete_ip(delete_floating_ip.id)
assign_floating_ip = update.get('assign_floating_ip')
if assign_floating_ip:
floating_ip_address = assign_floating_ip['floating_ip_address']
floating_ip_network = assign_floating_ip['floating_ip_network']
if floating_ip_network is not None:
network = self.conn.network.find_network(floating_ip_network,
ignore_missing=False)
else:
network = None
if floating_ip_address is not None:
kwargs = ({'floating_network_id': network.id}
if network is not None else {})
ip = self.conn.network.find_ip(floating_ip_address, **kwargs)
else:
ip = None
if ip:
if ip['port_id'] is not None:
self.fail_json(
msg="Floating ip {0} is associated to another fixed ip"
" address {1} already".format(
ip.floating_ip_address, ip.fixed_ip_address))
# Associate floating ip
floating_ip = self.conn.network.update_ip(
ip.id, fixed_ip_address=load_balancer.vip_address,
port_id=load_balancer.vip_port_id)
elif floating_ip_address: # and not ip
# Create new floating ip
kwargs = ({'floating_network_id': network.id}
if network is not None else {})
floating_ip = self.conn.network.create_ip(
fixed_ip_address=load_balancer.vip_address,
floating_ip_address=floating_ip_address,
port_id=load_balancer.vip_port_id,
**kwargs)
elif network:
# List disassociated floating ips on network
ips = [ip
for ip in
self.conn.network.ips(floating_network_id=network.id)
if ip['port_id'] is None]
if ips:
# Associate first disassociated floating ip
ip = ips[0]
floating_ip = self.conn.network.update_ip(
ip.id, fixed_ip_address=load_balancer.vip_address,
port_id=load_balancer.vip_port_id)
else:
# No disassociated floating ips
# Create new floating ip on network
floating_ip = self.conn.network.create_ip(
fixed_ip_address=load_balancer.vip_address,
floating_network_id=network.id,
port_id=load_balancer.vip_port_id)
else:
# Find disassociated floating ip
ip = self.conn.network.find_available_ip()
if ip:
# Associate disassociated floating ip
floating_ip = self.conn.network.update_ip(
ip.id, fixed_ip_address=load_balancer.vip_address,
port_id=load_balancer.vip_port_id)
else:
# Create new floating ip
floating_ip = self.conn.network.create_ip(
fixed_ip_address=load_balancer.vip_address,
port_id=load_balancer.vip_port_id)
return load_balancer, floating_ip
def _will_change(self, state, load_balancer):
if state == 'present' and not load_balancer:
return True
elif state == 'present' and load_balancer:
return bool(self._build_update(load_balancer)[0])
elif state == 'absent' and load_balancer:
return True
else:
# state == 'absent' and not load_balancer:
return False
def main():
module = LoadBalancerModule()
module()
if __name__ == "__main__":
main()