#!/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_server short_description: Manages F5 BIG-IP GTM servers description: - Manage BIG-IP server configuration. This module is able to manipulate the server definitions in a BIG-IP. version_added: 2.5 options: name: description: - The name of the server. required: True state: description: - The server state. If C(absent), an attempt to delete the server will be made. This will only succeed if this server is not in use by a virtual server. C(present) creates the server and enables it. If C(enabled), enable the server if it exists. If C(disabled), create the server if needed, and set state to C(disabled). default: present choices: - present - absent - enabled - disabled datacenter: description: - Data center the server belongs to. When creating a new GTM server, this value is required. devices: description: - Lists the self IP addresses and translations for each device. When creating a new GTM server, this value is required. This list is a complex list that specifies a number of keys. - The C(name) key specifies a name for the device. The device name must be unique per server. This key is required. - The C(address) key contains an IP address, or list of IP addresses, for the destination server. This key is required. - The C(translation) key contains an IP address to translate the C(address) value above to. This key is optional. - Specifying duplicate C(name) fields is a supported means of providing device addresses. In this scenario, the addresses will be assigned to the C(name)'s list of addresses. server_type: description: - Specifies the server type. The server type determines the metrics that the system can collect from the server. When creating a new GTM server, the default value C(bigip) is used. choices: - alteon-ace-director - cisco-css - cisco-server-load-balancer - generic-host - radware-wsd - windows-nt-4.0 - bigip - cisco-local-director-v2 - extreme - generic-load-balancer - sun-solaris - cacheflow - cisco-local-director-v3 - foundry-server-iron - netapp - windows-2000-server aliases: - product link_discovery: description: - Specifies whether the system auto-discovers the links for this server. When creating a new GTM server, if this parameter is not specified, the default value C(disabled) is used. - If you set this parameter to C(enabled) or C(enabled-no-delete), you must also ensure that the C(virtual_server_discovery) parameter is also set to C(enabled) or C(enabled-no-delete). choices: - enabled - disabled - enabled-no-delete virtual_server_discovery: description: - Specifies whether the system auto-discovers the virtual servers for this server. When creating a new GTM server, if this parameter is not specified, the default value C(disabled) is used. choices: - enabled - disabled - enabled-no-delete partition: description: - Device partition to manage resources on. default: Common version_added: 2.5 iquery_options: description: - Specifies whether the Global Traffic Manager uses this BIG-IP system to conduct a variety of probes before delegating traffic to it. suboptions: allow_path: description: - Specifies that the system verifies the logical network route between a data center server and a local DNS server. type: bool allow_service_check: description: - Specifies that the system verifies that an application on a server is running, by remotely running the application using an external service checker program. type: bool allow_snmp: description: - Specifies that the system checks the performance of a server running an SNMP agent. type: bool version_added: 2.7 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.8 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 pool, 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.8 prober_preference: description: - Specifies the type of prober to use to monitor this server's resources. - This option is ignored in C(TMOS) version C(12.x). - From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool) a C(prober_pool) parameter must be specified. choices: - inside-datacenter - outside-datacenter - inherit - pool version_added: 2.8 prober_fallback: description: - Specifies the type of prober to use to monitor this server's resources when the preferred prober is not available. - This option is ignored in C(TMOS) version C(12.x). - From C(TMOS) version C(13.x) and up, when prober_preference is set to C(pool) a C(prober_pool) parameter must be specified. - The choices are mutually exclusive with prober_preference parameter, with the exception of C(any-available) or C(none) option. choices: - any - inside-datacenter - outside-datacenter - inherit - pool - none version_added: 2.8 prober_pool: description: - Specifies the name of the prober pool to use to monitor this server's resources. - From C(TMOS) version C(13.x) and up, this parameter is mandatory when C(prober_preference) is set to C(pool). - Format of the name can be either be prepended by partition (C(/Common/foo)), or specified just as an object name (C(foo)). - In C(TMOS) version C(12.x) prober_pool can be set to empty string to revert to default setting of inherit. version_added: 2.8 limits: description: - Specifies resource thresholds or limit requirements at the pool member level. - When you enable one or more limit settings, the system then uses that data to take members in and out of service. - You can define limits for any or all of the limit settings. However, when a member does not meet the resource threshold limit requirement, the system marks the member as unavailable and directs load-balancing traffic to another resource. version_added: 2.8 suboptions: bits_enabled: description: - Whether the bits limit it 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 it 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 it enabled or not. - This parameter allows you to switch on or off the effect of the limit. type: bool cpu_enabled: description: - Whether the CPU limit it enabled or not. - This parameter allows you to switch on or off the effect of the limit. type: bool memory_enabled: description: - Whether the memory limit it 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 member. - If the network traffic volume exceeds this limit, the system marks the member as unavailable. packets_limit: description: - Specifies the maximum allowable data transfer rate, in packets per second, for the member. - If the network traffic volume exceeds this limit, the system marks the member as unavailable. connections_limit: description: - Specifies the maximum number of concurrent connections, combined, for all of the member. - If the connections exceed this limit, the system marks the server as unavailable. cpu_limit: description: - Specifies the percent of CPU usage. - If percent of CPU usage goes above the limit, the system marks the server as unavailable. memory_limit: description: - Specifies the available memory required by the virtual servers on the server. - If available memory falls below this limit, the system marks the server as unavailable. extends_documentation_fragment: f5 author: - Robert Teller (@r-teller) - Tim Rupp (@caphrim007) - Wojciech Wypior (@wojtek0806) ''' EXAMPLES = r''' - name: Create server "GTM_Server" bigip_gtm_server: name: GTM_Server datacenter: /Common/New York server_type: bigip link_discovery: disabled virtual_server_discovery: disabled devices: - name: server_1 address: 1.1.1.1 - name: server_2 address: 2.2.2.1 translation: 192.168.2.1 - name: server_2 address: 2.2.2.2 - name: server_3 addresses: - address: 3.3.3.1 - address: 3.3.3.2 - name: server_4 addresses: - address: 4.4.4.1 translation: 192.168.14.1 - address: 4.4.4.2 provider: user: admin password: secret server: lb.mydomain.com delegate_to: localhost - name: Create server "GTM_Server" with expanded keys bigip_gtm_server: server: lb.mydomain.com user: admin password: secret name: GTM_Server datacenter: /Common/New York server_type: bigip link_discovery: disabled virtual_server_discovery: disabled devices: - name: server_1 address: 1.1.1.1 - name: server_2 address: 2.2.2.1 translation: 192.168.2.1 - name: server_2 address: 2.2.2.2 - name: server_3 addresses: - address: 3.3.3.1 - address: 3.3.3.2 - name: server_4 addresses: - address: 4.4.4.1 translation: 192.168.14.1 - address: 4.4.4.2 provider: user: admin password: secret server: lb.mydomain.com delegate_to: localhost ''' RETURN = r''' bits_enabled: description: Whether the bits limit is enabled. returned: changed type: bool sample: yes bits_limit: description: The new bits_enabled limit. returned: changed type: int sample: 100 connections_enabled: description: Whether the connections limit is enabled. returned: changed type: bool sample: yes connections_limit: description: The new connections_limit limit. returned: changed type: int sample: 100 monitors: description: The new list of monitors for the resource. returned: changed type: list sample: ['/Common/monitor1', '/Common/monitor2'] link_discovery: description: The new C(link_discovery) configured on the remote device. returned: changed type: str sample: enabled virtual_server_discovery: description: The new C(virtual_server_discovery) name for the trap destination. returned: changed type: str sample: disabled server_type: description: The new type of the server. returned: changed type: str sample: bigip datacenter: description: The new C(datacenter) which the server is part of. returned: changed type: str sample: datacenter01 packets_enabled: description: Whether the packets limit is enabled. returned: changed type: bool sample: yes packets_limit: description: The new packets_limit limit. returned: changed type: int sample: 100 ''' import re from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from distutils.version import LooseVersion try: from library.module_utils.network.f5.bigip import F5RestClient 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 f5_argument_spec from library.module_utils.network.f5.common import transform_name from library.module_utils.network.f5.common import exit_json from library.module_utils.network.f5.common import fail_json from library.module_utils.network.f5.common import is_empty_list from library.module_utils.network.f5.icontrol import tmos_version from library.module_utils.network.f5.icontrol import module_provisioned except ImportError: from ansible.module_utils.network.f5.bigip import F5RestClient 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 f5_argument_spec from ansible.module_utils.network.f5.common import transform_name from ansible.module_utils.network.f5.common import exit_json from ansible.module_utils.network.f5.common import fail_json from ansible.module_utils.network.f5.common import is_empty_list from ansible.module_utils.network.f5.icontrol import tmos_version from ansible.module_utils.network.f5.icontrol import module_provisioned try: from collections import OrderedDict except ImportError: try: from ordereddict import OrderedDict except ImportError: pass class Parameters(AnsibleF5Parameters): api_map = { 'product': 'server_type', 'virtualServerDiscovery': 'virtual_server_discovery', 'linkDiscovery': 'link_discovery', 'addresses': 'devices', 'iqAllowPath': 'iquery_allow_path', 'iqAllowServiceCheck': 'iquery_allow_service_check', 'iqAllowSnmp': 'iquery_allow_snmp', 'monitor': 'monitors', 'proberPreference': 'prober_preference', 'proberPool': 'prober_pool', 'proberFallback': 'prober_fallback', 'limitMaxBps': 'bits_limit', 'limitMaxBpsStatus': 'bits_enabled', 'limitMaxConnections': 'connections_limit', 'limitMaxConnectionsStatus': 'connections_enabled', 'limitMaxPps': 'packets_limit', 'limitMaxPpsStatus': 'packets_enabled', 'limitCpuUsage': 'cpu_limit', 'limitCpuUsageStatus': 'cpu_enabled', 'limitMemAvail': 'memory_limit', 'limitMemAvailStatus': 'memory_enabled', } api_attributes = [ 'linkDiscovery', 'virtualServerDiscovery', 'product', 'addresses', 'datacenter', 'enabled', 'disabled', 'iqAllowPath', 'iqAllowServiceCheck', 'iqAllowSnmp', 'monitor', 'proberPreference', 'proberPool', 'proberFallback', 'limitMaxBps', 'limitMaxBpsStatus', 'limitMaxConnections', 'limitMaxConnectionsStatus', 'limitMaxPps', 'limitMaxPpsStatus', 'limitCpuUsage', 'limitCpuUsageStatus', 'limitMemAvail', 'limitMemAvailStatus', ] updatables = [ 'link_discovery', 'virtual_server_discovery', 'server_type_and_devices', 'datacenter', 'state', 'iquery_allow_path', 'iquery_allow_service_check', 'iquery_allow_snmp', 'monitors', 'prober_preference', 'prober_pool', 'prober_fallback', 'bits_enabled', 'bits_limit', 'connections_enabled', 'connections_limit', 'packets_enabled', 'packets_limit', 'cpu_enabled', 'cpu_limit', 'memory_enabled', 'memory_limit', ] returnables = [ 'link_discovery', 'virtual_server_discovery', 'server_type', 'datacenter', 'enabled', 'iquery_allow_path', 'iquery_allow_service_check', 'iquery_allow_snmp', 'devices', 'monitors', 'availability_requirements', 'prober_preference', 'prober_pool', 'prober_fallback', 'bits_enabled', 'bits_limit', 'connections_enabled', 'connections_limit', 'packets_enabled', 'packets_limit', 'cpu_enabled', 'cpu_limit', 'memory_enabled', 'memory_limit', ] class ApiParameters(Parameters): @property def devices(self): if self._values['devices'] is None: return None return self._values['devices'] @property def server_type(self): if self._values['server_type'] is None: return None elif self._values['server_type'] in ['single-bigip', 'redundant-bigip']: return 'bigip' else: return self._values['server_type'] @property def raw_server_type(self): if self._values['server_type'] is None: return None return self._values['server_type'] @property def enabled(self): if self._values['enabled'] is None: return None return True @property def disabled(self): if self._values['disabled'] is None: return None return True @property def iquery_allow_path(self): if self._values['iquery_allow_path'] is None: return None elif self._values['iquery_allow_path'] == 'yes': return True return False @property def iquery_allow_service_check(self): if self._values['iquery_allow_service_check'] is None: return None elif self._values['iquery_allow_service_check'] == 'yes': return True return False @property def iquery_allow_snmp(self): if self._values['iquery_allow_snmp'] is None: return None elif self._values['iquery_allow_snmp'] == 'yes': 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 if self._values['monitors'] == '/Common/bigip': return '/Common/bigip' 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\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\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\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_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 devices(self): if self._values['devices'] is None: return None result = [] for device in self._values['devices']: if not any(x for x in ['address', 'addresses'] if x in device): raise F5ModuleError( "The specified device list must contain an 'address' or 'addresses' key" ) if 'address' in device: translation = self._determine_translation(device) name = device['address'] device_name = device['name'] result.append({ 'name': name, 'deviceName': device_name, 'translation': translation }) elif 'addresses' in device: for address in device['addresses']: translation = self._determine_translation(address) name = address['address'] device_name = device['name'] result.append({ 'name': name, 'deviceName': device_name, 'translation': translation }) return result @property def enabled(self): if self._values['state'] in ['present', 'enabled']: return True return False @property def datacenter(self): if self._values['datacenter'] is None: return None return fq_name(self.partition, self._values['datacenter']) def _determine_translation(self, device): if 'translation' not in device: return 'none' return device['translation'] @property def state(self): if self._values['state'] == 'enabled': return 'present' return self._values['state'] @property def iquery_allow_path(self): if self._values['iquery_options'] is None: return None elif self._values['iquery_options']['allow_path'] is None: return None return self._values['iquery_options']['allow_path'] @property def iquery_allow_service_check(self): if self._values['iquery_options'] is None: return None elif self._values['iquery_options']['allow_service_check'] is None: return None return self._values['iquery_options']['allow_service_check'] @property def iquery_allow_snmp(self): if self._values['iquery_options'] is None: return None elif self._values['iquery_options']['allow_snmp'] is None: return None return self._values['iquery_options']['allow_snmp'] @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 if is_empty_list(self._values['monitors']): return '/Common/bigip' 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 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]) @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') @property def prober_pool(self): if self._values['prober_pool'] is None: return None if self._values['prober_pool'] == '': return self._values['prober_pool'] result = fq_name(self.partition, self._values['prober_pool']) return result @property def prober_fallback(self): if self._values['prober_fallback'] == 'any': return 'any-available' return self._values['prober_fallback'] @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 cpu_limit(self): return self._get_limit_value('cpu_limit') @property def memory_limit(self): return self._get_limit_value('memory_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 cpu_enabled(self): return self._get_limit_status('cpu_enabled') @property def memory_enabled(self): return self._get_limit_status('memory_enabled') class Changes(Parameters): def to_return(self): result = {} for returnable in self.returnables: result[returnable] = getattr(self, returnable) result = self._filter_params(result) return result class UsableChanges(Changes): @property def monitors(self): monitor_string = self._values['monitors'] if monitor_string is None: return None if '{' in monitor_string and '}': tmp = monitor_string.strip('}').split('{') monitor = ''.join(tmp).rstrip() return monitor return monitor_string @property def iquery_allow_path(self): if self._values['iquery_allow_path'] is None: return None elif self._values['iquery_allow_path']: return 'yes' return 'no' @property def iquery_allow_service_check(self): if self._values['iquery_allow_service_check'] is None: return None elif self._values['iquery_allow_service_check']: return 'yes' return 'no' @property def iquery_allow_snmp(self): if self._values['iquery_allow_snmp'] is None: return None elif self._values['iquery_allow_snmp']: return 'yes' return 'no' class ReportableChanges(Changes): @property def server_type(self): if self._values['server_type'] in ['single-bigip', 'redundant-bigip']: return 'bigip' return self._values['server_type'] @property def monitors(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 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 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\d+)\s+from' matches = re.search(pattern, self._values['monitors']) if matches is None: return None return int(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\d+)\s+' matches = re.search(pattern, self._values['monitors']) if matches is None: return None return int(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\d+)\s+of\s+' matches = re.search(pattern, self._values['monitors']) if matches is None: return None return int(matches.group('least')) @property def availability_requirements(self): if self._values['monitors'] is None: return None result = dict() result['type'] = self.availability_requirement_type result['at_least'] = self.at_least result['number_of_probers'] = self.number_of_probers result['number_of_probes'] = self.number_of_probes return result @property def prober_fallback(self): if self._values['prober_fallback'] == 'any-available': return 'any' return self._values['prober_fallback'] 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): want = getattr(self.want, param) try: have = getattr(self.have, param) if want != have: return want except AttributeError: return want def _discovery_constraints(self): if self.want.virtual_server_discovery is None: virtual_server_discovery = self.have.virtual_server_discovery else: virtual_server_discovery = self.want.virtual_server_discovery if self.want.link_discovery is None: link_discovery = self.have.link_discovery else: link_discovery = self.want.link_discovery if link_discovery in ['enabled', 'enabled-no-delete'] and virtual_server_discovery == 'disabled': raise F5ModuleError( "Virtual server discovery must be enabled if link discovery is enabled" ) def _devices_changed(self): if self.want.devices is None and self.want.server_type is None: return None if self.want.devices is None: devices = self.have.devices else: devices = self.want.devices if self.have.devices is None: have_devices = [] else: have_devices = self.have.devices if len(devices) == 0: raise F5ModuleError( "A GTM server must have at least one device associated with it." ) want = [OrderedDict(sorted(d.items())) for d in devices] have = [OrderedDict(sorted(d.items())) for d in have_devices] if want != have: return True return False def _server_type_changed(self): if self.want.server_type is None: self.want.update({'server_type': self.have.server_type}) if self.want.server_type != self.have.server_type: return True return False @property def link_discovery(self): self._discovery_constraints() if self.want.link_discovery != self.have.link_discovery: return self.want.link_discovery @property def virtual_server_discovery(self): self._discovery_constraints() if self.want.virtual_server_discovery != self.have.virtual_server_discovery: return self.want.virtual_server_discovery def _handle_current_server_type_and_devices(self, devices_change, server_change): result = {} if devices_change: result['devices'] = self.want.devices if server_change: result['server_type'] = self.want.server_type return result def _handle_legacy_server_type_and_devices(self, devices_change, server_change): result = {} if server_change and devices_change: result['devices'] = self.want.devices if len(self.want.devices) > 1 and self.want.server_type == 'bigip': if self.have.raw_server_type != 'redundant-bigip': result['server_type'] = 'redundant-bigip' elif self.want.server_type == 'bigip': if self.have.raw_server_type != 'single-bigip': result['server_type'] = 'single-bigip' else: result['server_type'] = self.want.server_type elif devices_change: result['devices'] = self.want.devices if len(self.want.devices) > 1 and self.have.server_type == 'bigip': if self.have.raw_server_type != 'redundant-bigip': result['server_type'] = 'redundant-bigip' elif self.have.server_type == 'bigip': if self.have.raw_server_type != 'single-bigip': result['server_type'] = 'single-bigip' else: result['server_type'] = self.want.server_type elif server_change: if len(self.have.devices) > 1 and self.want.server_type == 'bigip': if self.have.raw_server_type != 'redundant-bigip': result['server_type'] = 'redundant-bigip' elif self.want.server_type == 'bigip': if self.have.raw_server_type != 'single-bigip': result['server_type'] = 'single-bigip' else: result['server_type'] = self.want.server_type return result @property def server_type_and_devices(self): """Compares difference between server type and devices list These two parameters are linked with each other and, therefore, must be compared together to ensure that the correct setting is sent to BIG-IP :return: """ devices_change = self._devices_changed() server_change = self._server_type_changed() if not devices_change and not server_change: return None tmos = tmos_version(self.client) if LooseVersion(tmos) >= LooseVersion('13.0.0'): result = self._handle_current_server_type_and_devices( devices_change, server_change ) return result else: result = self._handle_legacy_server_type_and_devices( devices_change, server_change ) return result @property def state(self): if self.want.state == 'disabled' and self.have.enabled: return dict(disabled=True) elif self.want.state in ['present', 'enabled'] and self.have.disabled: return dict(enabled=True) @property def monitors(self): if self.want.monitors is None: return None if self.want.monitors == '/Common/bigip' and self.have.monitors == '/Common/bigip': return None if self.want.monitors == '/Common/bigip' and self.have.monitors is None: return None if self.want.monitors == '/Common/bigip' and len(self.have.monitors) > 0: return '/Common/bigip' if self.have.monitors is None: return self.want.monitors if self.have.monitors != self.want.monitors: return self.want.monitors @property def prober_pool(self): if self.want.prober_pool is None: return None if self.have.prober_pool is None: if self.want.prober_pool == '': return None if self.want.prober_pool != self.have.prober_pool: return self.want.prober_pool @property def prober_preference(self): if self.want.prober_preference is None: return None if self.want.prober_preference == self.have.prober_preference: return None if self.want.prober_preference == 'pool' and self.want.prober_pool is None: raise F5ModuleError( "A prober_pool needs to be set if prober_preference is set to 'pool'" ) if self.want.prober_preference != 'pool' and self.have.prober_preference == 'pool': if self.want.prober_fallback != 'pool' and self.want.prober_pool != '': raise F5ModuleError( "To change prober_preference from {0} to {1}, set prober_pool to an empty string".format( self.have.prober_preference, self.want.prober_preference ) ) if self.want.prober_preference == self.want.prober_fallback: raise F5ModuleError( "Prober_preference and prober_fallback must not be equal." ) if self.want.prober_preference == self.have.prober_fallback: raise F5ModuleError( "Cannot set prober_preference to {0} if prober_fallback on device is set to {1}.".format( self.want.prober_preference, self.have.prober_fallback ) ) if self.want.prober_preference != self.have.prober_preference: return self.want.prober_preference @property def prober_fallback(self): if self.want.prober_fallback is None: return None if self.want.prober_fallback == self.have.prober_fallback: return None if self.want.prober_fallback == 'pool' and self.want.prober_pool is None: raise F5ModuleError( "A prober_pool needs to be set if prober_fallback is set to 'pool'" ) if self.want.prober_fallback != 'pool' and self.have.prober_fallback == 'pool': if self.want.prober_preference != 'pool' and self.want.prober_pool != '': raise F5ModuleError( "To change prober_fallback from {0} to {1}, set prober_pool to an empty string".format( self.have.prober_fallback, self.want.prober_fallback ) ) if self.want.prober_preference == self.want.prober_fallback: raise F5ModuleError( "Prober_preference and prober_fallback must not be equal." ) if self.want.prober_fallback == self.have.prober_preference: raise F5ModuleError( "Cannot set prober_fallback to {0} if prober_preference on device is set to {1}.".format( self.want.prober_fallback, self.have.prober_preference ) ) if self.want.prober_fallback != self.have.prober_fallback: return self.want.prober_fallback class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.kwargs = kwargs def exec_module(self): if not module_provisioned(self.client, 'gtm'): raise F5ModuleError( "GTM must be provisioned to use this module." ) if self.version_is_less_than('13.0.0'): manager = self.get_manager('v1') else: manager = self.get_manager('v2') return manager.exec_module() def get_manager(self, type): if type == 'v1': return V1Manager(**self.kwargs) elif type == 'v2': return V2Manager(**self.kwargs) def version_is_less_than(self, version): tmos = tmos_version(self.client) if LooseVersion(tmos) < LooseVersion(version): return True else: return False class BaseManager(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.want.update(dict(client=self.client)) 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) diff.client = self.client 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 exec_module(self): changed = False result = dict() state = self.want.state if state in ['present', 'enabled', 'disabled']: changed = self.present() elif state == "absent": changed = self.absent() 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 _check_link_discovery_requirements(self): if self.want.link_discovery in ['enabled', 'enabled-no-delete'] and self.want.virtual_server_discovery == 'disabled': raise F5ModuleError( "Virtual server discovery must be enabled if link discovery is enabled" ) def present(self): if self.exists(): return self.update() else: return self.create() def create(self): if self.want.state == 'disabled': self.want.update({'disabled': True}) elif self.want.state in ['present', 'enabled']: self.want.update({'enabled': True}) self.adjust_server_type_by_version() self.should_update() if self.want.devices is None: raise F5ModuleError( "You must provide an initial device." ) self._assign_creation_defaults() self.handle_prober_settings() self._set_changed_options() if self.module.check_mode: return True self.create_on_device() if self.exists(): return True else: raise F5ModuleError("Failed to create the server") def create_on_device(self): params = self.changes.api_params() params['name'] = self.want.name params['partition'] = self.want.partition uri = "https://{0}:{1}/mgmt/tm/gtm/server/".format( self.client.provider['server'], self.client.provider['server_port'] ) resp = self.client.api.post(uri, json=params) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] in [400, 403]: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) return response['selfLink'] def read_current_from_device(self): uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.name) ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) return ApiParameters(params=response) def should_update(self): result = self._update_changed_options() if result: return True return False def update_on_device(self): params = self.changes.api_params() uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.name) ) resp = self.client.api.patch(uri, json=params) try: response = resp.json() except ValueError as ex: raise F5ModuleError(str(ex)) if 'code' in response and response['code'] == 400: if 'message' in response: raise F5ModuleError(response['message']) else: raise F5ModuleError(resp.content) def absent(self): changed = False if self.exists(): changed = self.remove() return changed def remove(self): if self.module.check_mode: return True self.remove_from_device() if self.exists(): raise F5ModuleError("Failed to delete the server") return True def remove_from_device(self): uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.name) ) response = self.client.api.delete(uri) if response.status == 200: return True raise F5ModuleError(response.content) def exists(self): uri = "https://{0}:{1}/mgmt/tm/gtm/server/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.name) ) resp = self.client.api.get(uri) try: response = resp.json() except ValueError: return False if resp.status == 404 or 'code' in response and response['code'] == 404: return False return True class V1Manager(BaseManager): def _assign_creation_defaults(self): if self.want.server_type is None: if len(self.want.devices) == 0: raise F5ModuleError( "You must provide at least one device." ) elif len(self.want.devices) == 1: self.want.update({'server_type': 'single-bigip'}) else: self.want.update({'server_type': 'redundant-bigip'}) if self.want.link_discovery is None: self.want.update({'link_discovery': 'disabled'}) if self.want.virtual_server_discovery is None: self.want.update({'virtual_server_discovery': 'disabled'}) self._check_link_discovery_requirements() def adjust_server_type_by_version(self): if len(self.want.devices) == 1 and self.want.server_type == 'bigip': self.want.update({'server_type': 'single-bigip'}) if len(self.want.devices) > 1 and self.want.server_type == 'bigip': self.want.update({'server_type': 'redundant-bigip'}) def update(self): self.have = self.read_current_from_device() self.handle_prober_settings() if not self.should_update(): return False if self.module.check_mode: return True self.update_on_device() return True def handle_prober_settings(self): if self.want.prober_preference is not None: self.want._values.pop('prober_preference') if self.want.prober_fallback is not None: self.want._values.pop('prober_fallback') class V2Manager(BaseManager): 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 _assign_creation_defaults(self): if self.want.server_type is None: self.want.update({'server_type': 'bigip'}) if self.want.link_discovery is None: self.want.update({'link_discovery': 'disabled'}) if self.want.virtual_server_discovery is None: self.want.update({'virtual_server_discovery': 'disabled'}) self._check_link_discovery_requirements() def adjust_server_type_by_version(self): pass def handle_prober_settings(self): if self.want.prober_preference == 'pool' and self.want.prober_pool is None: raise F5ModuleError( "A prober_pool needs to be set if prober_preference is set to 'pool'" ) if self.want.prober_preference is not None and self.want.prober_fallback is not None: if self.want.prober_preference == self.want.prober_fallback: raise F5ModuleError( "The parameters for prober_preference and prober_fallback must not be the same." ) if self.want.prober_fallback == 'pool' and self.want.prober_pool is None: raise F5ModuleError( "A prober_pool needs to be set if prober_fallback is set to 'pool'" ) class ArgumentSpec(object): def __init__(self): self.states = ['absent', 'present', 'enabled', 'disabled'] self.server_types = [ 'alteon-ace-director', 'cisco-css', 'cisco-server-load-balancer', 'generic-host', 'radware-wsd', 'windows-nt-4.0', 'bigip', 'cisco-local-director-v2', 'extreme', 'generic-load-balancer', 'sun-solaris', 'cacheflow', 'cisco-local-director-v3', 'foundry-server-iron', 'netapp', 'windows-2000-server' ] self.supports_check_mode = True argument_spec = dict( state=dict( default='present', choices=self.states, ), name=dict(required=True), server_type=dict( choices=self.server_types, aliases=['product'] ), datacenter=dict(), link_discovery=dict( choices=['enabled', 'disabled', 'enabled-no-delete'] ), virtual_server_discovery=dict( choices=['enabled', 'disabled', 'enabled-no-delete'] ), devices=dict( type='list' ), partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) ), iquery_options=dict( type='dict', options=dict( allow_path=dict(type='bool'), allow_service_check=dict(type='bool'), allow_snmp=dict(type='bool') ) ), 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']] ] ), limits=dict( type='dict', options=dict( bits_enabled=dict(type='bool'), packets_enabled=dict(type='bool'), connections_enabled=dict(type='bool'), cpu_enabled=dict(type='bool'), memory_enabled=dict(type='bool'), bits_limit=dict(type='int'), packets_limit=dict(type='int'), connections_limit=dict(type='int'), cpu_limit=dict(type='int'), memory_limit=dict(type='int'), ) ), monitors=dict(type='list'), prober_preference=dict( choices=['inside-datacenter', 'outside-datacenter', 'inherit', 'pool'] ), prober_fallback=dict( choices=['inside-datacenter', 'outside-datacenter', 'inherit', 'pool', 'any', 'none'] ), prober_pool=dict() ) 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, ) client = F5RestClient(**module.params) try: mm = ModuleManager(module=module, client=client) results = mm.exec_module() cleanup_tokens(client) exit_json(module, results, client) except F5ModuleError as ex: cleanup_tokens(client) fail_json(module, ex, client) if __name__ == '__main__': main()