#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright 2017 F5 Networks Inc. # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . ANSIBLE_METADATA = { 'status': ['preview'], 'supported_by': 'community', 'metadata_version': '1.0' } DOCUMENTATION = ''' --- module: bigip_pool short_description: Manages F5 BIG-IP LTM pools. description: - Manages F5 BIG-IP LTM pools via iControl REST API. version_added: 1.2 author: - Tim Rupp (@caphrim007) - Wojciech Wypior (@wojtek0806) notes: - Requires BIG-IP software version >= 11. - F5 developed module 'F5-SDK' required (https://github.com/F5Networks/f5-common-python). - Best run as a local_action in your playbook. requirements: - f5-sdk options: description: description: - Specifies descriptive text that identifies the pool. version_added: "2.3" name: description: - Pool name required: True aliases: - pool lb_method: description: - Load balancing method. When creating a new pool, if this value is not specified, the default of C(round-robin) will be used. version_added: "1.3" choices: - dynamic-ratio-member - dynamic-ratio-node - fastest-app-response - fastest-node - least-connections-member - least-connections-node - least-sessions - observed-member - observed-node - predictive-member - predictive-node - ratio-least-connections-member - ratio-least-connections-node - ratio-member - ratio-node - ratio-session - round-robin - weighted-least-connections-member - weighted-least-connections-nod monitor_type: description: - Monitor rule type when C(monitors) > 1. version_added: "1.3" choices: ['and_list', 'm_of_n'] quorum: description: - Monitor quorum value when C(monitor_type) is C(m_of_n). version_added: "1.3" monitors: description: - Monitor template name list. If the partition is not provided as part of the monitor name, then the C(partition) option will be used instead. version_added: "1.3" slow_ramp_time: description: - Sets the ramp-up time (in seconds) to gradually ramp up the load on newly added or freshly detected up pool members. version_added: "1.3" reselect_tries: description: - Sets the number of times the system tries to contact a pool member after a passive failure. version_added: "2.2" service_down_action: description: - Sets the action to take when node goes down in pool. version_added: "1.3" choices: - none - reset - drop - reselect host: description: - Pool member IP. aliases: - address port: description: - Pool member port. extends_documentation_fragment: f5 ''' EXAMPLES = ''' - name: Create pool bigip_pool: server: "lb.mydomain.com" user: "admin" password: "secret" state: "present" name: "my-pool" partition: "Common" lb_method: "least_connection_member" slow_ramp_time: 120 delegate_to: localhost - name: Modify load balancer method bigip_pool: server: "lb.mydomain.com" user: "admin" password: "secret" state: "present" name: "my-pool" partition: "Common" lb_method: "round_robin" delegate_to: localhost - name: Add pool member bigip_pool: server: "lb.mydomain.com" user: "admin" password: "secret" state: "present" name: "my-pool" partition: "Common" host: "{{ ansible_default_ipv4['address'] }}" port: 80 delegate_to: localhost - name: Remove pool member from pool bigip_pool: server: "lb.mydomain.com" user: "admin" password: "secret" state: "absent" name: "my-pool" partition: "Common" host: "{{ ansible_default_ipv4['address'] }}" port: 80 delegate_to: localhost - name: Delete pool bigip_pool: server: "lb.mydomain.com" user: "admin" password: "secret" state: "absent" name: "my-pool" partition: "Common" delegate_to: localhost ''' RETURN = ''' monitor_type: description: The contact that was set on the datacenter. returned: changed type: string sample: "admin@root.local" quorum: description: The quorum that was set on the pool returned: changed type: int sample: 2 monitors: description: Monitors set on the pool. returned: changed type: list sample: ['/Common/http', '/Common/gateway_icmp'] service_down_action: description: Service down action that is set on the pool. returned: changed type: string sample: "reset" description: description: Description set on the pool. returned: changed type: string sample: "Pool of web servers" lb_method: description: The LB method set for the pool. returned: changed type: string sample: "round-robin" host: description: IP of pool member included in pool. returned: changed type: string sample: "10.10.10.10" port: description: Port of pool member included in pool. returned: changed type: int sample: 80 slow_ramp_time: description: The new value that is set for the slow ramp-up time. returned: changed type: int sample: 500 reselect_tries: description: The new value that is set for the number of tries to contact member returned: changed type: int sample: 10 ''' import re import os from netaddr import IPAddress, AddrFormatError from ansible.module_utils.f5_utils import ( AnsibleF5Client, AnsibleF5Parameters, HAS_F5SDK, F5ModuleError, iControlUnexpectedHTTPError ) class Parameters(AnsibleF5Parameters): api_map = { 'loadBalancingMode': 'lb_method', 'slowRampTime': 'slow_ramp_time', 'reselectTries': 'reselect_tries', 'serviceDownAction': 'service_down_action' } updatables = [ 'monitor_type', 'quorum', 'monitors', 'service_down_action', 'description', 'lb_method', 'slow_ramp_time', 'reselect_tries', 'host', 'port' ] returnables = [ 'monitor_type', 'quorum', 'monitors', 'service_down_action', 'description', 'lb_method', 'host', 'port', 'slow_ramp_time', 'reselect_tries', 'monitor', 'member_name', 'name', 'partition' ] api_attributes = [ 'description', 'name', 'loadBalancingMode', 'monitor', 'slowRampTime', 'reselectTries', 'serviceDownAction' ] def __init__(self, params=None): super(Parameters, self).__init__(params) self._values['__warnings'] = [] @property def lb_method(self): lb_map = { 'ratio_node_address': 'ratio-node', 'dynamic_ratio': 'dynamic-ratio-node', 'least_connection_member': 'least-connections-member', 'least_connection_node_address': 'least-connections-node', 'fastest_node_address': 'fastest-node', 'observed_node_address': 'observed-node', 'predictive_node_address': 'predictive-node', 'weighted_least_connection_member': 'weighted-least-connections-member', 'weighted_least_connection_node_address': 'weighted-least-connections-node', 'ratio_least_connection_member': 'ratio-least-connections-member', 'ratio_least_connection_node_address': 'ratio-least-connections-node' } lb_method = self._values['lb_method'] if lb_method is None: return None spec = ArgumentSpec() if lb_method in spec.lb_choice_removed: raise F5ModuleError( "The provided lb_method is not supported" ) if lb_method in spec.lb_choice_deprecated: self._values['__warnings'].append( dict( msg="The provided lb_method '{0}' is deprecated".format(lb_method), version='2.4' ) ) lb_method = lb_map.get(lb_method, lb_method.replace('_', '-')) try: assert lb_method in spec.lb_choice except AssertionError: raise F5ModuleError('Provided lb_method is unknown') return lb_method @property def monitors(self): monitors = list() monitor_list = self._values['monitors'] monitor_type = self._values['monitor_type'] error1 = "The 'monitor_type' parameter cannot be empty when " \ "'monitors' parameter is specified." error2 = "The 'monitor' parameter cannot be empty when " \ "'monitor_type' parameter is specified" if monitor_list is not None and monitor_type is None: raise F5ModuleError(error1) elif monitor_list is None and monitor_type is not None: raise F5ModuleError(error2) elif monitor_list is None: return None for m in monitor_list: if re.match(r'\/\w+\/\w+', m): m = '/{0}/{1}'.format(self.partition, os.path.basename(m)) elif re.match(r'\w+', m): m = '/{0}/{1}'.format(self.partition, m) else: raise F5ModuleError( "Unknown monitor format '{0}'".format(m) ) monitors.append(m) return monitors @property def quorum(self): value = self._values['quorum'] error = "Quorum value must be specified with monitor_type 'm_of_n'." if self._values['monitor_type'] == 'm_of_n' and value is None: raise F5ModuleError(error) return value @property def monitor(self): monitors = self.monitors monitor_type = self._values['monitor_type'] quorum = self.quorum if monitors is None: return None if monitor_type == 'and_list': and_list = list() for m in monitors: if monitors.index(m) == 0: and_list.append(m) else: and_list.append('and') and_list.append(m) result = ' '.join(and_list) else: min_list = list() prefix = 'min {0} of {{'.format(str(quorum)) min_list.append(prefix) for m in monitors: min_list.append(m) min_list.append('}') result = ' '.join(min_list) return result @property def host(self): value = self._values['host'] if value is None: return None msg = "'%s' is not a valid IP address" % value try: IPAddress(value) except AddrFormatError: raise F5ModuleError(msg) return value @host.setter def host(self, value): self._values['host'] = value @property def port(self): value = self._values['port'] if value is None: return None msg = "The provided port '%s' must be between 0 and 65535" % value if value < 0 or value > 65535: raise F5ModuleError(msg) return value @port.setter def port(self, value): self._values['port'] = value @property def member_name(self): if self.host is None or self.port is None: return None mname = str(self.host) + ':' + str(self.port) return mname def to_return(self): result = {} for returnable in self.returnables: result[returnable] = getattr(self, returnable) result = self._filter_params(result) return result def api_params(self): result = {} for api_attribute in self.api_attributes: if self.api_map is not None and api_attribute in self.api_map: result[api_attribute] = getattr( self, self.api_map[api_attribute] ) else: result[api_attribute] = getattr(self, api_attribute) result = self._filter_params(result) return result class ModuleManager(object): def __init__(self, client): self.client = client self.have = None self.want = Parameters(self.client.module.params) self.changes = Parameters() def exec_module(self): changed = False result = dict() state = self.want.state try: if state == "present": changed = self.present() elif state == "absent": changed = self.absent() except iControlUnexpectedHTTPError as e: raise F5ModuleError(str(e)) changes = self.changes.to_return() result.update(**changes) result.update(dict(changed=changed)) self._announce_deprecations() return result def _announce_deprecations(self): warnings = [] if self.want: warnings += self.want._values.get('__warnings', []) if self.have: warnings += self.have._values.get('__warnings', []) for warning in warnings: self.client.module.deprecate( msg=warning['msg'], version=warning['version'] ) 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 = Parameters(changed) def _update_changed_options(self): changed = {} for key in Parameters.updatables: if getattr(self.want, key) is not None: attr1 = getattr(self.want, key) attr2 = getattr(self.have, key) if attr1 != attr2: changed[key] = attr1 if changed: self.changes = Parameters(changed) return True return False def _member_does_not_exist(self, members): name = self.want.member_name # Return False if name is None, so that we don't attempt to create it if name is None: return False for member in members: if member.name == name: host, port = name.split(':') self.have.host = host self.have.port = int(port) return False return True def present(self): if self.exists(): return self.update() else: return self.create() def absent(self): if self.exists(): return self.remove() return False def should_update(self): result = self._update_changed_options() if result: return True return False def update(self): self.have, members, poolres = self.read_current_from_device() if not self.client.check_mode: if self._member_does_not_exist(members): self.create_member_on_device(poolres) if not self.should_update(): return False if self.client.check_mode: return True self.update_on_device() return True def remove(self): if self.client.check_mode: return True self.remove_from_device() if self.exists(): raise F5ModuleError("Failed to delete the Pool") return True def create(self): self._set_changed_options() if self.client.check_mode: return True self.create_on_device() if self.want.member_name: self.have, members, poolres = self.read_current_from_device() if self._member_does_not_exist(members): self.create_member_on_device(poolres) return True def create_on_device(self): params = self.want.api_params() self.client.api.tm.ltm.pools.pool.create( partition=self.want.partition, **params ) def create_member_on_device(self, poolres): poolres.members_s.members.create( name=self.want.member_name, partition=self.want.partition ) def update_on_device(self): params = self.want.api_params() result = self.client.api.tm.ltm.pools.pool.load( name=self.want.name, partition=self.want.partition ) result.modify(**params) def exists(self): return self.client.api.tm.ltm.pools.pool.exists( name=self.want.name, partition=self.want.partition ) def remove_from_device(self): result = self.client.api.tm.ltm.pools.pool.load( name=self.want.name, partition=self.want.partition ) if self.want.member_name and self.want.port and self.want.pool: member = result.members_s.members.load( name=self.want.member_name, partition=self.want.partition ) if member: member.delete() self.delete_node_on_device() else: result.delete() def read_current_from_device(self): tmp_res = self.client.api.tm.ltm.pools.pool.load( name=self.want.name, partition=self.want.partition ) members = tmp_res.members_s.get_collection() result = tmp_res.attrs return Parameters(result), members, tmp_res def delete_node_on_device(self): resource = self.client.api.tm.ltm.nodes.node.load( name=self.want.host, partition=self.want.partition ) try: resource.delete() except iControlUnexpectedHTTPError as e: # If we cannot remove it, it is in use, it is up to user to delete # it later. if "is referenced by a member of pool" in str(e): return else: raise class ArgumentSpec(object): def __init__(self): self.lb_choice_deprecated = [ 'round_robin', 'ratio_member', 'least_connection_member', 'observed_member', 'predictive_member', 'ratio_node_address', 'least_connection_node_address', 'fastest_node_address', 'observed_node_address', 'predictive_node_address', 'dynamic_ratio', 'fastest_app_response', 'least_sessions', 'dynamic_ratio_member', 'ratio_session', 'weighted_least_connection_member', 'ratio_least_connection_member', 'weighted_least_connection_node_address', 'ratio_least_connection_node_address' ] self.lb_choice_removed = [ 'l3_addr' ] self.lb_choice = [ 'dynamic-ratio-member', 'dynamic-ratio-node', 'fastest-app-response', 'fastest-node', 'least-connections-member', 'least-connections-node', 'least-sessions', 'observed-member', 'observed-node', 'predictive-member', 'predictive-node', 'ratio-least-connections-member', 'ratio-least-connections-node', 'ratio-member', 'ratio-node', 'ratio-session', 'round-robin', 'weighted-least-connections-member', 'weighted-least-connections-node' ] lb_choices = self.lb_choice_removed + self.lb_choice + self.lb_choice_deprecated self.supports_check_mode = True self.argument_spec = dict( name=dict( required=True, aliases=['pool'] ), lb_method=dict( choices=lb_choices ), monitor_type=dict( choices=[ 'and_list', 'm_of_n' ] ), quorum=dict( type='int' ), monitors=dict( type='list' ), slow_ramp_time=dict( type='int' ), reselect_tries=dict( type='int' ), service_down_action=dict( choices=[ 'none', 'reset', 'drop', 'reselect' ] ), description=dict(), host=dict( aliases=['address'], removed_in_version='2.4' ), port=dict( type='int', removed_in_version='2.4' ) ) self.f5_product_name = 'bigip' def main(): if not HAS_F5SDK: raise F5ModuleError("The python f5-sdk module is required") spec = ArgumentSpec() client = AnsibleF5Client( argument_spec=spec.argument_spec, supports_check_mode=spec.supports_check_mode, f5_product_name=spec.f5_product_name ) try: mm = ModuleManager(client) results = mm.exec_module() client.module.exit_json(**results) except F5ModuleError as e: client.module.fail_json(msg=str(e)) if __name__ == '__main__': main()