mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-07-06 09:00:58 +02:00
1085 lines
36 KiB
Python
1085 lines
36 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': ['stableinterface'],
|
|
'supported_by': 'certified'}
|
|
|
|
DOCUMENTATION = r'''
|
|
---
|
|
module: bigip_gtm_virtual_server
|
|
short_description: Manages F5 BIG-IP GTM virtual servers
|
|
description:
|
|
- Manages F5 BIG-IP GTM virtual servers. A GTM server can have many virtual servers
|
|
associated with it. They are arranged in much the same way that pool members are
|
|
to pools.
|
|
version_added: 2.6
|
|
options:
|
|
name:
|
|
description:
|
|
- Specifies the name of the virtual server.
|
|
version_added: 2.6
|
|
server_name:
|
|
description:
|
|
- Specifies the name of the server that the virtual server is associated with.
|
|
version_added: 2.6
|
|
address:
|
|
description:
|
|
- Specifies the IP Address of the virtual server.
|
|
- When creating a new GTM virtual server, this parameter is required.
|
|
version_added: 2.6
|
|
port:
|
|
description:
|
|
- Specifies the service port number for the virtual server or pool member. For example,
|
|
the HTTP service is typically port 80.
|
|
- To specify all ports, use an C(*).
|
|
- When creating a new GTM virtual server, if this parameter is not specified, a
|
|
default of C(*) will be used.
|
|
translation_address:
|
|
description:
|
|
- Specifies the translation IP address for the virtual server.
|
|
- To unset this parameter, provide an empty string (C("")) as a value.
|
|
- When creating a new GTM virtual server, if this parameter is not specified, a
|
|
default of C(::) will be used.
|
|
version_added: 2.6
|
|
translation_port:
|
|
description:
|
|
- Specifies the translation port number or service name for the virtual server.
|
|
- To specify all ports, use an C(*).
|
|
- When creating a new GTM virtual server, if this parameter is not specified, a
|
|
default of C(*) will be used.
|
|
version_added: 2.6
|
|
availability_requirements:
|
|
description:
|
|
- Specifies, if you activate more than one health monitor, the number of health
|
|
monitors that must receive successful responses in order for the link to be
|
|
considered available.
|
|
suboptions:
|
|
type:
|
|
description:
|
|
- Monitor rule type when C(monitors) is specified.
|
|
- When creating a new virtual, if this value is not specified, the default of 'all' will be used.
|
|
choices: ['all', 'at_least', 'require']
|
|
at_least:
|
|
description:
|
|
- Specifies the minimum number of active health monitors that must be successful
|
|
before the link is considered up.
|
|
- This parameter is only relevant when a C(type) of C(at_least) is used.
|
|
- This parameter will be ignored if a type of either C(all) or C(require) is used.
|
|
number_of_probes:
|
|
description:
|
|
- Specifies the minimum number of probes that must succeed for this server to be declared up.
|
|
- When creating a new virtual server, if this parameter is specified, then the C(number_of_probers)
|
|
parameter must also be specified.
|
|
- The value of this parameter should always be B(lower) than, or B(equal to), the value of C(number_of_probers).
|
|
- This parameter is only relevant when a C(type) of C(require) is used.
|
|
- This parameter will be ignored if a type of either C(all) or C(at_least) is used.
|
|
number_of_probers:
|
|
description:
|
|
- Specifies the number of probers that should be used when running probes.
|
|
- When creating a new virtual server, if this parameter is specified, then the C(number_of_probes)
|
|
parameter must also be specified.
|
|
- The value of this parameter should always be B(higher) than, or B(equal to), the value of C(number_of_probers).
|
|
- This parameter is only relevant when a C(type) of C(require) is used.
|
|
- This parameter will be ignored if a type of either C(all) or C(at_least) is used.
|
|
version_added: 2.6
|
|
monitors:
|
|
description:
|
|
- Specifies the health monitors that the system currently uses to monitor this resource.
|
|
- When C(availability_requirements.type) is C(require), you may only have a single monitor in the
|
|
C(monitors) list.
|
|
version_added: 2.6
|
|
virtual_server_dependencies:
|
|
description:
|
|
- Specifies the virtual servers on which the current virtual server depends.
|
|
- If any of the specified servers are unavailable, the current virtual server is also listed as unavailable.
|
|
suboptions:
|
|
server:
|
|
description:
|
|
- Server which the dependant virtual server is part of.
|
|
required: True
|
|
virtual_server:
|
|
description:
|
|
- Virtual server to depend on.
|
|
required: True
|
|
version_added: 2.6
|
|
link:
|
|
description:
|
|
- Specifies a link to assign to the server or virtual server.
|
|
version_added: 2.6
|
|
limits:
|
|
description:
|
|
- Specifies resource thresholds or limit requirements at the server level.
|
|
- When you enable one or more limit settings, the system then uses that data to take servers in and out
|
|
of service.
|
|
- You can define limits for any or all of the limit settings. However, when a server does not meet the resource
|
|
threshold limit requirement, the system marks the entire server as unavailable and directs load-balancing
|
|
traffic to another resource.
|
|
- The limit settings available depend on the type of server.
|
|
suboptions:
|
|
bits_enabled:
|
|
description:
|
|
- Whether the bits limit is enabled or not.
|
|
- This parameter allows you to switch on or off the effect of the limit.
|
|
type: bool
|
|
packets_enabled:
|
|
description:
|
|
- Whether the packets limit is enabled or not.
|
|
- This parameter allows you to switch on or off the effect of the limit.
|
|
type: bool
|
|
connections_enabled:
|
|
description:
|
|
- Whether the current connections limit is enabled or not.
|
|
- This parameter allows you to switch on or off the effect of the limit.
|
|
type: bool
|
|
bits_limit:
|
|
description:
|
|
- Specifies the maximum allowable data throughput rate, in bits per second, for the virtual servers on the server.
|
|
- If the network traffic volume exceeds this limit, the system marks the server as unavailable.
|
|
packets_limit:
|
|
description:
|
|
- Specifies the maximum allowable data transfer rate, in packets per second, for the virtual servers on the server.
|
|
- If the network traffic volume exceeds this limit, the system marks the server as unavailable.
|
|
connections_limit:
|
|
description:
|
|
- Specifies the maximum number of concurrent connections, combined, for all of the virtual servers on the server.
|
|
- If the connections exceed this limit, the system marks the server as unavailable.
|
|
version_added: 2.6
|
|
partition:
|
|
description:
|
|
- Device partition to manage resources on.
|
|
default: Common
|
|
version_added: 2.6
|
|
state:
|
|
description:
|
|
- When C(present), ensures that the resource exists.
|
|
- When C(absent), ensures the resource is removed.
|
|
default: present
|
|
choices:
|
|
- present
|
|
- absent
|
|
- enabled
|
|
- disabled
|
|
extends_documentation_fragment: f5
|
|
author:
|
|
- Tim Rupp (@caphrim007)
|
|
'''
|
|
|
|
EXAMPLES = r'''
|
|
- name: Enable virtual server
|
|
bigip_gtm_virtual_server:
|
|
server: lb.mydomain.com
|
|
user: admin
|
|
password: secret
|
|
server_name: server1
|
|
name: my-virtual-server
|
|
state: enabled
|
|
delegate_to: localhost
|
|
'''
|
|
|
|
RETURN = r'''
|
|
server_name:
|
|
description: The server name associated with the virtual server.
|
|
returned: changed
|
|
type: string
|
|
sample: /Common/my-gtm-server
|
|
address:
|
|
description: The new address of the resource.
|
|
returned: changed
|
|
type: string
|
|
sample: 1.2.3.4
|
|
port:
|
|
description: The new port of the resource.
|
|
returned: changed
|
|
type: int
|
|
sample: 500
|
|
translation_address:
|
|
description: The new translation address of the resource.
|
|
returned: changed
|
|
type: int
|
|
sample: 500
|
|
translation_port:
|
|
description: The new translation port of the resource.
|
|
returned: changed
|
|
type: int
|
|
sample: 500
|
|
availability_requirements:
|
|
description: The new availability requirement configurations for the resource.
|
|
returned: changed
|
|
type: dict
|
|
sample: {'type': 'all'}
|
|
monitors:
|
|
description: The new list of monitors for the resource.
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/monitor1', '/Common/monitor2']
|
|
virtual_server_dependencies:
|
|
description: The new list of virtual server dependencies for the resource
|
|
returned: changed
|
|
type: list
|
|
sample: ['/Common/vs1', '/Common/vs2']
|
|
link:
|
|
description: The new link value for the resource.
|
|
returned: changed
|
|
type: string
|
|
sample: /Common/my-link
|
|
limits:
|
|
description: The new limit configurations for the resource.
|
|
returned: changed
|
|
type: dict
|
|
sample: { 'bits_enabled': true, 'bits_limit': 100 }
|
|
'''
|
|
|
|
import os
|
|
import re
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
from ansible.module_utils.basic import env_fallback
|
|
|
|
try:
|
|
from library.module_utils.compat.ipaddress import ip_address
|
|
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 fq_name
|
|
from library.module_utils.network.f5.common import compare_complex_list
|
|
from library.module_utils.network.f5.common import f5_argument_spec
|
|
from library.module_utils.network.f5.ipaddress import is_valid_ip
|
|
from library.module_utils.network.f5.ipaddress import validate_ip_v6_address
|
|
try:
|
|
from library.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
from f5.sdk_exception import LazyAttributesRequired
|
|
except ImportError:
|
|
HAS_F5SDK = False
|
|
except ImportError:
|
|
from ansible.module_utils.compat.ipaddress import ip_address
|
|
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 fq_name
|
|
from ansible.module_utils.network.f5.common import compare_complex_list
|
|
from ansible.module_utils.network.f5.common import f5_argument_spec
|
|
from ansible.module_utils.network.f5.ipaddress import is_valid_ip
|
|
from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address
|
|
try:
|
|
from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError
|
|
from f5.sdk_exception import LazyAttributesRequired
|
|
except ImportError:
|
|
HAS_F5SDK = False
|
|
|
|
|
|
class Parameters(AnsibleF5Parameters):
|
|
api_map = {
|
|
'limitMaxBps': 'bits_limit',
|
|
'limitMaxBpsStatus': 'bits_enabled',
|
|
'limitMaxConnections': 'connections_limit',
|
|
'limitMaxConnectionsStatus': 'connections_enabled',
|
|
'limitMaxPps': 'packets_limit',
|
|
'limitMaxPpsStatus': 'packets_enabled',
|
|
'translationAddress': 'translation_address',
|
|
'translationPort': 'translation_port',
|
|
'dependsOn': 'virtual_server_dependencies',
|
|
'explicitLinkName': 'link',
|
|
'monitor': 'monitors'
|
|
}
|
|
|
|
api_attributes = [
|
|
'dependsOn',
|
|
'destination',
|
|
'disabled',
|
|
'enabled',
|
|
'explicitLinkName',
|
|
'limitMaxBps',
|
|
'limitMaxBpsStatus',
|
|
'limitMaxConnections',
|
|
'limitMaxConnectionsStatus',
|
|
'limitMaxPps',
|
|
'limitMaxPpsStatus',
|
|
'translationAddress',
|
|
'translationPort',
|
|
# The monitor attribute is not included here, because it can break the
|
|
# API calls to the device. If this bug is ever fixed, uncomment this code.
|
|
#
|
|
# monitor
|
|
]
|
|
|
|
returnables = [
|
|
'bits_enabled',
|
|
'bits_limit',
|
|
'connections_enabled',
|
|
'connections_limit',
|
|
'destination',
|
|
'disabled',
|
|
'enabled',
|
|
'link',
|
|
'monitors',
|
|
'packets_enabled',
|
|
'packets_limit',
|
|
'translation_address',
|
|
'translation_port',
|
|
'virtual_server_dependencies',
|
|
]
|
|
|
|
updatables = [
|
|
'bits_enabled',
|
|
'bits_limit',
|
|
'connections_enabled',
|
|
'connections_limit',
|
|
'destination',
|
|
'enabled',
|
|
'link',
|
|
'monitors',
|
|
'packets_limit',
|
|
'packets_enabled',
|
|
'translation_address',
|
|
'translation_port',
|
|
'virtual_server_dependencies',
|
|
]
|
|
|
|
|
|
class ApiParameters(Parameters):
|
|
@property
|
|
def address(self):
|
|
if self._values['destination'].count(':') >= 2:
|
|
# IPv6
|
|
parts = self._values['destination'].split('.')
|
|
else:
|
|
# IPv4
|
|
parts = self._values['destination'].split(':')
|
|
if is_valid_ip(parts[0]):
|
|
return str(parts[0])
|
|
raise F5ModuleError(
|
|
"'address' parameter from API was not an IP address."
|
|
)
|
|
|
|
@property
|
|
def port(self):
|
|
if self._values['destination'].count(':') >= 2:
|
|
# IPv6
|
|
parts = self._values['destination'].split('.')
|
|
return parts[1]
|
|
# IPv4
|
|
parts = self._values['destination'].split(':')
|
|
return int(parts[1])
|
|
|
|
@property
|
|
def virtual_server_dependencies(self):
|
|
if self._values['virtual_server_dependencies'] is None:
|
|
return None
|
|
results = []
|
|
for dependency in self._values['virtual_server_dependencies']:
|
|
parts = dependency['name'].split(':')
|
|
result = dict(
|
|
server=parts[0],
|
|
virtual_server=parts[1],
|
|
)
|
|
results.append(result)
|
|
if results:
|
|
results = sorted(results, key=lambda k: k['server'])
|
|
return results
|
|
|
|
@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 availability_requirement_type(self):
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
if 'min ' in self._values['monitors']:
|
|
return 'at_least'
|
|
elif 'require ' in self._values['monitors']:
|
|
return 'require'
|
|
else:
|
|
return 'all'
|
|
|
|
@property
|
|
def monitors_list(self):
|
|
if self._values['monitors'] is None:
|
|
return []
|
|
try:
|
|
result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
|
|
result.sort()
|
|
return result
|
|
except Exception:
|
|
return self._values['monitors']
|
|
|
|
@property
|
|
def monitors(self):
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
monitors = [fq_name(self.partition, x) for x in self.monitors_list]
|
|
if self.availability_requirement_type == 'at_least':
|
|
monitors = ' '.join(monitors)
|
|
result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
|
|
elif self.availability_requirement_type == 'require':
|
|
monitors = ' '.join(monitors)
|
|
result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors)
|
|
else:
|
|
result = ' and '.join(monitors).strip()
|
|
|
|
return result
|
|
|
|
@property
|
|
def number_of_probes(self):
|
|
"""Returns the probes value from the monitor string.
|
|
|
|
The monitor string for a Require monitor looks like this.
|
|
|
|
require 1 from 2 { /Common/tcp }
|
|
|
|
This method parses out the first of the numeric values. This values represents
|
|
the "probes" value that can be updated in the module.
|
|
|
|
Returns:
|
|
int: The probes value if found. None otherwise.
|
|
"""
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
pattern = r'require\s+(?P<probes>\d+)\s+from'
|
|
matches = re.search(pattern, self._values['monitors'])
|
|
if matches is None:
|
|
return None
|
|
return matches.group('probes')
|
|
|
|
@property
|
|
def number_of_probers(self):
|
|
"""Returns the probers value from the monitor string.
|
|
|
|
The monitor string for a Require monitor looks like this.
|
|
|
|
require 1 from 2 { /Common/tcp }
|
|
|
|
This method parses out the first of the numeric values. This values represents
|
|
the "probers" value that can be updated in the module.
|
|
|
|
Returns:
|
|
int: The probers value if found. None otherwise.
|
|
"""
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
pattern = r'require\s+\d+\s+from\s+(?P<probers>\d+)\s+'
|
|
matches = re.search(pattern, self._values['monitors'])
|
|
if matches is None:
|
|
return None
|
|
return matches.group('probers')
|
|
|
|
@property
|
|
def at_least(self):
|
|
"""Returns the 'at least' value from the monitor string.
|
|
|
|
The monitor string for a Require monitor looks like this.
|
|
|
|
min 1 of { /Common/gateway_icmp }
|
|
|
|
This method parses out the first of the numeric values. This values represents
|
|
the "at_least" value that can be updated in the module.
|
|
|
|
Returns:
|
|
int: The at_least value if found. None otherwise.
|
|
"""
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
pattern = r'min\s+(?P<least>\d+)\s+of\s+'
|
|
matches = re.search(pattern, self._values['monitors'])
|
|
if matches is None:
|
|
return None
|
|
return matches.group('least')
|
|
|
|
|
|
class ModuleParameters(Parameters):
|
|
def _get_limit_value(self, type):
|
|
if self._values['limits'] is None:
|
|
return None
|
|
if self._values['limits'][type] is None:
|
|
return None
|
|
return int(self._values['limits'][type])
|
|
|
|
def _get_availability_value(self, type):
|
|
if self._values['availability_requirements'] is None:
|
|
return None
|
|
if self._values['availability_requirements'][type] is None:
|
|
return None
|
|
return int(self._values['availability_requirements'][type])
|
|
|
|
def _get_limit_status(self, type):
|
|
if self._values['limits'] is None:
|
|
return None
|
|
if self._values['limits'][type] is None:
|
|
return None
|
|
if self._values['limits'][type]:
|
|
return 'enabled'
|
|
return 'disabled'
|
|
|
|
@property
|
|
def address(self):
|
|
if self._values['address'] is None:
|
|
return None
|
|
if is_valid_ip(self._values['address']):
|
|
ip = str(ip_address(u'{0}'.format(self._values['address'])))
|
|
return ip
|
|
raise F5ModuleError(
|
|
"Specified 'address' is not an IP address."
|
|
)
|
|
|
|
@property
|
|
def port(self):
|
|
if self._values['port'] is None:
|
|
return None
|
|
if self._values['port'] == '*':
|
|
return 0
|
|
return int(self._values['port'])
|
|
|
|
@property
|
|
def destination(self):
|
|
if self.address is None:
|
|
return None
|
|
if self.port is None:
|
|
return None
|
|
if validate_ip_v6_address(self.address):
|
|
result = '{0}.{1}'.format(self.address, self.port)
|
|
else:
|
|
result = '{0}:{1}'.format(self.address, self.port)
|
|
return result
|
|
|
|
@property
|
|
def link(self):
|
|
if self._values['link'] is None:
|
|
return None
|
|
return fq_name(self.partition, self._values['link'])
|
|
|
|
@property
|
|
def bits_limit(self):
|
|
return self._get_limit_value('bits_limit')
|
|
|
|
@property
|
|
def packets_limit(self):
|
|
return self._get_limit_value('packets_limit')
|
|
|
|
@property
|
|
def connections_limit(self):
|
|
return self._get_limit_value('connections_limit')
|
|
|
|
@property
|
|
def bits_enabled(self):
|
|
return self._get_limit_status('bits_enabled')
|
|
|
|
@property
|
|
def packets_enabled(self):
|
|
return self._get_limit_status('packets_enabled')
|
|
|
|
@property
|
|
def connections_enabled(self):
|
|
return self._get_limit_status('connections_enabled')
|
|
|
|
@property
|
|
def translation_address(self):
|
|
if self._values['translation_address'] is None:
|
|
return None
|
|
if self._values['translation_address'] == '':
|
|
return 'none'
|
|
return self._values['translation_address']
|
|
|
|
@property
|
|
def translation_port(self):
|
|
if self._values['translation_port'] is None:
|
|
return None
|
|
if self._values['translation_port'] in ['*', ""]:
|
|
return 0
|
|
return int(self._values['translation_port'])
|
|
|
|
@property
|
|
def virtual_server_dependencies(self):
|
|
if self._values['virtual_server_dependencies'] is None:
|
|
return None
|
|
results = []
|
|
for dependency in self._values['virtual_server_dependencies']:
|
|
result = dict(
|
|
server=fq_name(self.partition, dependency['server']),
|
|
virtual_server=os.path.basename(dependency['virtual_server'])
|
|
)
|
|
results.append(result)
|
|
if results:
|
|
results = sorted(results, key=lambda k: k['server'])
|
|
return results
|
|
|
|
@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 monitors_list(self):
|
|
if self._values['monitors'] is None:
|
|
return []
|
|
try:
|
|
result = re.findall(r'/\w+/[^\s}]+', self._values['monitors'])
|
|
result.sort()
|
|
return result
|
|
except Exception:
|
|
return self._values['monitors']
|
|
|
|
@property
|
|
def monitors(self):
|
|
if self._values['monitors'] is None:
|
|
return None
|
|
monitors = [fq_name(self.partition, x) for x in self.monitors_list]
|
|
if self.availability_requirement_type == 'at_least':
|
|
if self.at_least > len(self.monitors_list):
|
|
raise F5ModuleError(
|
|
"The 'at_least' value must not exceed the number of 'monitors'."
|
|
)
|
|
monitors = ' '.join(monitors)
|
|
result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors)
|
|
elif self.availability_requirement_type == 'require':
|
|
monitors = ' '.join(monitors)
|
|
if self.number_of_probes > self.number_of_probers:
|
|
raise F5ModuleError(
|
|
"The 'number_of_probes' must not exceed the 'number_of_probers'."
|
|
)
|
|
result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors)
|
|
else:
|
|
result = ' and '.join(monitors).strip()
|
|
|
|
return result
|
|
|
|
@property
|
|
def availability_requirement_type(self):
|
|
if self._values['availability_requirements'] is None:
|
|
return None
|
|
return self._values['availability_requirements']['type']
|
|
|
|
@property
|
|
def number_of_probes(self):
|
|
return self._get_availability_value('number_of_probes')
|
|
|
|
@property
|
|
def number_of_probers(self):
|
|
return self._get_availability_value('number_of_probers')
|
|
|
|
@property
|
|
def at_least(self):
|
|
return self._get_availability_value('at_least')
|
|
|
|
|
|
class Changes(Parameters):
|
|
def to_return(self):
|
|
result = {}
|
|
try:
|
|
for returnable in self.returnables:
|
|
result[returnable] = getattr(self, returnable)
|
|
result = self._filter_params(result)
|
|
except Exception:
|
|
pass
|
|
return result
|
|
|
|
|
|
class UsableChanges(Changes):
|
|
@property
|
|
def virtual_server_dependencies(self):
|
|
if self._values['virtual_server_dependencies'] is None:
|
|
return None
|
|
results = []
|
|
for depend in self._values['virtual_server_dependencies']:
|
|
name = '{0}:{1}'.format(depend['server'], depend['virtual_server'])
|
|
results.append(dict(name=name))
|
|
return results
|
|
|
|
|
|
class ReportableChanges(Changes):
|
|
pass
|
|
|
|
|
|
class Difference(object):
|
|
def __init__(self, want, have=None):
|
|
self.want = want
|
|
self.have = have
|
|
|
|
def compare(self, param):
|
|
try:
|
|
result = getattr(self, param)
|
|
return result
|
|
except AttributeError:
|
|
return self.__default(param)
|
|
|
|
def __default(self, param):
|
|
attr1 = getattr(self.want, param)
|
|
try:
|
|
attr2 = getattr(self.have, param)
|
|
if attr1 != attr2:
|
|
return attr1
|
|
except AttributeError:
|
|
return attr1
|
|
|
|
@property
|
|
def destination(self):
|
|
if self.want.port is None:
|
|
self.want.update({'port': self.have.port})
|
|
if self.want.address is None:
|
|
self.want.update({'address': self.have.address})
|
|
if self.want.destination != self.have.destination:
|
|
return self.want.destination
|
|
|
|
@property
|
|
def virtual_server_dependencies(self):
|
|
if self.have.virtual_server_dependencies is None:
|
|
return self.want.virtual_server_dependencies
|
|
if self.want.virtual_server_dependencies is None and self.have.virtual_server_dependencies is None:
|
|
return None
|
|
if self.want.virtual_server_dependencies is None:
|
|
return None
|
|
result = compare_complex_list(self.want.virtual_server_dependencies, self.have.virtual_server_dependencies)
|
|
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 monitors(self):
|
|
if self.have.monitors is None:
|
|
return self.want.monitors
|
|
if self.have.monitors != self.want.monitors:
|
|
return self.want.monitors
|
|
|
|
|
|
class ModuleManager(object):
|
|
def __init__(self, *args, **kwargs):
|
|
self.module = kwargs.get('module', None)
|
|
self.client = kwargs.get('client', None)
|
|
self.want = ModuleParameters(params=self.module.params)
|
|
self.have = ApiParameters()
|
|
self.changes = UsableChanges()
|
|
|
|
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 should_update(self):
|
|
result = self._update_changed_options()
|
|
if result:
|
|
return True
|
|
return False
|
|
|
|
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.client.module.deprecate(
|
|
msg=warning['msg'],
|
|
version=warning['version']
|
|
)
|
|
|
|
def present(self):
|
|
if self.exists():
|
|
return self.update()
|
|
else:
|
|
return self.create()
|
|
|
|
def exists(self):
|
|
resource = self.client.api.tm.gtm.servers.server.load(
|
|
name=self.want.server_name,
|
|
partition=self.want.partition
|
|
)
|
|
result = resource.virtual_servers_s.virtual_server.exists(
|
|
name=self.want.name
|
|
)
|
|
return result
|
|
|
|
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 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 create(self):
|
|
if self.want.port in [None, ""]:
|
|
self.want.update({'port': '*'})
|
|
if self.want.translation_port in [None, ""]:
|
|
self.want.update({'translation_port': '*'})
|
|
if self.want.translation_address in [None, ""]:
|
|
self.want.update({'translation_address': '::'})
|
|
|
|
self._set_changed_options()
|
|
|
|
if self.want.address is None:
|
|
raise F5ModuleError(
|
|
"You must supply an 'address' when creating a new virtual server."
|
|
)
|
|
if self.want.availability_requirement_type == 'require' and len(self.want.monitors_list) > 1:
|
|
raise F5ModuleError(
|
|
"Only one monitor may be specified when using an availability_requirement type of 'require'"
|
|
)
|
|
if self.module.check_mode:
|
|
return True
|
|
self.create_on_device()
|
|
return True
|
|
|
|
def create_on_device(self):
|
|
params = self.changes.api_params()
|
|
resource = self.client.api.tm.gtm.servers.server.load(
|
|
name=self.want.server_name,
|
|
partition=self.want.partition
|
|
)
|
|
resource.virtual_servers_s.virtual_server.create(
|
|
name=self.want.name,
|
|
**params
|
|
)
|
|
if self.want.monitors:
|
|
self.update_monitors_on_device()
|
|
|
|
def update_on_device(self):
|
|
params = self.changes.api_params()
|
|
resource = self.client.api.tm.gtm.servers.server.load(
|
|
name=self.want.server_name,
|
|
partition=self.want.partition
|
|
)
|
|
resource = resource.virtual_servers_s.virtual_server.load(
|
|
name=self.want.name
|
|
)
|
|
resource.modify(**params)
|
|
if self.want.monitors:
|
|
self.update_monitors_on_device()
|
|
|
|
def absent(self):
|
|
if self.exists():
|
|
return self.remove()
|
|
return False
|
|
|
|
def remove_from_device(self):
|
|
resource = self.client.api.tm.gtm.servers.server.load(
|
|
name=self.want.server_name,
|
|
partition=self.want.partition
|
|
)
|
|
resource = resource.virtual_servers_s.virtual_server.load(
|
|
name=self.want.name
|
|
)
|
|
if resource:
|
|
resource.delete()
|
|
|
|
def read_current_from_device(self):
|
|
resource = self.client.api.tm.gtm.servers.server.load(
|
|
name=self.want.server_name,
|
|
partition=self.want.partition
|
|
)
|
|
resource = resource.virtual_servers_s.virtual_server.load(
|
|
name=self.want.name
|
|
)
|
|
result = resource.attrs
|
|
return ApiParameters(params=result)
|
|
|
|
def update_monitors_on_device(self):
|
|
"""Updates the monitors string on a virtual server
|
|
|
|
There is a long-standing bug in GTM virtual servers where the monitor value
|
|
is a string that includes braces. These braces cause the REST API to panic and
|
|
fail to update or create any resources that have an "at_least" or "require"
|
|
set of availability_requirements.
|
|
|
|
This method exists to do a tmsh command to cause the update to take place on
|
|
the device.
|
|
|
|
Preferably, this method can be removed and the bug be fixed. The API should
|
|
be working, obviously, but the more concerning issue is if tmsh commands change
|
|
over time, breaking this method.
|
|
"""
|
|
command = 'tmsh modify gtm server /{0}/{1} virtual-servers modify {{ {2} {{ monitor {3} }} }}'.format(
|
|
self.want.partition, self.want.server_name, self.want.name, self.want.monitors
|
|
)
|
|
output = self.client.api.tm.util.bash.exec_cmd(
|
|
'run',
|
|
utilCmdArgs='-c "{0}"'.format(command)
|
|
)
|
|
try:
|
|
if hasattr(output, 'commandResult'):
|
|
if len(output.commandResult.strip()) > 0:
|
|
raise F5ModuleError(output.commandResult)
|
|
except (AttributeError, NameError, LazyAttributesRequired):
|
|
pass
|
|
return True
|
|
|
|
|
|
class ArgumentSpec(object):
|
|
def __init__(self):
|
|
self.supports_check_mode = True
|
|
argument_spec = dict(
|
|
name=dict(required=True),
|
|
server_name=dict(required=True),
|
|
address=dict(),
|
|
port=dict(type='int'),
|
|
translation_address=dict(),
|
|
translation_port=dict(),
|
|
availability_requirements=dict(
|
|
type='dict',
|
|
options=dict(
|
|
type=dict(
|
|
choices=['all', 'at_least', 'require'],
|
|
required=True
|
|
),
|
|
at_least=dict(type='int'),
|
|
number_of_probes=dict(type='int'),
|
|
number_of_probers=dict(type='int')
|
|
),
|
|
mutually_exclusive=[
|
|
['at_least', 'number_of_probes'],
|
|
['at_least', 'number_of_probers'],
|
|
],
|
|
required_if=[
|
|
['type', 'at_least', ['at_least']],
|
|
['type', 'require', ['number_of_probes', 'number_of_probers']]
|
|
]
|
|
),
|
|
monitors=dict(type='list'),
|
|
virtual_server_dependencies=dict(
|
|
type='list',
|
|
options=dict(
|
|
server=dict(required=True),
|
|
virtual_server=dict(required=True)
|
|
)
|
|
),
|
|
link=dict(),
|
|
limits=dict(
|
|
type='dict',
|
|
options=dict(
|
|
bits_enabled=dict(type='bool'),
|
|
packets_enabled=dict(type='bool'),
|
|
connections_enabled=dict(type='bool'),
|
|
bits_limit=dict(type='int'),
|
|
packets_limit=dict(type='int'),
|
|
connections_limit=dict(type='int')
|
|
)
|
|
),
|
|
state=dict(
|
|
default='present',
|
|
choices=['present', 'absent', 'disabled', 'enabled']
|
|
),
|
|
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)
|
|
|
|
|
|
def main():
|
|
spec = ArgumentSpec()
|
|
|
|
module = AnsibleModule(
|
|
argument_spec=spec.argument_spec,
|
|
supports_check_mode=spec.supports_check_mode,
|
|
)
|
|
if not HAS_F5SDK:
|
|
module.fail_json(msg="The python f5-sdk 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()
|