EVOLUTION-MANAGER
Edit File: bigip_pool_member.py
#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright: (c) 2017, F5 Networks Inc. # Copyright: (c) 2013, Matt Hite <mhite@hotmail.com> # 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': 'certified'} DOCUMENTATION = r''' --- module: bigip_pool_member short_description: Manages F5 BIG-IP LTM pool members description: - Manages F5 BIG-IP LTM pool members via iControl SOAP API. version_added: 1.4 options: name: description: - Name of the node to create, or re-use, when creating a new pool member. - This parameter is optional and, if not specified, a node name will be created automatically from either the specified C(address) or C(fqdn). - The C(enabled) state is an alias of C(present). type: str version_added: 2.6 state: description: - Pool member state. type: str required: True choices: - present - absent - enabled - disabled - forced_offline default: present pool: description: - Pool name. This pool must exist. type: str required: True partition: description: - Partition to manage resources on. type: str default: Common address: description: - IP address of the pool member. This can be either IPv4 or IPv6. When creating a new pool member, one of either C(address) or C(fqdn) must be provided. This parameter cannot be updated after it is set. type: str aliases: - ip - host version_added: 2.2 fqdn: description: - FQDN name of the pool member. This can be any name that is a valid RFC 1123 DNS name. Therefore, the only characters that can be used are "A" to "Z", "a" to "z", "0" to "9", the hyphen ("-") and the period ("."). - FQDN names must include at lease one period; delineating the host from the domain. ex. C(host.domain). - FQDN names must end with a letter or a number. - When creating a new pool member, one of either C(address) or C(fqdn) must be provided. This parameter cannot be updated after it is set. type: str aliases: - hostname version_added: 2.6 port: description: - Pool member port. - This value cannot be changed after it has been set. type: int required: True connection_limit: description: - Pool member connection limit. Setting this to 0 disables the limit. type: int description: description: - Pool member description. type: str rate_limit: description: - Pool member rate limit (connections-per-second). Setting this to 0 disables the limit. type: int ratio: description: - Pool member ratio weight. Valid values range from 1 through 100. New pool members -- unless overridden with this value -- default to 1. type: int preserve_node: description: - When state is C(absent) attempts to remove the node that the pool member references. - The node will not be removed if it is still referenced by other pool members. If this happens, the module will not raise an error. - Setting this to C(yes) disables this behavior. type: bool version_added: 2.1 priority_group: description: - Specifies a number representing the priority group for the pool member. - When adding a new member, the default is 0, meaning that the member has no priority. - To specify a priority, you must activate priority group usage when you create a new pool or when adding or removing pool members. When activated, the system load balances traffic according to the priority group number assigned to the pool member. - The higher the number, the higher the priority, so a member with a priority of 3 has higher priority than a member with a priority of 1. type: int version_added: 2.5 fqdn_auto_populate: description: - Specifies whether the system automatically creates ephemeral nodes using the IP addresses returned by the resolution of a DNS query for a node defined by an FQDN. - When C(yes), the system generates an ephemeral node for each IP address returned in response to a DNS query for the FQDN of the node. Additionally, when a DNS response indicates the IP address of an ephemeral node no longer exists, the system deletes the ephemeral node. - When C(no), the system resolves a DNS query for the FQDN of the node with the single IP address associated with the FQDN. - When creating a new pool member, the default for this parameter is C(yes). - Once set this parameter cannot be changed afterwards. - This parameter is ignored when C(reuse_nodes) is C(yes). type: bool version_added: 2.6 reuse_nodes: description: - Reuses node definitions if requested. type: bool default: yes version_added: 2.6 monitors: description: - Specifies the health monitors that the system currently uses to monitor this resource. type: 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. - Specifying an empty string will remove the monitors and revert to inheriting from pool (default). - Specifying C(none) value will remove any health monitoring from the member completely. 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. type: str choices: - all - at_least 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 C(all) is used. type: int type: dict version_added: 2.8 ip_encapsulation: description: - Specifies the IP encapsulation using either IPIP (IP encapsulation within IP, RFC 2003) or GRE (Generic Router Encapsulation, RFC 2784) on outbound packets (from BIG-IP system to server-pool member). - When C(none), disables IP encapsulation. - When C(inherit), inherits IP encapsulation setting from the member's pool. - When any other value, Options are None, Inherit from Pool, and Member Specific. type: str version_added: 2.8 aggregate: description: - List of pool member definitions to be created, modified or removed. - When using C(aggregates) if one of the aggregate definitions is invalid, the aggregate run will fail, indicating the error it last encountered. - The module will C(NOT) rollback any changes it has made prior to encountering the error. - The module also will not indicate what changes were made prior to failure, therefore it is strongly advised to run the module in check mode to make basic validation, prior to module execution. type: list aliases: - members version_added: 2.8 replace_all_with: description: - Remove members not defined in the C(aggregate) parameter. - This operation is all or none, meaning that it will stop if there are some pool members that cannot be removed. type: bool default: no aliases: - purge version_added: 2.8 notes: - In previous versions of this module, which used the SDK, the C(name) parameter would act as C(fqdn) if C(address) or C(fqdn) were not provided. extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) - Wojciech Wypior (@wojtek0806) ''' EXAMPLES = ''' - name: Add pool member bigip_pool_member: pool: my-pool partition: Common host: "{{ ansible_default_ipv4['address'] }}" port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost - name: Modify pool member ratio and description bigip_pool_member: pool: my-pool partition: Common host: "{{ ansible_default_ipv4['address'] }}" port: 80 ratio: 1 description: nginx server provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost - name: Remove pool member from pool bigip_pool_member: state: absent pool: my-pool partition: Common host: "{{ ansible_default_ipv4['address'] }}" port: 80 provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost - name: Force pool member offline bigip_pool_member: state: forced_offline pool: my-pool partition: Common host: "{{ ansible_default_ipv4['address'] }}" port: 80 provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost - name: Create members with priority groups bigip_pool_member: pool: my-pool partition: Common host: "{{ item.address }}" name: "{{ item.name }}" priority_group: "{{ item.priority_group }}" port: 80 provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost loop: - address: 1.1.1.1 name: web1 priority_group: 4 - address: 2.2.2.2 name: web2 priority_group: 3 - address: 3.3.3.3 name: web3 priority_group: 2 - address: 4.4.4.4 name: web4 priority_group: 1 - name: Add pool members aggregate bigip_pool_member: pool: my-pool aggregate: - host: 192.168.1.1 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 - host: 192.168.1.2 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 - host: 192.168.1.3 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost - name: Add pool members aggregate, remove non aggregates bigip_pool_member: pool: my-pool aggregate: - host: 192.168.1.1 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 - host: 192.168.1.2 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 - host: 192.168.1.3 partition: Common port: 80 description: web server connection_limit: 100 rate_limit: 50 ratio: 2 replace_all_with: yes provider: server: lb.mydomain.com user: admin password: secret delegate_to: localhost ''' RETURN = ''' rate_limit: description: The new rate limit, in connections per second, of the pool member. returned: changed type: int sample: 100 connection_limit: description: The new connection limit of the pool member returned: changed type: int sample: 1000 description: description: The new description of pool member. returned: changed type: str sample: My pool member ratio: description: The new pool member ratio weight. returned: changed type: int sample: 50 priority_group: description: The new priority group. returned: changed type: int sample: 3 fqdn_auto_populate: description: Whether FQDN auto population was set on the member or not. returned: changed type: bool sample: True fqdn: description: The FQDN of the pool member. returned: changed type: str sample: foo.bar.com address: description: The address of the pool member. returned: changed type: str sample: 1.2.3.4 monitors: description: The new list of monitors for the resource. returned: changed type: list sample: ['/Common/monitor1', '/Common/monitor2'] replace_all_with: description: Purges all non-aggregate pool members from device returned: changed type: bool sample: yes ''' import os import re from copy import deepcopy from ansible.module_utils.urls import urlparse from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import iteritems from ansible.module_utils.network.common.utils import remove_default_spec 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 fq_name from library.module_utils.network.f5.common import transform_name from library.module_utils.network.f5.common import f5_argument_spec from library.module_utils.network.f5.common import is_valid_hostname from library.module_utils.network.f5.common import flatten_boolean from library.module_utils.network.f5.compare import cmp_str_with_none from library.module_utils.network.f5.ipaddress import is_valid_ip from library.module_utils.network.f5.ipaddress import validate_ip_v6_address from library.module_utils.network.f5.icontrol import TransactionContextManager 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 fq_name from ansible.module_utils.network.f5.common import transform_name from ansible.module_utils.network.f5.common import f5_argument_spec from ansible.module_utils.network.f5.common import is_valid_hostname from ansible.module_utils.network.f5.common import flatten_boolean from ansible.module_utils.network.f5.compare import cmp_str_with_none from ansible.module_utils.network.f5.ipaddress import is_valid_ip from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address from ansible.module_utils.network.f5.icontrol import TransactionContextManager class Parameters(AnsibleF5Parameters): api_map = { 'rateLimit': 'rate_limit', 'connectionLimit': 'connection_limit', 'priorityGroup': 'priority_group', 'monitor': 'monitors', 'inheritProfile': 'inherit_profile', 'profiles': 'ip_encapsulation', } api_attributes = [ 'rateLimit', 'connectionLimit', 'description', 'ratio', 'priorityGroup', 'address', 'fqdn', 'session', 'state', 'monitor', # These two settings are for IP Encapsulation 'inheritProfile', 'profiles', ] returnables = [ 'rate_limit', 'connection_limit', 'description', 'ratio', 'priority_group', 'fqdn_auto_populate', 'session', 'state', 'fqdn', 'address', 'monitors', # IP Encapsulation related 'inherit_profile', 'ip_encapsulation', ] updatables = [ 'rate_limit', 'connection_limit', 'description', 'ratio', 'priority_group', 'fqdn_auto_populate', 'state', 'monitors', 'inherit_profile', 'ip_encapsulation', ] class ModuleParameters(Parameters): @property def full_name(self): delimiter = ':' try: if validate_ip_v6_address(self.full_name_dict['name']): delimiter = '.' except TypeError: pass return '{0}{1}{2}'.format(self.full_name_dict['name'], delimiter, self.port) @property def full_name_dict(self): if self._values['name'] is None: name = self._values['address'] if self._values['address'] else self._values['fqdn'] else: name = self._values['name'] return dict( name=name, port=self.port ) @property def node_name(self): return self.full_name_dict['name'] @property def fqdn_name(self): return self._values['fqdn'] @property def fqdn(self): result = {} if self.fqdn_auto_populate: result['autopopulate'] = 'enabled' else: result['autopopulate'] = 'disabled' if self._values['fqdn'] is None: return result if not is_valid_hostname(self._values['fqdn']): raise F5ModuleError( "The specified 'fqdn' value of: {0} is not a valid hostname.".format(self._values['fqdn']) ) result['tmName'] = self._values['fqdn'] return result @property def pool(self): return fq_name(self.want.partition, self._values['pool']) @property def port(self): if self._values['port'] is None: raise F5ModuleError( "Port value must be specified." ) if 0 > int(self._values['port']) or int(self._values['port']) > 65535: raise F5ModuleError( "Valid ports must be in range 0 - 65535" ) return int(self._values['port']) @property def address(self): if self._values['address'] is None: return None elif self._values['address'] == 'any6': return 'any6' address = self._values['address'].split('%')[0] if is_valid_ip(address): return self._values['address'] raise F5ModuleError( "The specified 'address' value of: {0} is not a valid IP address.".format(address) ) @property def state(self): if self._values['state'] == 'enabled': return 'present' return self._values['state'] @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 len(self._values['monitors']) == 1 and self._values['monitors'][0] == '': return 'default' if len(self._values['monitors']) == 1 and self._values['monitors'][0] == 'none': return '/Common/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) 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 at_least(self): return self._get_availability_value('at_least') @property def ip_encapsulation(self): if self._values['ip_encapsulation'] is None: return None if self._values['ip_encapsulation'] == 'inherit': return 'inherit' if self._values['ip_encapsulation'] in ['', 'none']: return '' return fq_name(self.partition, self._values['ip_encapsulation']) 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]) class ApiParameters(Parameters): @property def ip_encapsulation(self): """Returns a simple name for the tunnel. The API stores the data like so "profiles": [ { "name": "gre", "partition": "Common", "nameReference": { "link": "https://localhost/mgmt/tm/net/tunnels/gre/~Common~gre?ver=13.1.0.7" } } ] This method returns that data as a simple profile name. For instance, /Common/gre This allows us to do comparisons of it in the Difference class and then, as needed, translate it back to the more complex form in the UsableChanges class. Returns: string: The simple form representation of the tunnel """ if self._values['ip_encapsulation'] is None and self.inherit_profile == 'yes': return 'inherit' if self._values['ip_encapsulation'] is None and self.inherit_profile == 'no': return '' if self._values['ip_encapsulation'] is None: return None # There can be only one tunnel = self._values['ip_encapsulation'][0] return fq_name(tunnel['partition'], tunnel['name']) @property def inherit_profile(self): return flatten_boolean(self._values['inherit_profile']) @property def allow(self): if self._values['allow'] is None: return '' if self._values['allow'][0] == 'All': return 'all' allow = self._values['allow'] result = list(set([str(x) for x in allow])) result = sorted(result) return result @property def rate_limit(self): if self._values['rate_limit'] is None: return None if self._values['rate_limit'] == 'disabled': return 0 return int(self._values['rate_limit']) @property def state(self): if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and self._values['session'] in ['user-enabled']: return 'present' elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled': # monitor-enabled + checking: # Monitor is checking to see state of pool member. For instance, # whether it is up or down # # monitor-enabled + down: # Monitor returned and determined that pool member is down. # # monitor-enabled + up # Monitor returned and determined that pool member is up. return 'present' elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: return 'forced_offline' else: return 'disabled' @property def availability_requirement_type(self): if self._values['monitors'] is None: return None if 'min ' in self._values['monitors']: return 'at_least' 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'] == 'default': return 'default' 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) else: result = ' and '.join(monitors).strip() return result @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') @property def fqdn_auto_populate(self): if self._values['fqdn'] is None: return None if 'autopopulate' in self._values['fqdn']: if self._values['fqdn']['autopopulate'] == 'enabled': return True return False @property def fqdn(self): if self._values['fqdn'] is None: return None if 'tmName' in self._values['fqdn']: return self._values['fqdn']['tmName'] class NodeApiParameters(Parameters): pass 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 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 class ReportableChanges(Changes): @property def ssl_cipher_suite(self): default = ':'.join(sorted(Parameters._ciphers.split(':'))) if self._values['ssl_cipher_suite'] == default: return 'default' else: return self._values['ssl_cipher_suite'] @property def fqdn_auto_populate(self): if self._values['fqdn'] is None: return None if 'autopopulate' in self._values['fqdn']: if self._values['fqdn']['autopopulate'] == 'enabled': return True return False @property def fqdn(self): if self._values['fqdn'] is None: return None if 'tmName' in self._values['fqdn']: return self._values['fqdn']['tmName'] @property def state(self): if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and self._values['session'] in ['user-enabled']: return 'present' elif self._values['state'] in ['down', 'up', 'checking'] and self._values['session'] == 'monitor-enabled': return 'present' elif self._values['state'] in ['user-down'] and self._values['session'] in ['user-disabled']: return 'forced_offline' else: return 'disabled' @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' else: return 'all' @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 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 return result 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 state(self): if self.want.state == self.have.state: return None if self.want.state == 'forced_offline': return { 'state': 'user-down', 'session': 'user-disabled' } elif self.want.state == 'disabled': return { 'state': 'user-up', 'session': 'user-disabled' } elif self.want.state in ['present', 'enabled']: return { 'state': 'user-up', 'session': 'user-enabled' } @property def fqdn_auto_populate(self): if self.want.fqdn_auto_populate is not None: if self.want.fqdn_auto_populate != self.have.fqdn_auto_populate: raise F5ModuleError( "The fqdn_auto_populate cannot be changed once it has been set." ) @property def monitors(self): if self.want.monitors is None: return None if self.want.monitors == 'default' and self.have.monitors == 'default': return None if self.want.monitors == 'default' and self.have.monitors is None: return None if self.want.monitors == 'default' and len(self.have.monitors) > 0: return 'default' # this is necessary as in v12 there is a bug where returned value has a space at the end if self.want.monitors == '/Common/none' and self.have.monitors in ['/Common/none', '/Common/none ']: return None if self.have.monitors is None: return self.want.monitors if self.have.monitors != self.want.monitors: return self.want.monitors @property def ip_encapsulation(self): result = cmp_str_with_none(self.want.ip_encapsulation, self.have.ip_encapsulation) if result is None: return None if result == 'inherit': return dict( inherit_profile='enabled', ip_encapsulation=[] ) elif result in ['', 'none']: return dict( inherit_profile='disabled', ip_encapsulation=[] ) else: return dict( inherit_profile='disabled', ip_encapsulation=[ dict( name=os.path.basename(result).strip('/'), partition=os.path.dirname(result) ) ] ) class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = F5RestClient(**self.module.params) self.want = None self.have = None self.changes = None self.replace_all_with = False self.purge_links = list() self.on_device = None 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 _announce_deprecations(self, result): warnings = result.pop('__warnings', []) for warning in warnings: self.module.deprecate( msg=warning['msg'], version=warning['version'] ) def exec_module(self): wants = None if self.module.params['replace_all_with']: self.replace_all_with = True if self.module.params['aggregate']: wants = self.merge_defaults_for_aggregate(self.module.params) result = dict() changed = False if self.replace_all_with and self.purge_links: self.purge() changed = True if self.module.params['aggregate']: result['aggregate'] = list() for want in wants: output = self.execute(want) if output['changed']: changed = output['changed'] result['aggregate'].append(output) else: output = self.execute(self.module.params) if output['changed']: changed = output['changed'] result.update(output) if changed: result['changed'] = True return result def merge_defaults_for_aggregate(self, params): defaults = deepcopy(params) aggregate = defaults.pop('aggregate') for i, j in enumerate(aggregate): for k, v in iteritems(defaults): if k != 'replace_all_with': if j.get(k, None) is None and v is not None: aggregate[i][k] = v if self.replace_all_with: self.compare_aggregate_names(aggregate) return aggregate def _filter_ephemerals(self): on_device = self._read_purge_collection() if not on_device: self.on_device = [] return self.on_device = [member for member in on_device if member['ephemeral'] != "true"] def compare_fqdns(self, items): if any('fqdn' in item for item in items): aggregates = [item['fqdn'] for item in items if 'fqdn' in item and item['fqdn']] collection = [member['fqdn']['tmName'] for member in self.on_device if 'tmName' in member['fqdn']] diff = set(collection) - set(aggregates) if diff: fqdns = [ member['selfLink'] for member in self.on_device if 'tmName' in member['fqdn'] and member['fqdn']['tmName'] in diff] self.purge_links.extend(fqdns) return True return False return False def compare_addresses(self, items): if any('address' in item for item in items): aggregates = [item['address'] for item in items if 'address' in item and item['address']] collection = [member['address'] for member in self.on_device] diff = set(collection) - set(aggregates) if diff: addresses = [item['selfLink'] for item in self.on_device if item['address'] in diff] self.purge_links.extend(addresses) return True return False return False def compare_aggregate_names(self, items): self._filter_ephemerals() if not self.on_device: return False fqdns = self.compare_fqdns(items) addresses = self.compare_addresses(items) if self.purge_links: if fqdns: if not addresses: self.purge_links.extend([item['selfLink'] for item in self.on_device if 'tmName' not in item['fqdn']]) def execute(self, params=None): self.want = ModuleParameters(params=params) self.have = ApiParameters() self.changes = UsableChanges() changed = False result = dict() state = params['state'] if state in ['present', 'enabled', 'disabled', 'forced_offline']: 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 present(self): if self.exists(): return self.update() else: return self.create() def absent(self): if self.exists(): return self.remove() elif not self.want.preserve_node and self.node_exists(): return self.remove_node_from_device() 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 not self.want.preserve_node: self.remove_node_from_device() if self.exists(): raise F5ModuleError("Failed to delete the resource.") return True def purge(self): if self.module.check_mode: return True if not self.pool_exist(): raise F5ModuleError('The specified pool does not exist') self.purge_from_device() return True def create(self): if self.want.reuse_nodes: self._update_address_with_existing_nodes() if self.want.name and not any(x for x in [self.want.address, self.want.fqdn_name]): self._set_host_by_name() if self.want.ip_encapsulation == '': self.changes.update({'inherit_profile': 'enabled'}) self.changes.update({'profiles': []}) elif self.want.ip_encapsulation: # Read the current list of tunnels so that IP encapsulation # checking can take place. tunnels_gre = self.read_current_tunnels_from_device('gre') tunnels_ipip = self.read_current_tunnels_from_device('ipip') tunnels = tunnels_gre + tunnels_ipip if self.want.ip_encapsulation not in tunnels: raise F5ModuleError( "The specified 'ip_encapsulation' tunnel was not found on the system." ) self.changes.update({'inherit_profile': 'disabled'}) self._update_api_state_attributes() self._set_changed_options() if self.module.check_mode: return True self.create_on_device() return True def exists(self): if not self.pool_exist(): raise F5ModuleError('The specified pool does not exist') uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.want.partition, self.want.pool)), transform_name(self.want.partition, self.want.full_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 def pool_exist(self): if self.replace_all_with: pool_name = transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) else: pool_name = transform_name(name=fq_name(self.want.partition, self.want.pool)) uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}".format( self.client.provider['server'], self.client.provider['server_port'], pool_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 def node_exists(self): uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.node_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 def _set_host_by_name(self): if is_valid_ip(self.want.name): self.want.update({ 'fqdn': None, 'address': self.want.name }) else: if not is_valid_hostname(self.want.name): raise F5ModuleError( "'name' is neither a valid IP address or FQDN name." ) self.want.update({ 'fqdn': self.want.name, 'address': None }) def _update_api_state_attributes(self): if self.want.state == 'forced_offline': self.want.update({ 'state': 'user-down', 'session': 'user-disabled', }) elif self.want.state == 'disabled': self.want.update({ 'state': 'user-up', 'session': 'user-disabled', }) elif self.want.state in ['present', 'enabled']: self.want.update({ 'state': 'user-up', 'session': 'user-enabled', }) def _update_address_with_existing_nodes(self): try: have = self.read_current_node_from_device(self.want.node_name) if self.want.fqdn_auto_populate and self.want.reuse_nodes: self.module.warn("'fqdn_auto_populate' is discarded in favor of the re-used node's auto-populate setting.") self.want.update({ 'fqdn_auto_populate': True if have.fqdn['autopopulate'] == 'enabled' else False }) if 'tmName' in have.fqdn: self.want.update({ 'fqdn': have.fqdn['tmName'], 'address': 'any6' }) else: self.want.update({ 'address': have.address }) except Exception: return None def _read_purge_collection(self): uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.module.params['partition'], self.module.params['pool'])) ) query = '?$select=name,selfLink,fqdn,address,ephemeral' resp = self.client.api.get(uri + query) 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) if 'items' in response: return response['items'] return [] def create_on_device(self): params = self.changes.api_params() params['name'] = self.want.full_name params['partition'] = self.want.partition uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.want.partition, self.want.pool)), ) 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) def update_on_device(self): params = self.changes.api_params() uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.want.partition, self.want.pool)), transform_name(self.want.partition, self.want.full_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 remove_from_device(self): uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.want.partition, self.want.pool)), transform_name(self.want.partition, self.want.full_name) ) response = self.client.api.delete(uri) if response.status == 200: return True raise F5ModuleError(response.content) def remove_node_from_device(self): uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, self.want.node_name) ) response = self.client.api.delete(uri) if response.status == 200: return True raise F5ModuleError(response.content) def read_current_from_device(self): uri = "https://{0}:{1}/mgmt/tm/ltm/pool/{2}/members/{3}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(name=fq_name(self.want.partition, self.want.pool)), transform_name(self.want.partition, self.want.full_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) # Read the current list of tunnels so that IP encapsulation # checking can take place. tunnels_gre = self.read_current_tunnels_from_device('gre') tunnels_ipip = self.read_current_tunnels_from_device('ipip') response['tunnels'] = tunnels_gre + tunnels_ipip return ApiParameters(params=response) def read_current_node_from_device(self, node): uri = "https://{0}:{1}/mgmt/tm/ltm/node/{2}".format( self.client.provider['server'], self.client.provider['server_port'], transform_name(self.want.partition, node) ) 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 NodeApiParameters(params=response) def read_current_tunnels_from_device(self, tunnel_type): uri = "https://{0}:{1}/mgmt/tm/net/tunnels/{2}".format( self.client.provider['server'], self.client.provider['server_port'], tunnel_type ) 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) if 'items' not in response: return [] return [x['fullPath'] for x in response['items']] def _prepare_links(self, collection): # this is to ensure no duplicates are in the provided collection no_dupes = list(set(collection)) links = list() purge_paths = [urlparse(link).path for link in no_dupes] for path in purge_paths: link = "https://{0}:{1}{2}".format( self.client.provider['server'], self.client.provider['server_port'], path ) links.append(link) return links def purge_from_device(self): links = self._prepare_links(self.purge_links) with TransactionContextManager(self.client) as transact: for link in links: resp = transact.api.delete(link) 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 True class ArgumentSpec(object): def __init__(self): self.supports_check_mode = True element_spec = dict( address=dict(aliases=['host', 'ip']), fqdn=dict( aliases=['hostname'] ), name=dict(), port=dict(type='int'), connection_limit=dict(type='int'), description=dict(), rate_limit=dict(type='int'), ratio=dict(type='int'), preserve_node=dict(type='bool'), priority_group=dict(type='int'), state=dict( default='present', choices=['absent', 'present', 'enabled', 'disabled', 'forced_offline'] ), fqdn_auto_populate=dict(type='bool'), reuse_nodes=dict(type='bool', default=True), availability_requirements=dict( type='dict', options=dict( type=dict( choices=['all', 'at_least'], required=True ), at_least=dict(type='int'), ), required_if=[ ['type', 'at_least', ['at_least']], ] ), monitors=dict(type='list'), ip_encapsulation=dict(), partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) ), ) aggregate_spec = deepcopy(element_spec) # remove default in aggregate spec, to handle common arguments remove_default_spec(aggregate_spec) self.argument_spec = dict( aggregate=dict( type='list', elements='dict', options=aggregate_spec, aliases=['members'], mutually_exclusive=[ ['address', 'fqdn'] ], required_one_of=[ ['address', 'fqdn'] ], ), replace_all_with=dict( type='bool', aliases=['purge'], default='no' ), pool=dict(required=True), partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) ), ) self.argument_spec.update(element_spec) self.argument_spec.update(f5_argument_spec) self.mutually_exclusive = [ ['address', 'aggregate'], ['fqdn', 'aggregate'] ] self.required_one_of = [ ['address', 'fqdn', 'aggregate'], ] def main(): spec = ArgumentSpec() module = AnsibleModule( argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode, mutually_exclusive=spec.mutually_exclusive, required_one_of=spec.required_one_of, ) try: mm = ModuleManager(module=module) results = mm.exec_module() module.exit_json(**results) except F5ModuleError as ex: module.fail_json(msg=str(ex)) if __name__ == '__main__': main()