1658 lines
53 KiB
Python
1658 lines
53 KiB
Python
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (c) 2017 F5 Networks Inc.
|
|
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
|
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
|
'status': ['preview'],
|
|
'supported_by': 'community'}
|
|
|
|
DOCUMENTATION = r'''
|
|
---
|
|
module: bigip_virtual_server
|
|
short_description: Manage LTM virtual servers on a BIG-IP
|
|
description:
|
|
- Manage LTM virtual servers on a BIG-IP.
|
|
version_added: "2.1"
|
|
options:
|
|
state:
|
|
description:
|
|
- The virtual server state. If C(absent), delete the virtual server
|
|
if it exists. C(present) creates the virtual server and enable it.
|
|
If C(enabled), enable the virtual server if it exists. If C(disabled),
|
|
create the virtual server if needed, and set state to C(disabled).
|
|
default: present
|
|
choices:
|
|
- present
|
|
- absent
|
|
- enabled
|
|
- disabled
|
|
name:
|
|
description:
|
|
- Virtual server name.
|
|
required: True
|
|
aliases:
|
|
- vs
|
|
destination:
|
|
description:
|
|
- Destination IP of the virtual server.
|
|
- Required when C(state) is C(present) and virtual server does not exist.
|
|
required: True
|
|
aliases:
|
|
- address
|
|
- ip
|
|
source:
|
|
description:
|
|
- Specifies an IP address or network from which the virtual server accepts traffic.
|
|
- The virtual server accepts clients only from one of these IP addresses.
|
|
- For this setting to function effectively, specify a value other than 0.0.0.0/0 or ::/0
|
|
(that is, any/0, any6/0).
|
|
- In order to maximize utility of this setting, specify the most specific address
|
|
prefixes covering all customer addresses and no others.
|
|
- Specify the IP address in Classless Inter-Domain Routing (CIDR) format; address/prefix,
|
|
where the prefix length is in bits. For example, for IPv4, 10.0.0.1/32 or 10.0.0.0/24,
|
|
and for IPv6, ffe1::0020/64 or 2001:ed8:77b5:2:10:10:100:42/64.
|
|
version_added: 2.5
|
|
port:
|
|
description:
|
|
- Port of the virtual server. Required when C(state) is C(present)
|
|
and virtual server does not exist.
|
|
- If you do not want to specify a particular port, use the value C(0).
|
|
The result is that the virtual server will listen on any port.
|
|
profiles:
|
|
description:
|
|
- List of profiles (HTTP, ClientSSL, ServerSSL, etc) to apply to both sides
|
|
of the connection (client-side and server-side).
|
|
- If you only want to apply a particular profile to the client-side of
|
|
the connection, specify C(client-side) for the profile's C(context).
|
|
- If you only want to apply a particular profile to the server-side of
|
|
the connection, specify C(server-side) for the profile's C(context).
|
|
- If C(context) is not provided, it will default to C(all).
|
|
suboptions:
|
|
name:
|
|
description:
|
|
- Name of the profile.
|
|
- If this is not specified, then it is assumed that the profile item is
|
|
only a name of a profile.
|
|
- This must be specified if a context is specified.
|
|
required: false
|
|
context:
|
|
description:
|
|
- The side of the connection on which the profile should be applied.
|
|
choices:
|
|
- all
|
|
- server-side
|
|
- client-side
|
|
default: all
|
|
aliases:
|
|
- all_profiles
|
|
irules:
|
|
version_added: "2.2"
|
|
description:
|
|
- List of rules to be applied in priority order.
|
|
- If you want to remove existing iRules, specify a single empty value; C("").
|
|
See the documentation for an example.
|
|
aliases:
|
|
- all_rules
|
|
enabled_vlans:
|
|
version_added: "2.2"
|
|
description:
|
|
- List of VLANs to be enabled. When a VLAN named C(all) is used, all
|
|
VLANs will be allowed. VLANs can be specified with or without the
|
|
leading partition. If the partition is not specified in the VLAN,
|
|
then the C(partition) option of this module will be used.
|
|
- This parameter is mutually exclusive with the C(disabled_vlans) parameter.
|
|
disabled_vlans:
|
|
version_added: 2.5
|
|
description:
|
|
- List of VLANs to be disabled. If the partition is not specified in the VLAN,
|
|
then the C(partition) option of this module will be used.
|
|
- This parameter is mutually exclusive with the C(enabled_vlans) parameters.
|
|
pool:
|
|
description:
|
|
- Default pool for the virtual server.
|
|
- If you want to remove the existing pool, specify an empty value; C("").
|
|
See the documentation for an example.
|
|
policies:
|
|
description:
|
|
- Specifies the policies for the virtual server
|
|
aliases:
|
|
- all_policies
|
|
snat:
|
|
description:
|
|
- Source network address policy.
|
|
required: false
|
|
choices:
|
|
- None
|
|
- Automap
|
|
- Name of a SNAT pool (eg "/Common/snat_pool_name") to enable SNAT
|
|
with the specific pool
|
|
default_persistence_profile:
|
|
description:
|
|
- Default Profile which manages the session persistence.
|
|
- If you want to remove the existing default persistence profile, specify an
|
|
empty value; C(""). See the documentation for an example.
|
|
description:
|
|
description:
|
|
- Virtual server description.
|
|
fallback_persistence_profile:
|
|
description:
|
|
- Specifies the persistence profile you want the system to use if it
|
|
cannot use the specified default persistence profile.
|
|
- If you want to remove the existing fallback persistence profile, specify an
|
|
empty value; C(""). See the documentation for an example.
|
|
version_added: 2.3
|
|
partition:
|
|
description:
|
|
- Device partition to manage resources on.
|
|
default: Common
|
|
version_added: 2.5
|
|
metadata:
|
|
description:
|
|
- Arbitrary key/value pairs that you can attach to a pool. This is useful in
|
|
situations where you might want to annotate a virtual to me managed by Ansible.
|
|
- Key names will be stored as strings; this includes names that are numbers.
|
|
- Values for all of the keys will be stored as strings; this includes values
|
|
that are numbers.
|
|
- Data will be persisted, not ephemeral.
|
|
version_added: 2.5
|
|
notes:
|
|
- Requires BIG-IP software version >= 11
|
|
- Requires the netaddr Python package on the host. This is as easy as pip
|
|
install netaddr.
|
|
requirements:
|
|
- netaddr
|
|
extends_documentation_fragment: f5
|
|
author:
|
|
- Tim Rupp (@caphrim007)
|
|
'''
|
|
|
|
EXAMPLES = r'''
|
|
- name: Modify Port of the Virtual Server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
state: present
|
|
partition: Common
|
|
name: my-virtual-server
|
|
port: 8080
|
|
delegate_to: localhost
|
|
|
|
- name: Delete virtual server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
state: absent
|
|
partition: Common
|
|
name: my-virtual-server
|
|
delegate_to: localhost
|
|
|
|
- name: Add virtual server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
state: present
|
|
partition: Common
|
|
name: my-virtual-server
|
|
destination: 10.10.10.10
|
|
port: 443
|
|
pool: my-pool
|
|
snat: Automap
|
|
description: Test Virtual Server
|
|
profiles:
|
|
- http
|
|
- fix
|
|
- name: clientssl
|
|
context: server-side
|
|
- name: ilx
|
|
context: client-side
|
|
policies:
|
|
- my-ltm-policy-for-asm
|
|
- ltm-uri-policy
|
|
- ltm-policy-2
|
|
- ltm-policy-3
|
|
enabled_vlans:
|
|
- /Common/vlan2
|
|
delegate_to: localhost
|
|
|
|
- name: Add FastL4 virtual server
|
|
bigip_virtual_server:
|
|
destination: 1.1.1.1
|
|
name: fastl4_vs
|
|
port: 80
|
|
profiles:
|
|
- fastL4
|
|
state: present
|
|
|
|
- name: Add iRules to the Virtual Server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
name: my-virtual-server
|
|
irules:
|
|
- irule1
|
|
- irule2
|
|
delegate_to: localhost
|
|
|
|
- name: Remove one iRule from the Virtual Server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
name: my-virtual-server
|
|
irules:
|
|
- irule2
|
|
delegate_to: localhost
|
|
|
|
- name: Remove all iRules from the Virtual Server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
name: my-virtual-server
|
|
irules: ""
|
|
delegate_to: localhost
|
|
|
|
- name: Remove pool from the Virtual Server
|
|
bigip_virtual_server:
|
|
server: lb.mydomain.net
|
|
user: admin
|
|
password: secret
|
|
name: my-virtual-server
|
|
pool: ""
|
|
delegate_to: localhost
|
|
|
|
- name: Add metadata to virtual
|
|
bigip_pool:
|
|
server: lb.mydomain.com
|
|
user: admin
|
|
password: secret
|
|
state: absent
|
|
name: my-pool
|
|
partition: Common
|
|
metadata:
|
|
ansible: 2.4
|
|
updated_at: 2017-12-20T17:50:46Z
|
|
delegate_to: localhost
|
|
'''
|
|
|
|
RETURN = r'''
|
|
description:
|
|
description: New description of the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: This is my description
|
|
default_persistence_profile:
|
|
description: Default persistence profile set on the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: /Common/dest_addr
|
|
destination:
|
|
description: Destination of the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: 1.1.1.1
|
|
disabled:
|
|
description: Whether the virtual server is disabled, or not.
|
|
returned: changed
|
|
type: bool
|
|
sample: True
|
|
disabled_vlans:
|
|
description: List of VLANs that the virtual is disabled for.
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/vlan1', '/Common/vlan2']
|
|
enabled:
|
|
description: Whether the virtual server is enabled, or not.
|
|
returned: changed
|
|
type: bool
|
|
sample: False
|
|
enabled_vlans:
|
|
description: List of VLANs that the virtual is enabled for.
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/vlan5', '/Common/vlan6']
|
|
fallback_persistence_profile:
|
|
description: Fallback persistence profile set on the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: /Common/source_addr
|
|
irules:
|
|
description: iRules set on the virtual server.
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/irule1', '/Common/irule2']
|
|
pool:
|
|
description: Pool that the virtual server is attached to.
|
|
returned: changed
|
|
type: string
|
|
sample: /Common/my-pool
|
|
policies:
|
|
description: List of policies attached to the virtual.
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/policy1', '/Common/policy2']
|
|
port:
|
|
description: Port that the virtual server is configured to listen on.
|
|
returned: changed
|
|
type: int
|
|
sample: 80
|
|
profiles:
|
|
description: List of profiles set on the virtual server.
|
|
returned: changed
|
|
type: list
|
|
sample: [{'name': 'tcp', 'context': 'server-side'}, {'name': 'tcp-legacy', 'context': 'client-side'}]
|
|
snat:
|
|
description: SNAT setting of the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: Automap
|
|
source:
|
|
description: Source address, in CIDR form, set on the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: 1.2.3.4/32
|
|
metadata:
|
|
description: The new value of the virtual.
|
|
returned: changed
|
|
type: dict
|
|
sample: {'key1': 'foo', 'key2': 'bar'}
|
|
'''
|
|
|
|
import re
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.basic import env_fallback
|
|
from ansible.module_utils.six import iteritems
|
|
from collections import namedtuple
|
|
|
|
try:
|
|
# Sideband repository used for dev
|
|
from library.module_utils.network.f5.bigip import HAS_F5SDK
|
|
from library.module_utils.network.f5.bigip import F5Client
|
|
from library.module_utils.network.f5.common import F5ModuleError
|
|
from library.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from library.module_utils.network.f5.common import cleanup_tokens
|
|
from library.module_utils.network.f5.common import fqdn_name
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
try:
|
|
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
except ImportError:
|
|
HAS_F5SDK = False
|
|
HAS_DEVEL_IMPORTS = True
|
|
except ImportError:
|
|
# Upstream Ansible
|
|
from ansible.module_utils.network.f5.bigip import HAS_F5SDK
|
|
from ansible.module_utils.network.f5.bigip import F5Client
|
|
from ansible.module_utils.network.f5.common import F5ModuleError
|
|
from ansible.module_utils.network.f5.common import AnsibleF5Parameters
|
|
from ansible.module_utils.network.f5.common import cleanup_tokens
|
|
from ansible.module_utils.network.f5.common import fqdn_name
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
try:
|
|
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
except ImportError:
|
|
HAS_F5SDK = False
|
|
|
|
try:
|
|
import netaddr
|
|
HAS_NETADDR = True
|
|
except ImportError:
|
|
HAS_NETADDR = False
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
api_map = {
|
|
'sourceAddressTranslation': 'snat',
|
|
'fallbackPersistence': 'fallback_persistence_profile',
|
|
'persist': 'default_persistence_profile',
|
|
'vlansEnabled': 'vlans_enabled',
|
|
'vlansDisabled': 'vlans_disabled',
|
|
'profilesReference': 'profiles',
|
|
'policiesReference': 'policies',
|
|
'rules': 'irules'
|
|
}
|
|
|
|
api_attributes = [
|
|
'description',
|
|
'destination',
|
|
'disabled',
|
|
'enabled',
|
|
'fallbackPersistence',
|
|
'metadata',
|
|
'persist',
|
|
'policies',
|
|
'pool',
|
|
'profiles',
|
|
'rules',
|
|
'source',
|
|
'sourceAddressTranslation',
|
|
'vlans',
|
|
'vlansEnabled',
|
|
'vlansDisabled',
|
|
]
|
|
|
|
updatables = [
|
|
'description',
|
|
'default_persistence_profile',
|
|
'destination',
|
|
'disabled_vlans',
|
|
'enabled',
|
|
'enabled_vlans',
|
|
'fallback_persistence_profile',
|
|
'irules',
|
|
'metadata',
|
|
'pool',
|
|
'policies',
|
|
'port',
|
|
'profiles',
|
|
'snat',
|
|
'source'
|
|
]
|
|
|
|
returnables = [
|
|
'description',
|
|
'default_persistence_profile',
|
|
'destination',
|
|
'disabled',
|
|
'disabled_vlans',
|
|
'enabled',
|
|
'enabled_vlans',
|
|
'fallback_persistence_profile',
|
|
'irules',
|
|
'metadata',
|
|
'pool',
|
|
'policies',
|
|
'port',
|
|
'profiles',
|
|
'snat',
|
|
'source',
|
|
'vlans',
|
|
'vlans_enabled',
|
|
'vlans_disabled'
|
|
]
|
|
|
|
profiles_mutex = [
|
|
'sip', 'sipsession', 'iiop', 'rtsp', 'http', 'diameter',
|
|
'diametersession', 'radius', 'ftp', 'tftp', 'dns', 'pptp', 'fix'
|
|
]
|
|
|
|
def to_return(self):
|
|
result = {}
|
|
for returnable in self.returnables:
|
|
try:
|
|
result[returnable] = getattr(self, returnable)
|
|
except Exception as ex:
|
|
pass
|
|
result = self._filter_params(result)
|
|
return result
|
|
|
|
def _fqdn_name(self, value):
|
|
if value is not None and not value.startswith('/'):
|
|
return '/{0}/{1}'.format(self.partition, value)
|
|
return value
|
|
|
|
def is_valid_ip(self, value):
|
|
try:
|
|
netaddr.IPAddress(value)
|
|
return True
|
|
except (netaddr.core.AddrFormatError, ValueError):
|
|
return False
|
|
|
|
def _format_port_for_destination(self, ip, port):
|
|
addr = netaddr.IPAddress(ip)
|
|
if addr.version == 6:
|
|
if port == 0:
|
|
result = '.any'
|
|
else:
|
|
result = '.{0}'.format(port)
|
|
else:
|
|
result = ':{0}'.format(port)
|
|
return result
|
|
|
|
def _format_destination(self, address, port, route_domain):
|
|
if port is None:
|
|
if route_domain is None:
|
|
result = '{0}'.format(
|
|
self._fqdn_name(address)
|
|
)
|
|
else:
|
|
result = '{0}%{1}'.format(
|
|
self._fqdn_name(address),
|
|
route_domain
|
|
)
|
|
else:
|
|
port = self._format_port_for_destination(address, port)
|
|
if route_domain is None:
|
|
result = '{0}{1}'.format(
|
|
self._fqdn_name(address),
|
|
port
|
|
)
|
|
else:
|
|
result = '{0}%{1}{2}'.format(
|
|
self._fqdn_name(address),
|
|
route_domain,
|
|
port
|
|
)
|
|
return result
|
|
|
|
|
|
class ApiParameters(Parameters):
|
|
@property
|
|
def destination(self):
|
|
if self._values['destination'] is None:
|
|
return None
|
|
destination = self.destination_tuple
|
|
result = self._format_destination(destination.ip, destination.port, destination.route_domain)
|
|
return result
|
|
|
|
@property
|
|
def source(self):
|
|
if self._values['source'] is None:
|
|
return None
|
|
try:
|
|
addr = netaddr.IPNetwork(self._values['source'])
|
|
result = '{0}/{1}'.format(str(addr.ip), addr.prefixlen)
|
|
return result
|
|
except netaddr.core.AddrFormatError:
|
|
raise F5ModuleError(
|
|
"The source IP address must be specified in CIDR format: address/prefix"
|
|
)
|
|
|
|
@property
|
|
def destination_tuple(self):
|
|
Destination = namedtuple('Destination', ['ip', 'port', 'route_domain'])
|
|
|
|
# Remove the partition
|
|
if self._values['destination'] is None:
|
|
result = Destination(ip=None, port=None, route_domain=None)
|
|
return result
|
|
destination = re.sub(r'^/[a-zA-Z0-9_.-]+/', '', self._values['destination'])
|
|
|
|
if self.is_valid_ip(destination):
|
|
result = Destination(
|
|
ip=destination,
|
|
port=None,
|
|
route_domain=None
|
|
)
|
|
return result
|
|
|
|
# Covers the following examples
|
|
#
|
|
# /Common/2700:bc00:1f10:101::6%2.80
|
|
# 2700:bc00:1f10:101::6%2.80
|
|
# 1.1.1.1%2:80
|
|
# /Common/1.1.1.1%2:80
|
|
# /Common/2700:bc00:1f10:101::6%2.any
|
|
#
|
|
pattern = r'(?P<ip>[^%]+)%(?P<route_domain>[0-9]+)[:.](?P<port>[0-9]+|any)'
|
|
matches = re.search(pattern, destination)
|
|
if matches:
|
|
try:
|
|
port = int(matches.group('port'))
|
|
except ValueError:
|
|
# Can be a port of "any". This only happens with IPv6
|
|
port = matches.group('port')
|
|
if port == 'any':
|
|
port = 0
|
|
ip = matches.group('ip')
|
|
if not self.is_valid_ip(ip):
|
|
raise F5ModuleError(
|
|
"The provided destination is not a valid IP address"
|
|
)
|
|
result = Destination(
|
|
ip=matches.group('ip'),
|
|
port=port,
|
|
route_domain=int(matches.group('route_domain'))
|
|
)
|
|
return result
|
|
|
|
pattern = r'(?P<ip>[^%]+)%(?P<route_domain>[0-9]+)'
|
|
matches = re.search(pattern, destination)
|
|
if matches:
|
|
ip = matches.group('ip')
|
|
if not self.is_valid_ip(ip):
|
|
raise F5ModuleError(
|
|
"The provided destination is not a valid IP address"
|
|
)
|
|
result = Destination(
|
|
ip=matches.group('ip'),
|
|
port=None,
|
|
route_domain=int(matches.group('route_domain'))
|
|
)
|
|
return result
|
|
|
|
parts = destination.split('.')
|
|
if len(parts) == 4:
|
|
# IPv4
|
|
ip, port = destination.split(':')
|
|
if not self.is_valid_ip(ip):
|
|
raise F5ModuleError(
|
|
"The provided destination is not a valid IP address"
|
|
)
|
|
result = Destination(
|
|
ip=ip,
|
|
port=int(port),
|
|
route_domain=None
|
|
)
|
|
return result
|
|
elif len(parts) == 2:
|
|
# IPv6
|
|
ip, port = destination.split('.')
|
|
try:
|
|
port = int(port)
|
|
except ValueError:
|
|
# Can be a port of "any". This only happens with IPv6
|
|
if port == 'any':
|
|
port = 0
|
|
if not self.is_valid_ip(ip):
|
|
raise F5ModuleError(
|
|
"The provided destination is not a valid IP address"
|
|
)
|
|
result = Destination(
|
|
ip=ip,
|
|
port=port,
|
|
route_domain=None
|
|
)
|
|
return result
|
|
else:
|
|
result = Destination(ip=None, port=None, route_domain=None)
|
|
return result
|
|
|
|
@property
|
|
def port(self):
|
|
destination = self.destination_tuple
|
|
self._values['port'] = destination.port
|
|
return destination.port
|
|
|
|
@property
|
|
def route_domain(self):
|
|
destination = self.destination_tuple
|
|
self._values['route_domain'] = destination.route_domain
|
|
return destination.route_domain
|
|
|
|
@property
|
|
def profiles(self):
|
|
if 'items' not in self._values['profiles']:
|
|
return None
|
|
result = []
|
|
for item in self._values['profiles']['items']:
|
|
context = item['context']
|
|
name = item['name']
|
|
if context in ['all', 'serverside', 'clientside']:
|
|
result.append(dict(name=name, context=context, fullPath=item['fullPath']))
|
|
else:
|
|
raise F5ModuleError(
|
|
"Unknown profile context found: '{0}'".format(context)
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def policies(self):
|
|
if 'items' not in self._values['policies']:
|
|
return None
|
|
result = []
|
|
for item in self._values['policies']['items']:
|
|
name = item['name']
|
|
partition = item['partition']
|
|
result.append(dict(name=name, partition=partition))
|
|
return result
|
|
|
|
@property
|
|
def default_persistence_profile(self):
|
|
if self._values['default_persistence_profile'] is None:
|
|
return None
|
|
# These persistence profiles are always lists when we get them
|
|
# from the REST API even though there can only be one. We'll
|
|
# make it a list again when we get to the Difference engine.
|
|
return self._values['default_persistence_profile'][0]
|
|
|
|
@property
|
|
def enabled(self):
|
|
if 'enabled' in self._values:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
@property
|
|
def disabled(self):
|
|
if 'disabled' in self._values:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def metadata(self):
|
|
if self._values['metadata'] is None:
|
|
return None
|
|
result = []
|
|
for md in self._values['metadata']:
|
|
tmp = dict(name=str(md['name']))
|
|
if 'value' in md:
|
|
tmp['value'] = str(md['value'])
|
|
else:
|
|
tmp['value'] = ''
|
|
result.append(tmp)
|
|
return result
|
|
|
|
|
|
class ModuleParameters(Parameters):
|
|
def _handle_profile_context(self, tmp):
|
|
if 'context' not in tmp:
|
|
tmp['context'] = 'all'
|
|
else:
|
|
if 'name' not in tmp:
|
|
raise F5ModuleError(
|
|
"A profile name must be specified when a context is specified."
|
|
)
|
|
tmp['context'] = tmp['context'].replace('server-side', 'serverside')
|
|
tmp['context'] = tmp['context'].replace('client-side', 'clientside')
|
|
|
|
def _handle_clientssl_profile_nuances(self, profile):
|
|
if profile['name'] != 'clientssl':
|
|
return
|
|
if profile['context'] != 'clientside':
|
|
profile['context'] = 'clientside'
|
|
|
|
@property
|
|
def destination(self):
|
|
addr = self._values['destination'].split("%")[0]
|
|
if not self.is_valid_ip(addr):
|
|
raise F5ModuleError(
|
|
"The provided destination is not a valid IP address"
|
|
)
|
|
result = self._format_destination(addr, self.port, self.route_domain)
|
|
return result
|
|
|
|
@property
|
|
def destination_tuple(self):
|
|
Destination = namedtuple('Destination', ['ip', 'port', 'route_domain'])
|
|
if self._values['destination'] is None:
|
|
result = Destination(ip=None, port=None, route_domain=None)
|
|
return result
|
|
addr = self._values['destination'].split("%")[0]
|
|
result = Destination(ip=addr, port=self.port, route_domain=self.route_domain)
|
|
return result
|
|
|
|
@property
|
|
def source(self):
|
|
if self._values['source'] is None:
|
|
return None
|
|
try:
|
|
addr = netaddr.IPNetwork(self._values['source'])
|
|
result = '{0}/{1}'.format(str(addr.ip), addr.prefixlen)
|
|
return result
|
|
except netaddr.core.AddrFormatError:
|
|
raise F5ModuleError(
|
|
"The source IP address must be specified in CIDR format: address/prefix"
|
|
)
|
|
|
|
@property
|
|
def port(self):
|
|
if self._values['port'] is None:
|
|
return None
|
|
if self._values['port'] in ['*', 'any']:
|
|
return 0
|
|
self._check_port()
|
|
return int(self._values['port'])
|
|
|
|
def _check_port(self):
|
|
try:
|
|
port = int(self._values['port'])
|
|
except ValueError:
|
|
raise F5ModuleError(
|
|
"The specified port was not a valid integer"
|
|
)
|
|
if 0 <= port <= 65535:
|
|
return port
|
|
raise F5ModuleError(
|
|
"Valid ports must be in range 0 - 65535"
|
|
)
|
|
|
|
@property
|
|
def irules(self):
|
|
results = []
|
|
if self._values['irules'] is None:
|
|
return None
|
|
if len(self._values['irules']) == 1 and self._values['irules'][0] == '':
|
|
return ''
|
|
for irule in self._values['irules']:
|
|
result = self._fqdn_name(irule)
|
|
results.append(result)
|
|
return results
|
|
|
|
@property
|
|
def profiles(self):
|
|
if self._values['profiles'] is None:
|
|
return None
|
|
if len(self._values['profiles']) == 1 and self._values['profiles'][0] == '':
|
|
return ''
|
|
result = []
|
|
for profile in self._values['profiles']:
|
|
tmp = dict()
|
|
if isinstance(profile, dict):
|
|
tmp.update(profile)
|
|
self._handle_profile_context(tmp)
|
|
if 'name' not in profile:
|
|
tmp['name'] = profile
|
|
tmp['fullPath'] = self._fqdn_name(tmp['name'])
|
|
self._handle_clientssl_profile_nuances(tmp)
|
|
else:
|
|
tmp['name'] = profile
|
|
tmp['context'] = 'all'
|
|
tmp['fullPath'] = self._fqdn_name(tmp['name'])
|
|
self._handle_clientssl_profile_nuances(tmp)
|
|
result.append(tmp)
|
|
mutually_exclusive = [x['name'] for x in result if x in self.profiles_mutex]
|
|
if len(mutually_exclusive) > 1:
|
|
raise F5ModuleError(
|
|
"Profiles {0} are mutually exclusive".format(
|
|
', '.join(self.profiles_mutex).strip()
|
|
)
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def policies(self):
|
|
if self._values['policies'] is None:
|
|
return None
|
|
if len(self._values['policies']) == 1 and self._values['policies'][0] == '':
|
|
return ''
|
|
result = []
|
|
policies = [self._fqdn_name(p) for p in self._values['policies']]
|
|
policies = set(policies)
|
|
for policy in policies:
|
|
parts = policy.split('/')
|
|
if len(parts) != 3:
|
|
raise F5ModuleError(
|
|
"The specified policy '{0}' is malformed".format(policy)
|
|
)
|
|
tmp = dict(
|
|
name=parts[2],
|
|
partition=parts[1]
|
|
)
|
|
result.append(tmp)
|
|
return result
|
|
|
|
@property
|
|
def pool(self):
|
|
if self._values['pool'] is None:
|
|
return None
|
|
if self._values['pool'] == '':
|
|
return ''
|
|
return self._fqdn_name(self._values['pool'])
|
|
|
|
@property
|
|
def vlans_enabled(self):
|
|
if self._values['enabled_vlans'] is None:
|
|
return None
|
|
elif self._values['vlans_enabled'] is False:
|
|
# This is a special case for "all" enabled VLANs
|
|
return False
|
|
if self._values['disabled_vlans'] is None:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def vlans_disabled(self):
|
|
if self._values['disabled_vlans'] is None:
|
|
return None
|
|
elif self._values['vlans_disabled'] is True:
|
|
# This is a special case for "all" enabled VLANs
|
|
return True
|
|
elif self._values['enabled_vlans'] is None:
|
|
return True
|
|
return False
|
|
|
|
@property
|
|
def enabled_vlans(self):
|
|
if self._values['enabled_vlans'] is None:
|
|
return None
|
|
elif any(x.lower() for x in self._values['enabled_vlans'] if x.lower() in ['all', '*']):
|
|
result = [self._fqdn_name('all')]
|
|
if result[0].endswith('/all'):
|
|
if self._values['__warnings'] is None:
|
|
self._values['__warnings'] = []
|
|
self._values['__warnings'].append(
|
|
dict(
|
|
msg="Usage of the 'ALL' value for 'enabled_vlans' parameter is deprecated. Use '*' instead",
|
|
version='2.5'
|
|
)
|
|
)
|
|
return result
|
|
results = list(set([self._fqdn_name(x) for x in self._values['enabled_vlans']]))
|
|
results.sort()
|
|
return results
|
|
|
|
@property
|
|
def disabled_vlans(self):
|
|
if self._values['disabled_vlans'] is None:
|
|
return None
|
|
elif any(x.lower() for x in self._values['disabled_vlans'] if x.lower() in ['all', '*']):
|
|
raise F5ModuleError(
|
|
"You cannot disable all VLANs. You must name them individually."
|
|
)
|
|
results = list(set([self._fqdn_name(x) for x in self._values['disabled_vlans']]))
|
|
results.sort()
|
|
return results
|
|
|
|
@property
|
|
def vlans(self):
|
|
disabled = self.disabled_vlans
|
|
if disabled:
|
|
return self.disabled_vlans
|
|
return self.enabled_vlans
|
|
|
|
@property
|
|
def state(self):
|
|
if self._values['state'] == 'present':
|
|
return 'enabled'
|
|
return self._values['state']
|
|
|
|
@property
|
|
def snat(self):
|
|
if self._values['snat'] is None:
|
|
return None
|
|
lowercase = self._values['snat'].lower()
|
|
if lowercase in ['automap', 'none']:
|
|
return dict(type=lowercase)
|
|
snat_pool = self._fqdn_name(self._values['snat'])
|
|
return dict(pool=snat_pool, type='snat')
|
|
|
|
@property
|
|
def default_persistence_profile(self):
|
|
if self._values['default_persistence_profile'] is None:
|
|
return None
|
|
if self._values['default_persistence_profile'] == '':
|
|
return ''
|
|
profile = self._fqdn_name(self._values['default_persistence_profile'])
|
|
parts = profile.split('/')
|
|
if len(parts) != 3:
|
|
raise F5ModuleError(
|
|
"The specified 'default_persistence_profile' is malformed"
|
|
)
|
|
result = dict(
|
|
name=parts[2],
|
|
partition=parts[1]
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def fallback_persistence_profile(self):
|
|
if self._values['fallback_persistence_profile'] is None:
|
|
return None
|
|
if self._values['fallback_persistence_profile'] == '':
|
|
return ''
|
|
result = self._fqdn_name(self._values['fallback_persistence_profile'])
|
|
return result
|
|
|
|
@property
|
|
def enabled(self):
|
|
if self._values['state'] == 'enabled':
|
|
return True
|
|
elif self._values['state'] == 'disabled':
|
|
return False
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def disabled(self):
|
|
if self._values['state'] == 'enabled':
|
|
return False
|
|
elif self._values['state'] == 'disabled':
|
|
return True
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def metadata(self):
|
|
if self._values['metadata'] is None:
|
|
return None
|
|
if self._values['metadata'] == '':
|
|
return []
|
|
result = []
|
|
try:
|
|
for k, v in iteritems(self._values['metadata']):
|
|
tmp = dict(name=str(k))
|
|
if v:
|
|
tmp['value'] = str(v)
|
|
else:
|
|
tmp['value'] = ''
|
|
result.append(tmp)
|
|
except AttributeError:
|
|
raise F5ModuleError(
|
|
"The 'metadata' parameter must be a dictionary of key/value pairs."
|
|
)
|
|
return result
|
|
|
|
|
|
class Changes(Parameters):
|
|
pass
|
|
|
|
|
|
class UsableChanges(Changes):
|
|
@property
|
|
def vlans(self):
|
|
if self._values['vlans'] is None:
|
|
return None
|
|
elif len(self._values['vlans']) == 0:
|
|
return []
|
|
elif any(x for x in self._values['vlans'] if x.lower() in ['/common/all', 'all']):
|
|
return []
|
|
return self._values['vlans']
|
|
|
|
|
|
class ReportableChanges(Changes):
|
|
@property
|
|
def snat(self):
|
|
if self._values['snat'] is None:
|
|
return None
|
|
result = self._values['snat'].get('type', None)
|
|
if result == 'automap':
|
|
return 'Automap'
|
|
elif result == 'none':
|
|
return 'none'
|
|
result = self._values['snat'].get('pool', None)
|
|
return result
|
|
|
|
@property
|
|
def destination(self):
|
|
params = ApiParameters(params=dict(destination=self._values['destination']))
|
|
result = params.destination_tuple.ip
|
|
return result
|
|
|
|
@property
|
|
def port(self):
|
|
params = ApiParameters(params=dict(destination=self._values['destination']))
|
|
result = params.destination_tuple.port
|
|
return result
|
|
|
|
@property
|
|
def default_persistence_profile(self):
|
|
if len(self._values['default_persistence_profile']) == 0:
|
|
return []
|
|
profile = self._values['default_persistence_profile'][0]
|
|
result = '/{0}/{1}'.format(profile['partition'], profile['name'])
|
|
return result
|
|
|
|
@property
|
|
def policies(self):
|
|
if len(self._values['policies']) == 0:
|
|
return []
|
|
result = ['/{0}/{1}'.format(x['partition'], x['name']) for x in self._values['policies']]
|
|
return result
|
|
|
|
@property
|
|
def enabled_vlans(self):
|
|
if len(self._values['vlans']) == 0 and self._values['vlans_disabled'] is True:
|
|
return 'all'
|
|
elif len(self._values['vlans']) > 0 and self._values['vlans_enabled'] is True:
|
|
return self._values['vlans']
|
|
|
|
@property
|
|
def disabled_vlans(self):
|
|
if len(self._values['vlans']) > 0 and self._values['vlans_disabled'] is True:
|
|
return self._values['vlans']
|
|
|
|
|
|
class Difference(object):
|
|
def __init__(self, want, have=None):
|
|
self.have = have
|
|
self.want = want
|
|
|
|
def compare(self, param):
|
|
try:
|
|
result = getattr(self, param)
|
|
return result
|
|
except AttributeError:
|
|
result = self.__default(param)
|
|
return result
|
|
|
|
def __default(self, param):
|
|
attr1 = getattr(self.want, param)
|
|
try:
|
|
attr2 = getattr(self.have, param)
|
|
if attr1 != attr2:
|
|
return attr1
|
|
except AttributeError:
|
|
return attr1
|
|
|
|
def to_tuple(self, items):
|
|
result = []
|
|
for x in items:
|
|
tmp = [(str(k), str(v)) for k, v in iteritems(x)]
|
|
result += tmp
|
|
return result
|
|
|
|
def _diff_complex_items(self, want, have):
|
|
if want == [] and have is None:
|
|
return None
|
|
if want is None:
|
|
return None
|
|
w = self.to_tuple(want)
|
|
h = self.to_tuple(have)
|
|
if set(w).issubset(set(h)):
|
|
return None
|
|
else:
|
|
return want
|
|
|
|
def _update_vlan_status(self, result):
|
|
if self.want.vlans_disabled is not None:
|
|
if self.want.vlans_disabled != self.have.vlans_disabled:
|
|
result['vlans_disabled'] = self.want.vlans_disabled
|
|
result['vlans_enabled'] = not self.want.vlans_disabled
|
|
elif self.want.vlans_enabled is not None:
|
|
if any(x.lower().endswith('/all') for x in self.want.vlans):
|
|
if self.have.vlans_enabled is True:
|
|
result['vlans_disabled'] = True
|
|
result['vlans_enabled'] = False
|
|
elif self.want.vlans_enabled != self.have.vlans_enabled:
|
|
result['vlans_disabled'] = not self.want.vlans_enabled
|
|
result['vlans_enabled'] = self.want.vlans_enabled
|
|
|
|
@property
|
|
def destination(self):
|
|
addr_tuple = [self.want.destination, self.want.port, self.want.route_domain]
|
|
if all(x for x in addr_tuple if x is None):
|
|
return None
|
|
|
|
have = self.have.destination_tuple
|
|
|
|
if self.want.port is None:
|
|
self.want.update({'port': have.port})
|
|
if self.want.route_domain is None:
|
|
self.want.update({'route_domain': have.route_domain})
|
|
if self.want.destination_tuple.ip is None:
|
|
address = have.ip
|
|
else:
|
|
address = self.want.destination_tuple.ip
|
|
|
|
want = self.want._format_destination(address, self.want.port, self.want.route_domain)
|
|
if want != self.have.destination:
|
|
return self.want._fqdn_name(want)
|
|
|
|
@property
|
|
def source(self):
|
|
if self.want.source is None:
|
|
return None
|
|
want = netaddr.IPNetwork(self.want.source)
|
|
have = netaddr.IPNetwork(self.have.destination_tuple.ip)
|
|
if want.version != have.version:
|
|
raise F5ModuleError(
|
|
"The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)."
|
|
)
|
|
if self.want.source != self.have.source:
|
|
return self.want.source
|
|
|
|
@property
|
|
def vlans(self):
|
|
if self.want.vlans is None:
|
|
return None
|
|
elif self.want.vlans == [] and self.have.vlans is None:
|
|
return None
|
|
elif self.want.vlans == self.have.vlans:
|
|
return None
|
|
|
|
# Specifically looking for /all because the vlans return value will be
|
|
# an FQDN list. This means that "all" will be returned as "/partition/all",
|
|
# ex, /Common/all.
|
|
#
|
|
# We do not want to accidentally match values that would end with the word
|
|
# "all", like "vlansall". Therefore we look for the forward slash because this
|
|
# is a path delimiter.
|
|
elif any(x.lower().endswith('/all') for x in self.want.vlans):
|
|
if self.have.vlans is None:
|
|
return None
|
|
else:
|
|
return []
|
|
else:
|
|
return self.want.vlans
|
|
|
|
@property
|
|
def enabled_vlans(self):
|
|
return self.vlan_status
|
|
|
|
@property
|
|
def disabled_vlans(self):
|
|
return self.vlan_status
|
|
|
|
@property
|
|
def vlan_status(self):
|
|
result = dict()
|
|
vlans = self.vlans
|
|
if vlans is not None:
|
|
result['vlans'] = vlans
|
|
self._update_vlan_status(result)
|
|
return result
|
|
|
|
@property
|
|
def port(self):
|
|
result = self.destination
|
|
if result is not None:
|
|
return dict(
|
|
destination=result
|
|
)
|
|
|
|
@property
|
|
def profiles(self):
|
|
if self.want.profiles is None:
|
|
return None
|
|
if self.want.profiles == '' and len(self.have.profiles) > 0:
|
|
have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles])
|
|
if len(self.have.profiles) == 1:
|
|
if not any(x[0] in ['tcp', 'udp', 'sctp'] for x in have):
|
|
return []
|
|
else:
|
|
return None
|
|
else:
|
|
return []
|
|
if self.want.profiles == '' and len(self.have.profiles) == 0:
|
|
return None
|
|
want = set([(p['name'], p['context'], p['fullPath']) for p in self.want.profiles])
|
|
have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles])
|
|
if len(have) == 0:
|
|
return self.want.profiles
|
|
elif len(have) == 1:
|
|
if want != have:
|
|
return self.want.profiles
|
|
else:
|
|
if not any(x[0] == 'tcp' for x in want):
|
|
have = set([x for x in have if x[0] != 'tcp'])
|
|
if not any(x[0] == 'udp' for x in want):
|
|
have = set([x for x in have if x[0] != 'udp'])
|
|
if not any(x[0] == 'sctp' for x in want):
|
|
have = set([x for x in have if x[0] != 'sctp'])
|
|
want = set([(p[2], p[1]) for p in want])
|
|
have = set([(p[2], p[1]) for p in have])
|
|
if want != have:
|
|
return self.want.profiles
|
|
|
|
@property
|
|
def fallback_persistence_profile(self):
|
|
if self.want.fallback_persistence_profile is None:
|
|
return None
|
|
if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is not None:
|
|
return ""
|
|
if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is None:
|
|
return None
|
|
if self.want.fallback_persistence_profile != self.have.fallback_persistence_profile:
|
|
return self.want.fallback_persistence_profile
|
|
|
|
@property
|
|
def default_persistence_profile(self):
|
|
if self.want.default_persistence_profile is None:
|
|
return None
|
|
if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is not None:
|
|
return []
|
|
if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is None:
|
|
return None
|
|
if self.have.default_persistence_profile is None:
|
|
return [self.want.default_persistence_profile]
|
|
w_name = self.want.default_persistence_profile.get('name', None)
|
|
w_partition = self.want.default_persistence_profile.get('partition', None)
|
|
h_name = self.have.default_persistence_profile.get('name', None)
|
|
h_partition = self.have.default_persistence_profile.get('partition', None)
|
|
if w_name != h_name or w_partition != h_partition:
|
|
return [self.want.default_persistence_profile]
|
|
|
|
@property
|
|
def policies(self):
|
|
if self.want.policies is None:
|
|
return None
|
|
if self.want.policies == '' and self.have.policies is None:
|
|
return None
|
|
if self.want.policies == '' and len(self.have.policies) > 0:
|
|
return []
|
|
if not self.have.policies:
|
|
return self.want.policies
|
|
want = set([(p['name'], p['partition']) for p in self.want.policies])
|
|
have = set([(p['name'], p['partition']) for p in self.have.policies])
|
|
if not want == have:
|
|
return self.want.policies
|
|
|
|
@property
|
|
def snat(self):
|
|
if self.want.snat is None:
|
|
return None
|
|
if self.want.snat['type'] != self.have.snat['type']:
|
|
result = dict(snat=self.want.snat)
|
|
return result
|
|
|
|
if self.want.snat.get('pool', None) is None:
|
|
return None
|
|
|
|
if self.want.snat['pool'] != self.have.snat['pool']:
|
|
result = dict(snat=self.want.snat)
|
|
return result
|
|
|
|
@property
|
|
def enabled(self):
|
|
if self.want.state == 'enabled' and self.have.disabled:
|
|
result = dict(
|
|
enabled=True,
|
|
disabled=False
|
|
)
|
|
return result
|
|
elif self.want.state == 'disabled' and self.have.enabled:
|
|
result = dict(
|
|
enabled=False,
|
|
disabled=True
|
|
)
|
|
return result
|
|
|
|
@property
|
|
def irules(self):
|
|
if self.want.irules is None:
|
|
return None
|
|
if self.want.irules == '' and len(self.have.irules) > 0:
|
|
return []
|
|
if self.want.irules == '' and len(self.have.irules) == 0:
|
|
return None
|
|
if sorted(set(self.want.irules)) != sorted(set(self.have.irules)):
|
|
return self.want.irules
|
|
|
|
@property
|
|
def pool(self):
|
|
if self.want.pool is None:
|
|
return None
|
|
if self.want.pool == '' and self.have.pool is not None:
|
|
return ""
|
|
if self.want.pool == '' and self.have.pool is None:
|
|
return None
|
|
if self.want.pool != self.have.pool:
|
|
return self.want.pool
|
|
|
|
@property
|
|
def metadata(self):
|
|
if self.want.metadata is None:
|
|
return None
|
|
elif len(self.want.metadata) == 0 and self.have.metadata is None:
|
|
return None
|
|
elif len(self.want.metadata) == 0:
|
|
return []
|
|
elif self.have.metadata is None:
|
|
return self.want.metadata
|
|
result = self._diff_complex_items(self.want.metadata, self.have.metadata)
|
|
return result
|
|
|
|
|
|
class ModuleManager(object):
|
|
def __init__(self, *args, **kwargs):
|
|
self.module = kwargs.get('module', None)
|
|
self.client = kwargs.get('client', None)
|
|
self.have = ApiParameters()
|
|
self.want = ModuleParameters(client=self.client, params=self.module.params)
|
|
self.changes = UsableChanges()
|
|
|
|
def exec_module(self):
|
|
changed = False
|
|
result = dict()
|
|
state = self.want.state
|
|
|
|
try:
|
|
if state in ['present', 'enabled', 'disabled']:
|
|
changed = self.present()
|
|
elif state == "absent":
|
|
changed = self.absent()
|
|
except iControlUnexpectedHTTPError as e:
|
|
raise F5ModuleError(str(e))
|
|
|
|
reportable = ReportableChanges(params=self.changes.to_return())
|
|
changes = reportable.to_return()
|
|
result.update(**changes)
|
|
result.update(dict(changed=changed))
|
|
self._announce_deprecations(result)
|
|
return result
|
|
|
|
def _announce_deprecations(self, result):
|
|
warnings = result.pop('__warnings', [])
|
|
for warning in warnings:
|
|
self.module.deprecate(
|
|
msg=warning['msg'],
|
|
version=warning['version']
|
|
)
|
|
|
|
def present(self):
|
|
if self.exists():
|
|
return self.update()
|
|
else:
|
|
return self.create()
|
|
|
|
def absent(self):
|
|
if self.exists():
|
|
return self.remove()
|
|
return False
|
|
|
|
def update(self):
|
|
self.have = self.read_current_from_device()
|
|
if not self.should_update():
|
|
return False
|
|
if self.module.check_mode:
|
|
return True
|
|
self.update_on_device()
|
|
return True
|
|
|
|
def should_update(self):
|
|
result = self._update_changed_options()
|
|
if result:
|
|
return True
|
|
return False
|
|
|
|
def remove(self):
|
|
if self.module.check_mode:
|
|
return True
|
|
self.remove_from_device()
|
|
if self.exists():
|
|
raise F5ModuleError("Failed to delete the resource")
|
|
return True
|
|
|
|
def get_reportable_changes(self):
|
|
result = ReportableChanges(params=self.changes.to_return())
|
|
return result
|
|
|
|
def _set_changed_options(self):
|
|
changed = {}
|
|
for key in Parameters.returnables:
|
|
if getattr(self.want, key) is not None:
|
|
changed[key] = getattr(self.want, key)
|
|
if changed:
|
|
self.changes = UsableChanges(params=changed)
|
|
|
|
def _update_changed_options(self):
|
|
diff = Difference(self.want, self.have)
|
|
updatables = Parameters.updatables
|
|
changed = dict()
|
|
for k in updatables:
|
|
change = diff.compare(k)
|
|
if change is None:
|
|
continue
|
|
else:
|
|
if isinstance(change, dict):
|
|
changed.update(change)
|
|
else:
|
|
changed[k] = change
|
|
if changed:
|
|
self.changes = UsableChanges(params=changed)
|
|
return True
|
|
return False
|
|
|
|
def exists(self):
|
|
result = self.client.api.tm.ltm.virtuals.virtual.exists(
|
|
name=self.want.name,
|
|
partition=self.want.partition
|
|
)
|
|
return result
|
|
|
|
def create(self):
|
|
required_resources = ['destination', 'port']
|
|
|
|
self._set_changed_options()
|
|
# This must be changed back to a list to make a valid REST API
|
|
# value. The module manipulates this as a normal dictionary
|
|
if self.want.default_persistence_profile is not None:
|
|
self.want.update({'default_persistence_profile': [self.want.default_persistence_profile]})
|
|
|
|
if self.want.destination is None:
|
|
raise F5ModuleError(
|
|
"'destination' must be specified when creating a virtual server"
|
|
)
|
|
if all(getattr(self.want, v) is None for v in required_resources):
|
|
raise F5ModuleError(
|
|
"You must specify both of " + ', '.join(required_resources)
|
|
)
|
|
if self.want.enabled_vlans is not None:
|
|
if any(x for x in self.want.enabled_vlans if x.lower() in ['/common/all', 'all']):
|
|
self.want.update(
|
|
dict(
|
|
enabled_vlans=[],
|
|
vlans_disabled=True,
|
|
vlans_enabled=False
|
|
)
|
|
)
|
|
if self.want.source and self.want.destination:
|
|
want = netaddr.IPNetwork(self.want.source)
|
|
have = netaddr.IPNetwork(self.want.destination_tuple.ip)
|
|
if want.version != have.version:
|
|
raise F5ModuleError(
|
|
"The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)."
|
|
)
|
|
if self.module.check_mode:
|
|
return True
|
|
self.create_on_device()
|
|
return True
|
|
|
|
def update_on_device(self):
|
|
params = self.changes.api_params()
|
|
resource = self.client.api.tm.ltm.virtuals.virtual.load(
|
|
name=self.want.name,
|
|
partition=self.want.partition
|
|
)
|
|
resource.modify(**params)
|
|
|
|
def read_current_from_device(self):
|
|
result = self.client.api.tm.ltm.virtuals.virtual.load(
|
|
name=self.want.name,
|
|
partition=self.want.partition,
|
|
requests_params=dict(
|
|
params=dict(
|
|
expandSubcollections='true'
|
|
)
|
|
)
|
|
)
|
|
params = result.attrs
|
|
params.update(dict(kind=result.to_dict().get('kind', None)))
|
|
result = ApiParameters(params=params)
|
|
return result
|
|
|
|
def create_on_device(self):
|
|
params = self.want.api_params()
|
|
self.client.api.tm.ltm.virtuals.virtual.create(
|
|
name=self.want.name,
|
|
partition=self.want.partition,
|
|
**params
|
|
)
|
|
|
|
def remove_from_device(self):
|
|
resource = self.client.api.tm.ltm.virtuals.virtual.load(
|
|
name=self.want.name,
|
|
partition=self.want.partition
|
|
)
|
|
if resource:
|
|
resource.delete()
|
|
|
|
|
|
class ArgumentSpec(object):
|
|
def __init__(self):
|
|
self.supports_check_mode = True
|
|
argument_spec = dict(
|
|
state=dict(
|
|
default='present',
|
|
choices=['present', 'absent', 'disabled', 'enabled']
|
|
),
|
|
name=dict(
|
|
required=True,
|
|
aliases=['vs']
|
|
),
|
|
destination=dict(
|
|
aliases=['address', 'ip']
|
|
),
|
|
port=dict(
|
|
type='int'
|
|
),
|
|
profiles=dict(
|
|
type='list',
|
|
aliases=['all_profiles'],
|
|
options=dict(
|
|
name=dict(required=False),
|
|
context=dict(default='all', choices=['all', 'server-side', 'client-side'])
|
|
)
|
|
),
|
|
policies=dict(
|
|
type='list',
|
|
aliases=['all_policies']
|
|
),
|
|
irules=dict(
|
|
type='list',
|
|
aliases=['all_rules']
|
|
),
|
|
enabled_vlans=dict(
|
|
type='list'
|
|
),
|
|
disabled_vlans=dict(
|
|
type='list'
|
|
),
|
|
pool=dict(),
|
|
description=dict(),
|
|
snat=dict(),
|
|
default_persistence_profile=dict(),
|
|
fallback_persistence_profile=dict(),
|
|
source=dict(),
|
|
metadata=dict(type='raw'),
|
|
partition=dict(
|
|
default='Common',
|
|
fallback=(env_fallback, ['F5_PARTITION'])
|
|
)
|
|
)
|
|
self.argument_spec = {}
|
|
self.argument_spec.update(f5_argument_spec)
|
|
self.argument_spec.update(argument_spec)
|
|
self.mutually_exclusive = [
|
|
['enabled_vlans', 'disabled_vlans']
|
|
]
|
|
|
|
|
|
def main():
|
|
spec = ArgumentSpec()
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=spec.argument_spec,
|
|
supports_check_mode=spec.supports_check_mode,
|
|
mutually_exclusive=spec.mutually_exclusive
|
|
)
|
|
if not HAS_F5SDK:
|
|
module.fail_json(msg="The python f5-sdk module is required")
|
|
if not HAS_NETADDR:
|
|
module.fail_json(msg="The python netaddr module is required")
|
|
|
|
try:
|
|
client = F5Client(**module.params)
|
|
mm = ModuleManager(module=module, client=client)
|
|
results = mm.exec_module()
|
|
cleanup_tokens(client)
|
|
module.exit_json(**results)
|
|
except F5ModuleError as ex:
|
|
cleanup_tokens(client)
|
|
module.fail_json(msg=str(ex))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|