diff --git a/lib/ansible/modules/network/f5/bigip_gtm_facts.py b/lib/ansible/modules/network/f5/bigip_gtm_facts.py index 1ba5501eb4e..5bb23c41edd 100644 --- a/lib/ansible/modules/network/f5/bigip_gtm_facts.py +++ b/lib/ansible/modules/network/f5/bigip_gtm_facts.py @@ -582,6 +582,14 @@ class ServerParameters(BaseParameters): 'virtual_server_discovery', 'addresses', 'devices', 'virtual_servers' ] + @property + def product(self): + if self._values['product'] is None: + return None + if self._values['product'] in ['single-bigip', 'redundant-bigip']: + return 'bigip' + return self._values['product'] + @property def devices(self): result = [] diff --git a/lib/ansible/modules/network/f5/bigip_gtm_pool.py b/lib/ansible/modules/network/f5/bigip_gtm_pool.py index d12cf4f2a3a..c2e83ecc788 100644 --- a/lib/ansible/modules/network/f5/bigip_gtm_pool.py +++ b/lib/ansible/modules/network/f5/bigip_gtm_pool.py @@ -18,15 +18,14 @@ module: bigip_gtm_pool short_description: Manages F5 BIG-IP GTM pools description: - Manages F5 BIG-IP GTM pools. -version_added: "2.4" +version_added: 2.4 options: state: description: - - Pool member state. When C(present), ensures that the pool is - created and enabled. When C(absent), ensures that the pool is - removed from the system. When C(enabled) or C(disabled), ensures - that the pool is enabled or disabled (respectively) on the remote - device. + - Pool state. When C(present), ensures that the pool is created and enabled. + When C(absent), ensures that the pool is removed from the system. When + C(enabled) or C(disabled), ensures that the pool is enabled or disabled + (respectively) on the remote device. choices: - present - absent @@ -122,6 +121,60 @@ options: - Device partition to manage resources on. default: Common version_added: 2.5 + members: + description: + - Members to assign to the pool. + - The order of the members in this list is the order that they will be listed in the pool. + suboptions: + server: + description: + - Name of the server which the pool member is a part of. + required: True + virtual_server: + description: + - Name of the virtual server, associated with the server, that the pool member is a part of. + required: True + version_added: 2.6 + monitors: + description: + - Specifies the health monitors that the system currently uses to monitor this resource. + - When C(availability_requirements.type) is C(require), you may only have a single monitor in the + C(monitors) list. + version_added: 2.6 + 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.6 notes: - Requires the netaddr Python package on the host. This is as easy as pip install netaddr. @@ -153,6 +206,24 @@ fallback_ip: returned: changed type: string sample: 10.10.10.10 +monitors: + description: The new list of monitors for the resource. + returned: changed + type: list + sample: ['/Common/monitor1', '/Common/monitor2'] +members: + description: List of members in the pool. + returned: changed + type: complex + contains: + server: + description: The name of the server portion of the member. + returned: changed + type: string + virtual_server: + description: The name of the virtual server portion of the member. + returned: changed + type: string ''' EXAMPLES = r''' @@ -174,37 +245,37 @@ EXAMPLES = r''' delegate_to: localhost ''' -from distutils.version import LooseVersion +import copy +import re + from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback - -HAS_DEVEL_IMPORTS = False +from distutils.version import LooseVersion try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError + from f5.sdk_exception import LazyAttributesRequired except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from f5.sdk_exception import LazyAttributesRequired except ImportError: HAS_F5SDK = False @@ -214,8 +285,6 @@ try: except ImportError: HAS_NETADDR = False -import copy - class Parameters(AnsibleF5Parameters): api_map = { @@ -225,19 +294,47 @@ class Parameters(AnsibleF5Parameters): 'verifyMemberAvailability': 'verify_member_availability', 'fallbackIpv4': 'fallback_ip', 'fallbackIpv6': 'fallback_ip', - 'fallbackIp': 'fallback_ip' + 'fallbackIp': 'fallback_ip', + 'membersReference': 'members', + 'monitor': 'monitors' } + updatables = [ - 'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method', - 'fallback_ip', 'state' + 'alternate_lb_method', + 'fallback_ip', + 'fallback_lb_method', + 'members', + 'monitors', + 'preferred_lb_method', + 'state', ] + returnables = [ - 'preferred_lb_method', 'alternate_lb_method', 'fallback_lb_method', - 'fallback_ip' + 'alternate_lb_method', + 'fallback_ip', + 'fallback_lb_method', + 'members', + 'monitors', + 'preferred_lb_method', + 'enabled', + 'disabled' ] + api_attributes = [ - 'loadBalancingMode', 'alternateMode', 'fallbackMode', 'verifyMemberAvailability', - 'fallbackIpv4', 'fallbackIpv6', 'fallbackIp', 'enabled', 'disabled' + 'alternateMode', + 'disabled', + 'enabled', + 'fallbackIp', + 'fallbackIpv4', + 'fallbackIpv6', + 'fallbackMode', + 'loadBalancingMode', + 'members', + 'verifyMemberAvailability', + # The monitor attribute is not included here, because it can break the + # API calls to the device. If this bug is ever fixed, uncomment this code. + # + # monitor ] def to_return(self): @@ -316,10 +413,252 @@ class Parameters(AnsibleF5Parameters): return True +class ApiParameters(Parameters): + @property + def members(self): + result = [] + if self._values['members'] is None or 'items' not in self._values['members']: + return [] + for item in self._values['members']['items']: + result.append(dict(item=item['fullPath'], order=item['memberOrder'])) + result = [x['item'] for x in sorted(result, key=lambda k: k['order'])] + return result + + @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'] == '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) + 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_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 members(self): + if self._values['members'] is None: + return None + if len(self._values['members']) == 1 and self._values['members'][0] == '': + return [] + result = [] + for member in self._values['members']: + if 'server' not in member: + raise F5ModuleError( + "One of the provided members is missing a 'server' sub-option." + ) + if 'virtual_server' not in member: + raise F5ModuleError( + "One of the provided members is missing a 'virtual_server' sub-option." + ) + name = '{0}:{1}'.format(member['server'], member['virtual_server']) + name = fq_name(self.partition, name) + if name in result: + continue + result.append(name) + result = list(result) + return result + + @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' + monitors = [fq_name(self.partition, x) for x in self.monitors_list] + if self.availability_requirement_type == 'at_least': + if self.at_least > len(self.monitors_list): + raise F5ModuleError( + "The 'at_least' value must not exceed the number of 'monitors'." + ) + monitors = ' '.join(monitors) + result = 'min {0} of {{ {1} }}'.format(self.at_least, monitors) + elif self.availability_requirement_type == 'require': + monitors = ' '.join(monitors) + if self.number_of_probes > self.number_of_probers: + raise F5ModuleError( + "The 'number_of_probes' must not exceed the 'number_of_probers'." + ) + result = 'require {0} from {1} {{ {2} }}'.format(self.number_of_probes, self.number_of_probers, monitors) + else: + result = ' and '.join(monitors).strip() + + return result + + @property + def availability_requirement_type(self): + if self._values['availability_requirements'] is None: + return None + return self._values['availability_requirements']['type'] + + @property + def number_of_probes(self): + return self._get_availability_value('number_of_probes') + + @property + def number_of_probers(self): + return self._get_availability_value('number_of_probers') + + @property + def at_least(self): + return self._get_availability_value('at_least') + + class Changes(Parameters): pass +class UsableChanges(Changes): + @property + def monitors(self): + if self._values['monitors'] is None: + return None + return self._values['monitors'] + + @property + def members(self): + results = [] + if self._values['members'] is None: + return None + for idx, member in enumerate(self._values['members']): + result = dict( + name=member, + memberOrder=idx + ) + results.append(result) + return results + + +class ReportableChanges(Changes): + @property + def members(self): + results = [] + if self._values['members'] is None: + return None + for member in self._values['members']: + parts = member.split(':') + results.append(dict( + server=fq_name(self.partition, parts[0]), + virtual_server=fq_name(self.partition, parts[1]) + )) + return results + + class Difference(object): def __init__(self, want, have=None): self.want = want @@ -352,6 +691,21 @@ class Difference(object): enabled=True ) + @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' + if self.have.monitors is None: + return self.want.monitors + if self.have.monitors != self.want.monitors: + return self.want.monitors + class ModuleManager(object): def __init__(self, *args, **kwargs): @@ -396,8 +750,8 @@ class BaseManager(object): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) self.have = None - self.want = Parameters(params=self.module.params) - self.changes = Changes() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() def _set_changed_options(self): changed = {} @@ -405,7 +759,7 @@ class BaseManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) def _update_changed_options(self): diff = Difference(self.want, self.have) @@ -421,7 +775,7 @@ class BaseManager(object): else: changed[k] = change if changed: - self.changes = Changes(params=changed) + self.changes = UsableChanges(params=changed) return True return False @@ -438,11 +792,21 @@ class BaseManager(object): except iControlUnexpectedHTTPError as e: raise F5ModuleError(str(e)) - changes = self.changes.to_return() + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() result.update(**changes) result.update(dict(changed=changed)) + self._announce_deprecations(result) return result + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) + def present(self): if self.exists(): return self.update() @@ -474,7 +838,14 @@ class BaseManager(object): self.want.update({'disabled': True}) elif self.want.state in ['present', 'enabled']: self.want.update({'enabled': True}) + self._set_changed_options() + + if self.want.availability_requirement_type == 'require' and len(self.want.monitors_list) > 1: + raise F5ModuleError( + "Only one monitor may be specified when using an availability_requirement type of 'require'" + ) + if self.module.check_mode: return True self.create_on_device() @@ -491,6 +862,36 @@ class BaseManager(object): raise F5ModuleError("Failed to delete the GTM pool") return True + def update_monitors_on_device(self): + """Updates the monitors string on a virtual server + + There is a long-standing bug in GTM virtual servers where the monitor value + is a string that includes braces. These braces cause the REST API to panic and + fail to update or create any resources that have an "at_least" or "require" + set of availability_requirements. + + This method exists to do a tmsh command to cause the update to take place on + the device. + + Preferably, this method can be removed and the bug be fixed. The API should + be working, obviously, but the more concerning issue is if tmsh commands change + over time, breaking this method. + """ + command = 'tmsh modify gtm pool {0} /{1}/{2} monitor {3}'.format( + self.want.type, self.want.partition, self.want.name, self.want.monitors + ) + output = self.client.api.tm.util.bash.exec_cmd( + 'run', + utilCmdArgs='-c "{0}"'.format(command) + ) + try: + if hasattr(output, 'commandResult'): + if len(output.commandResult.strip()) > 0: + raise F5ModuleError(output.commandResult) + except (AttributeError, NameError, LazyAttributesRequired): + pass + return True + class TypedManager(BaseManager): def __init__(self, *args, **kwargs): @@ -535,7 +936,10 @@ class TypedManager(BaseManager): name=self.want.name, partition=self.want.partition ) - result.modify(**params) + if params: + result.modify(**params) + if self.want.monitors: + self.update_monitors_on_device() def read_current_from_device(self): pools = self.client.api.tm.gtm.pools @@ -543,13 +947,18 @@ class TypedManager(BaseManager): resource = getattr(collection, self.want.type) result = resource.load( name=self.want.name, - partition=self.want.partition + partition=self.want.partition, + requests_params=dict( + params=dict( + expandSubcollections='true' + ) + ) ) result = result.attrs - return Parameters(params=result) + return ApiParameters(params=result) def create_on_device(self): - params = self.want.api_params() + params = self.changes.api_params() pools = self.client.api.tm.gtm.pools collection = getattr(pools, self.want.collection) resource = getattr(collection, self.want.type) @@ -558,6 +967,8 @@ class TypedManager(BaseManager): partition=self.want.partition, **params ) + if self.want.monitors: + self.update_monitors_on_device() def remove_from_device(self): pools = self.client.api.tm.gtm.pools @@ -586,6 +997,8 @@ class UntypedManager(BaseManager): partition=self.want.partition ) resource.modify(**params) + if self.want.monitors: + self.update_monitors_on_device() def read_current_from_device(self): resource = self.client.api.tm.gtm.pools.pool.load( @@ -593,15 +1006,17 @@ class UntypedManager(BaseManager): partition=self.want.partition ) result = resource.attrs - return Parameters(params=result) + return ApiParameters(params=result) def create_on_device(self): - params = self.want.api_params() + params = self.changes.api_params() self.client.api.tm.gtm.pools.pool.create( name=self.want.name, partition=self.want.partition, **params ) + if self.want.monitors: + self.update_monitors_on_device() def remove_from_device(self): resource = self.client.api.tm.gtm.pools.pool.load( @@ -656,7 +1071,35 @@ class ArgumentSpec(object): partition=dict( default='Common', fallback=(env_fallback, ['F5_PARTITION']) - ) + ), + members=dict( + type='list', + options=dict( + server=dict(required=True), + virtual_server=dict(required=True) + ) + ), + availability_requirements=dict( + type='dict', + options=dict( + type=dict( + choices=['all', 'at_least', 'require'], + required=True + ), + at_least=dict(type='int'), + number_of_probes=dict(type='int'), + number_of_probers=dict(type='int') + ), + mutually_exclusive=[ + ['at_least', 'number_of_probes'], + ['at_least', 'number_of_probers'], + ], + required_if=[ + ['type', 'at_least', ['at_least']], + ['type', 'require', ['number_of_probes', 'number_of_probers']] + ] + ), + monitors=dict(type='list'), ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) diff --git a/lib/ansible/modules/network/f5/bigip_hostname.py b/lib/ansible/modules/network/f5/bigip_hostname.py index 95081961bcf..9ce9bc688dc 100644 --- a/lib/ansible/modules/network/f5/bigip_hostname.py +++ b/lib/ansible/modules/network/f5/bigip_hostname.py @@ -18,7 +18,7 @@ module: bigip_hostname short_description: Manage the hostname of a BIG-IP description: - Manage the hostname of a BIG-IP. -version_added: "2.3" +version_added: 2.3 options: hostname: description: @@ -50,30 +50,23 @@ hostname: from ansible.module_utils.basic import AnsibleModule -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -100,13 +93,55 @@ class Parameters(AnsibleF5Parameters): return str(self._values['hostname']) +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + pass + + +class UsableChanges(Changes): + pass + + +class ReportableChanges(Changes): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.want = want + self.have = have + + def compare(self, param): + try: + result = getattr(self, param) + return result + except AttributeError: + return self.__default(param) + + def __default(self, param): + attr1 = getattr(self.want, param) + try: + attr2 = getattr(self.have, param) + if attr1 != attr2: + return attr1 + except AttributeError: + return attr1 + + class ModuleManager(object): def __init__(self, *args, **kwargs): self.module = kwargs.get('module', None) self.client = kwargs.get('client', None) - self.have = None - self.want = Parameters(params=self.module.params) - self.changes = Parameters() + self.have = ApiParameters() + self.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() def _set_changed_options(self): changed = {} @@ -114,18 +149,20 @@ class ModuleManager(object): if getattr(self.want, key) is not None: changed[key] = getattr(self.want, key) if changed: - self.changes = Parameters(params=changed) + self.changes = UsableChanges(params=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 - self.changes = Parameters(params=changed) + 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: + changed[k] = change if changed: + self.changes = UsableChanges(params=changed) return True return False @@ -137,15 +174,29 @@ class ModuleManager(object): except iControlUnexpectedHTTPError as e: raise F5ModuleError(str(e)) - changes = self.changes.to_return() + 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 read_current_from_device(self): resource = self.client.api.tm.sys.global_settings.load() result = resource.attrs - return Parameters(params=result) + + collection = self.client.api.tm.cm.devices.get_collection() + self_device = next((x.name for x in collection if x.selfDevice == "true"), None) + result['self_device'] = self_device + return ApiParameters(params=result) def update(self): self.have = self.read_current_from_device() @@ -166,9 +217,10 @@ class ModuleManager(object): params = self.want.api_params() resource = self.client.api.tm.sys.global_settings.load() resource.modify(**params) - self.client.api.tm.cm.devices.exec_cmd( - 'mv', name=self.have.hostname, target=self.want.hostname - ) + if self.have.self_device: + self.client.api.tm.cm.devices.exec_cmd( + 'mv', name=self.have.self_device, target=self.want.hostname + ) class ArgumentSpec(object): diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py index 8b151819388..0ff0d86ba3a 100644 --- a/lib/ansible/modules/network/f5/bigip_iapp_service.py +++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py @@ -18,7 +18,11 @@ module: bigip_iapp_service short_description: Manages TCL iApp services on a BIG-IP description: - Manages TCL iApp services on a BIG-IP. -version_added: "2.4" + - If you are looking for the API that is communicated with on the BIG-IP, + the one the is used is C(/mgmt/tm/sys/application/service/). There are a + couple of APIs in a BIG-IP that might seem like they are relevant to iApp + Services, but the API mentioned here is the one that is used. +version_added: 2.4 options: name: description: @@ -46,6 +50,7 @@ options: This option is equivalent to re-configuring the iApp if that template has changed. default: no + type: bool state: description: - When C(present), ensures that the iApp service is created and running. @@ -214,30 +219,25 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback from ansible.module_utils.six import iteritems -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name + from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name + from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -250,18 +250,16 @@ class Parameters(AnsibleF5Parameters): 'strictUpdates': 'strict_updates', 'trafficGroup': 'traffic_group', } + returnables = [] + api_attributes = [ 'tables', 'variables', 'template', 'lists', 'deviceGroup', 'inheritedDevicegroup', 'inheritedTrafficGroup', 'trafficGroup', 'strictUpdates' ] - updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group'] - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value + updatables = ['tables', 'variables', 'lists', 'strict_updates', 'traffic_group'] def to_return(self): result = {} @@ -388,7 +386,7 @@ class Parameters(AnsibleF5Parameters): def template(self): if self._values['template'] is None: return None - return self._fqdn_name(self._values['template']) + return fq_name(self.partition, self._values['template']) @template.setter def template(self, value): @@ -419,14 +417,14 @@ class Parameters(AnsibleF5Parameters): # Specifying the value overrides any associated value in the payload elif self._values['traffic_group']: - result = self._fqdn_name(self._values['traffic_group']) + result = fq_name(self.partition, self._values['traffic_group']) # This will be automatically `None` if it was not set by the # `parameters` setter elif self.trafficGroup: - result = self._fqdn_name(self.trafficGroup) + result = fq_name(self.partition, self.trafficGroup) else: - result = self._fqdn_name(self._values['traffic_group']) + result = fq_name(self.partition, self._values['traffic_group']) if result.startswith('/Common/'): return result else: diff --git a/lib/ansible/modules/network/f5/bigip_iapp_template.py b/lib/ansible/modules/network/f5/bigip_iapp_template.py index 5917fea0fc4..d1b2eb2f4f4 100644 --- a/lib/ansible/modules/network/f5/bigip_iapp_template.py +++ b/lib/ansible/modules/network/f5/bigip_iapp_template.py @@ -30,7 +30,7 @@ description: existing services are changed to consume that new template. As such, the ability to update templates in-place requires the C(force) option to be used. -version_added: "2.4" +version_added: 2.4 options: force: description: @@ -108,41 +108,31 @@ import uuid from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback -HAS_DEVEL_IMPORTS = False - try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError + from f5.utils.iapp_parser import NonextantTemplateNameException except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from f5.utils.iapp_parser import NonextantTemplateNameException except ImportError: HAS_F5SDK = False -try: - from f5.utils.iapp_parser import NonextantTemplateNameException -except ImportError: - HAS_F5SDK = False - try: from StringIO import StringIO except ImportError: @@ -151,6 +141,7 @@ except ImportError: class Parameters(AnsibleF5Parameters): api_attributes = [] + returnables = [] @property diff --git a/lib/ansible/modules/network/f5/bigip_iapplx_package.py b/lib/ansible/modules/network/f5/bigip_iapplx_package.py index 4fdcd5e80a4..fde8a717b6a 100644 --- a/lib/ansible/modules/network/f5/bigip_iapplx_package.py +++ b/lib/ansible/modules/network/f5/bigip_iapplx_package.py @@ -19,7 +19,7 @@ short_description: Manages Javascript iApp packages on a BIG-IP description: - Manages Javascript iApp packages on a BIG-IP. This module will allow you to deploy iAppLX packages to the BIG-IP and manage their lifecycle. -version_added: "2.5" +version_added: 2.5 options: package: description: @@ -88,33 +88,26 @@ import os import subprocess import time -from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule - -HAS_DEVEL_IMPORTS = False +from distutils.version import LooseVersion try: - # Sideband repository used for dev from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens - from library.module_utils.network.f5.common import fqdn_name from library.module_utils.network.f5.common import f5_argument_spec try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False - HAS_DEVEL_IMPORTS = True except ImportError: - # Upstream Ansible from ansible.module_utils.network.f5.bigip import HAS_F5SDK from ansible.module_utils.network.f5.bigip import F5Client from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens - from ansible.module_utils.network.f5.common import fqdn_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError diff --git a/lib/ansible/modules/network/f5/bigip_policy_rule.py b/lib/ansible/modules/network/f5/bigip_policy_rule.py index af09d718327..72877333c6a 100644 --- a/lib/ansible/modules/network/f5/bigip_policy_rule.py +++ b/lib/ansible/modules/network/f5/bigip_policy_rule.py @@ -179,7 +179,7 @@ conditions: type: complex contains: type: - description: The condition type + description: The condition type. returned: changed type: string sample: http_uri diff --git a/lib/ansible/modules/network/f5/bigip_static_route.py b/lib/ansible/modules/network/f5/bigip_static_route.py index 14230977af5..b89b12bcf52 100644 --- a/lib/ansible/modules/network/f5/bigip_static_route.py +++ b/lib/ansible/modules/network/f5/bigip_static_route.py @@ -66,6 +66,11 @@ options: - The route domain id of the system. When creating a new static route, if this value is not specified, a default value of C(0) will be used. - This value cannot be changed once it is set. + partition: + description: + - Device partition to manage resources on. + default: Common + version_added: 2.6 state: description: - When C(present), ensures that the static route exists. @@ -129,6 +134,11 @@ pool: returned: changed type: string sample: true +partition: + description: The partition that the static route was created on. + returned: changed + type: string + sample: Common description: description: Whether the banner is enabled or not. returned: changed @@ -144,6 +154,7 @@ reject: import re from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import env_fallback from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE try: @@ -575,6 +586,10 @@ class ArgumentSpec(object): default='present', choices=['absent', 'present'] ), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), route_domain=dict(type='int') ) self.argument_spec = {} diff --git a/test/units/modules/network/f5/fixtures/load_gtm_pool_a_with_members_1.json b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_with_members_1.json new file mode 100644 index 00000000000..ba2ac716f01 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_gtm_pool_a_with_members_1.json @@ -0,0 +1,94 @@ +{ + "kind": "tm:gtm:pool:a:astate", + "name": "foo", + "partition": "Common", + "fullPath": "/Common/foo", + "generation": 142, + "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo?expandSubcollections=true&ver=12.0.0", + "alternateMode": "round-robin", + "dynamicRatio": "disabled", + "enabled": true, + "fallbackIp": "any", + "fallbackMode": "return-to-dns", + "limitMaxBps": 0, + "limitMaxBpsStatus": "disabled", + "limitMaxConnections": 0, + "limitMaxConnectionsStatus": "disabled", + "limitMaxPps": 0, + "limitMaxPpsStatus": "disabled", + "loadBalancingMode": "round-robin", + "manualResume": "disabled", + "maxAnswersReturned": 1, + "monitor": "default", + "qosHitRatio": 5, + "qosHops": 0, + "qosKilobytesSecond": 3, + "qosLcs": 30, + "qosPacketRate": 1, + "qosRtt": 50, + "qosTopology": 0, + "qosVsCapacity": 0, + "qosVsScore": 0, + "ttl": 30, + "verifyMemberAvailability": "enabled", + "membersReference": { + "link": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members?ver=12.0.0", + "isSubcollection": true, + "items": [ + { + "kind": "tm:gtm:pool:a:members:membersstate", + "name": "server1:vs1", + "partition": "Common", + "fullPath": "/Common/server1:vs1", + "generation": 141, + "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs1?ver=12.0.0", + "enabled": true, + "limitMaxBps": 0, + "limitMaxBpsStatus": "disabled", + "limitMaxConnections": 0, + "limitMaxConnectionsStatus": "disabled", + "limitMaxPps": 0, + "limitMaxPpsStatus": "disabled", + "memberOrder": 0, + "monitor": "default", + "ratio": 1 + }, + { + "kind": "tm:gtm:pool:a:members:membersstate", + "name": "server1:vs2", + "partition": "Common", + "fullPath": "/Common/server1:vs2", + "generation": 142, + "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs1?ver=12.0.0", + "enabled": true, + "limitMaxBps": 0, + "limitMaxBpsStatus": "disabled", + "limitMaxConnections": 0, + "limitMaxConnectionsStatus": "disabled", + "limitMaxPps": 0, + "limitMaxPpsStatus": "disabled", + "memberOrder": 1, + "monitor": "/Common/tcp ", + "ratio": 1 + }, + { + "kind": "tm:gtm:pool:a:members:membersstate", + "name": "server1:vs3", + "partition": "Common", + "fullPath": "/Common/server1:vs3", + "generation": 141, + "selfLink": "https://localhost/mgmt/tm/gtm/pool/a/~Common~foo/members/~Common~server1:vs3?ver=12.0.0", + "enabled": true, + "limitMaxBps": 0, + "limitMaxBpsStatus": "disabled", + "limitMaxConnections": 0, + "limitMaxConnectionsStatus": "disabled", + "limitMaxPps": 0, + "limitMaxPpsStatus": "disabled", + "memberOrder": 2, + "monitor": "default", + "ratio": 1 + } + ] + } +} diff --git a/test/units/modules/network/f5/test_bigip_gtm_pool.py b/test/units/modules/network/f5/test_bigip_gtm_pool.py index 300594f4bcc..6ba12231430 100644 --- a/test/units/modules/network/f5/test_bigip_gtm_pool.py +++ b/test/units/modules/network/f5/test_bigip_gtm_pool.py @@ -20,17 +20,19 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_gtm_pool import Parameters - from library.bigip_gtm_pool import ModuleManager - from library.bigip_gtm_pool import ArgumentSpec - from library.bigip_gtm_pool import UntypedManager - from library.bigip_gtm_pool import TypedManager + from library.modules.bigip_gtm_pool import ApiParameters + from library.modules.bigip_gtm_pool import ModuleParameters + from library.modules.bigip_gtm_pool import ModuleManager + from library.modules.bigip_gtm_pool import ArgumentSpec + from library.modules.bigip_gtm_pool import UntypedManager + from library.modules.bigip_gtm_pool import TypedManager from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args except ImportError: try: - from ansible.modules.network.f5.bigip_gtm_pool import Parameters + from ansible.modules.network.f5.bigip_gtm_pool import ApiParameters + from ansible.modules.network.f5.bigip_gtm_pool import ModuleParameters from ansible.modules.network.f5.bigip_gtm_pool import ModuleManager from ansible.modules.network.f5.bigip_gtm_pool import ArgumentSpec from ansible.modules.network.f5.bigip_gtm_pool import UntypedManager @@ -73,7 +75,7 @@ class TestParameters(unittest.TestCase): fallback_ip='10.10.10.10', type='a' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.name == 'foo' assert p.preferred_lb_method == 'topology' assert p.alternate_lb_method == 'ratio' @@ -81,6 +83,20 @@ class TestParameters(unittest.TestCase): assert p.fallback_ip == '10.10.10.10' assert p.type == 'a' + def test_module_parameters_members(self): + args = dict( + partition='Common', + members=[ + dict( + server='foo', + virtual_server='bar' + ) + ] + ) + p = ModuleParameters(params=args) + assert len(p.members) == 1 + assert p.members[0] == '/Common/foo:bar' + def test_api_parameters(self): args = dict( name='foo', @@ -89,13 +105,21 @@ class TestParameters(unittest.TestCase): fallbackMode='fewest-hops', fallbackIp='10.10.10.10' ) - p = Parameters(params=args) + p = ApiParameters(params=args) assert p.name == 'foo' assert p.preferred_lb_method == 'topology' assert p.alternate_lb_method == 'ratio' assert p.fallback_lb_method == 'fewest-hops' assert p.fallback_ip == '10.10.10.10' + def test_api_parameters_members(self): + args = load_fixture('load_gtm_pool_a_with_members_1.json') + p = ApiParameters(params=args) + assert len(p.members) == 3 + assert p.members[0] == '/Common/server1:vs1' + assert p.members[1] == '/Common/server1:vs2' + assert p.members[2] == '/Common/server1:vs3' + class TestUntypedManager(unittest.TestCase): @@ -148,7 +172,7 @@ class TestUntypedManager(unittest.TestCase): supports_check_mode=self.spec.supports_check_mode ) - current = Parameters(params=load_fixture('load_gtm_pool_untyped_default.json')) + current = ApiParameters(params=load_fixture('load_gtm_pool_untyped_default.json')) # Override methods in the specific type of manager tm = UntypedManager(module=module) @@ -252,7 +276,7 @@ class TestTypedManager(unittest.TestCase): supports_check_mode=self.spec.supports_check_mode ) - current = Parameters(params=load_fixture('load_gtm_pool_a_default.json')) + current = ApiParameters(params=load_fixture('load_gtm_pool_a_default.json')) # Override methods in the specific type of manager tm = TypedManager(module=module) diff --git a/test/units/modules/network/f5/test_bigip_hostname.py b/test/units/modules/network/f5/test_bigip_hostname.py index 47d2d130127..1e5c3559960 100644 --- a/test/units/modules/network/f5/test_bigip_hostname.py +++ b/test/units/modules/network/f5/test_bigip_hostname.py @@ -20,15 +20,17 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_hostname import Parameters - from library.bigip_hostname import ModuleManager - from library.bigip_hostname import ArgumentSpec + from library.modules.bigip_hostname import ApiParameters + from library.modules.bigip_hostname import ModuleParameters + from library.modules.bigip_hostname import ModuleManager + from library.modules.bigip_hostname import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args except ImportError: try: - from ansible.modules.network.f5.bigip_hostname import Parameters + from ansible.modules.network.f5.bigip_hostname import ApiParameters + from ansible.modules.network.f5.bigip_hostname import ModuleParameters from ansible.modules.network.f5.bigip_hostname import ModuleManager from ansible.modules.network.f5.bigip_hostname import ArgumentSpec from ansible.module_utils.network.f5.common import F5ModuleError @@ -64,7 +66,7 @@ class TestParameters(unittest.TestCase): args = dict( hostname='foo.internal.com' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.hostname == 'foo.internal.com' @@ -83,8 +85,8 @@ class TestManager(unittest.TestCase): # Configure the parameters that would be returned by querying the # remote device - current = Parameters( - dict( + current = ApiParameters( + params=dict( hostname='foo.internal.com' ) ) diff --git a/test/units/modules/network/f5/test_bigip_iapp_service.py b/test/units/modules/network/f5/test_bigip_iapp_service.py index ef0f4b5d660..a7ac029eaf7 100644 --- a/test/units/modules/network/f5/test_bigip_iapp_service.py +++ b/test/units/modules/network/f5/test_bigip_iapp_service.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_iapp_service import Parameters - from library.bigip_iapp_service import ModuleManager - from library.bigip_iapp_service import ArgumentSpec + from library.modules.bigip_iapp_service import Parameters + from library.modules.bigip_iapp_service import ModuleManager + from library.modules.bigip_iapp_service import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_iapp_template.py b/test/units/modules/network/f5/test_bigip_iapp_template.py index 19b571e9c11..1031283111f 100644 --- a/test/units/modules/network/f5/test_bigip_iapp_template.py +++ b/test/units/modules/network/f5/test_bigip_iapp_template.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_iapp_template import Parameters - from library.bigip_iapp_template import ModuleManager - from library.bigip_iapp_template import ArgumentSpec + from library.modules.bigip_iapp_template import Parameters + from library.modules.bigip_iapp_template import ModuleManager + from library.modules.bigip_iapp_template import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args diff --git a/test/units/modules/network/f5/test_bigip_iapplx_package.py b/test/units/modules/network/f5/test_bigip_iapplx_package.py index b8df7c238fb..05ed42a79b2 100644 --- a/test/units/modules/network/f5/test_bigip_iapplx_package.py +++ b/test/units/modules/network/f5/test_bigip_iapplx_package.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_iapp_template import Parameters - from library.bigip_iapp_template import ModuleManager - from library.bigip_iapp_template import ArgumentSpec + from library.modules.bigip_iapp_template import Parameters + from library.modules.bigip_iapp_template import ModuleManager + from library.modules.bigip_iapp_template import ArgumentSpec from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import iControlUnexpectedHTTPError from test.unit.modules.utils import set_module_args