From 256b5535ece70b76948962113c64615a9bc581d5 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Tue, 24 Apr 2018 19:49:52 -0700 Subject: [PATCH] Various fixes to F5 modules (#39255) * Adds gnat provisioning to bigip_provision * Adds special handling for AFM in bigip_provision * Add device rebooting for provisioning as necessary * Refactored route domain module to be inline with current f5 conventions * Minor refactors across modules --- .../modules/network/f5/bigip_provision.py | 273 ++++++- .../modules/network/f5/bigip_qkview.py | 16 +- .../modules/network/f5/bigip_remote_syslog.py | 8 +- .../modules/network/f5/bigip_routedomain.py | 770 ++++++++++-------- .../network/f5/bigip_security_address_list.py | 28 +- .../network/f5/bigip_security_port_list.py | 20 +- test/sanity/validate-modules/ignore.txt | 4 - .../f5/fixtures/load_net_route_domain_1.json | 36 + .../network/f5/test_bigip_provision.py | 8 +- .../modules/network/f5/test_bigip_qkview.py | 10 +- .../network/f5/test_bigip_remote_syslog.py | 10 +- .../network/f5/test_bigip_routedomain.py | 125 +++ .../f5/test_bigip_security_address_list.py | 9 +- .../f5/test_bigip_security_port_list.py | 9 +- 14 files changed, 845 insertions(+), 481 deletions(-) create mode 100644 test/units/modules/network/f5/fixtures/load_net_route_domain_1.json diff --git a/lib/ansible/modules/network/f5/bigip_provision.py b/lib/ansible/modules/network/f5/bigip_provision.py index 2a3cb350f5..23662848b0 100644 --- a/lib/ansible/modules/network/f5/bigip_provision.py +++ b/lib/ansible/modules/network/f5/bigip_provision.py @@ -19,9 +19,9 @@ short_description: Manage BIG-IP module provisioning description: - Manage BIG-IP module provisioning. This module will only provision at the standard levels of Dedicated, Nominal, and Minimum. -version_added: "2.4" +version_added: 2.4 options: - name: + module: description: - The module to provision in BIG-IP. required: true @@ -31,6 +31,7 @@ options: - apm - asm - avr + - cgnat - fps - gtm - ilx @@ -41,7 +42,7 @@ options: - swg - vcmp aliases: - - module + - name level: description: - Sets the provisioning level for the requested modules. Changing the @@ -49,6 +50,8 @@ options: For example, changing one module to C(dedicated) requires setting all others to C(none). Setting the level of a module to C(none) means that the module is not activated. + - This parameter is not relevant to C(cgnat) and will not be applied to the + C(cgnat) module. default: nominal choices: - dedicated @@ -104,49 +107,40 @@ import time 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 + from f5.bigip.contexts import TransactionContextManager + 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 f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from f5.bigip.contexts import TransactionContextManager + from f5.sdk_exception import LazyAttributesRequired except ImportError: HAS_F5SDK = False -try: - from f5.bigip.contexts import TransactionContextManager - from f5.sdk_exception import LazyAttributesRequired -except ImportError: - HAS_F5SDK = False - class Parameters(AnsibleF5Parameters): api_attributes = ['level'] returnables = ['level'] - updatables = ['level'] + updatables = ['level', 'cgnat'] def to_return(self): result = {} @@ -162,27 +156,85 @@ class Parameters(AnsibleF5Parameters): def level(self): if self._values['level'] is None: return None + if self.state == 'absent': + return 'none' return str(self._values['level']) +class ApiParameters(Parameters): + pass + + +class ModuleParameters(Parameters): + pass + + +class Changes(Parameters): + pass + + +class UsableChanges(Parameters): + pass + + +class ReportableChanges(Parameters): + 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: + result = self.__default(param) + return result + + 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 cgnat(self): + if self.want.module == 'cgnat': + if self.want.state == 'absent' and self.have.enabled is True: + return True + if self.want.state == 'present' and self.have.disabled is True: + return True + + 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.want = ModuleParameters(params=self.module.params) + self.changes = UsableChanges() 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 + 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 = Parameters(params=changed) + self.changes = UsableChanges(params=changed) return True return False @@ -196,7 +248,7 @@ class ModuleManager(object): try: if state == "present": - changed = self.update() + changed = self.present() elif state == "absent": changed = self.absent() except iControlUnexpectedHTTPError as e: @@ -207,14 +259,36 @@ class ModuleManager(object): result.update(dict(changed=changed)) return result - def exists(self): - provision = self.client.api.tm.sys.provision - resource = getattr(provision, self.want.module) - resource = resource.load() - result = resource.attrs - if str(result['level']) == 'none': + def present(self): + if self.exists(): return False - return True + return self.update() + + def exists(self): + if self.want.module == 'cgnat': + resource = self.client.api.tm.sys.feature_module.cgnat.load() + if resource.disabled is True: + return False + elif resource.enabled is True: + return True + + try: + for x in range(0, 5): + provision = self.client.api.tm.sys.provision + resource = getattr(provision, self.want.module) + resource = resource.load() + result = resource.attrs + if str(result['level']) != 'none' and self.want.level == 'none': + return True + if str(result['level']) == 'none' and self.want.level == 'none': + return False + if str(result['level']) == self.want.level: + return True + return False + except Exception as ex: + if 'not registered' in str(ex): + return False + time.sleep(1) def update(self): self.have = self.read_current_from_device() @@ -223,7 +297,10 @@ class ModuleManager(object): if self.module.check_mode: return True - self.update_on_device() + result = self.update_on_device() + if self.want.module == 'cgnat': + return result + self._wait_for_module_provisioning() if self.want.module == 'vcmp': @@ -232,8 +309,55 @@ class ModuleManager(object): if self.want.module == 'asm': self._wait_for_asm_ready() + if self.want.module == 'afm': + self._wait_for_afm_ready() return True + def should_reboot(self): + for x in range(0, 24): + try: + resource = self.client.api.tm.sys.dbs.db.load(name='provision.action') + if resource.value == 'reboot': + return True + elif resource.value == 'none': + time.sleep(5) + except Exception: + time.sleep(5) + return False + + def reboot_device(self): + nops = 0 + last_reboot = self._get_last_reboot() + + try: + output = self.client.api.tm.util.bash.exec_cmd( + 'run', + utilCmdArgs='-c "/sbin/reboot"' + ) + if hasattr(output, 'commandResult'): + return str(output.commandResult) + except Exception: + pass + + # Sleep a little to let rebooting take effect + time.sleep(20) + + while nops < 6: + try: + self.client.reconnect() + next_reboot = self._get_last_reboot() + if next_reboot is None: + nops = 0 + if next_reboot == last_reboot: + nops = 0 + else: + nops += 1 + except Exception as ex: + # This can be caused by restjavad restarting. + pass + time.sleep(10) + return None + def should_update(self): result = self._update_changed_options() if result: @@ -241,11 +365,22 @@ class ModuleManager(object): return False def update_on_device(self): - if self.want.level == 'dedicated': + if self.want.module == 'cgnat': + if self.changes.cgnat: + return self.provision_cgnat_on_device() + return False + elif self.want.level == 'dedicated': self.provision_dedicated_on_device() else: self.provision_non_dedicated_on_device() + def provision_cgnat_on_device(self): + resource = self.client.api.tm.sys.feature_module.cgnat.load() + resource.modify( + enabled=True + ) + return True + def provision_dedicated_on_device(self): params = self.want.api_params() tx = self.client.api.tm.transactions.transaction @@ -269,11 +404,15 @@ class ModuleManager(object): resource.update(**params) def read_current_from_device(self): - provision = self.client.api.tm.sys.provision - resource = getattr(provision, str(self.want.module)) - resource = resource.load() - result = resource.attrs - return Parameters(params=result) + if self.want.module == 'cgnat': + resource = self.client.api.tm.sys.feature_module.cgnat.load() + result = resource.attrs + else: + provision = self.client.api.tm.sys.provision + resource = getattr(provision, str(self.want.module)) + resource = resource.load() + result = resource.attrs + return ApiParameters(params=result) def absent(self): if self.exists(): @@ -283,7 +422,9 @@ class ModuleManager(object): def remove(self): if self.module.check_mode: return True - self.remove_from_device() + result = self.remove_from_device() + if self.want.module == 'cgnat': + return result self._wait_for_module_provisioning() # For vCMP, because it has to reboot, we also wait for mcpd to become available @@ -293,16 +434,40 @@ class ModuleManager(object): self._wait_for_reboot() self._wait_for_module_provisioning() + if self.should_reboot(): + self.save_on_device() + self.reboot_device() + self._wait_for_module_provisioning() + if self.exists(): raise F5ModuleError("Failed to de-provision the module") return True + def save_on_device(self): + command = 'tmsh save sys config' + self.client.api.tm.util.bash.exec_cmd( + 'run', + utilCmdArgs='-c "{0}"'.format(command) + ) + def remove_from_device(self): + if self.want.module == 'cgnat': + if self.changes.cgnat: + return self.deprovision_cgnat_on_device() + return False + provision = self.client.api.tm.sys.provision resource = getattr(provision, self.want.module) resource = resource.load() resource.update(level='none') + def deprovision_cgnat_on_device(self): + resource = self.client.api.tm.sys.feature_module.cgnat.load() + resource.modify( + disabled=True + ) + return True + def _wait_for_module_provisioning(self): # To prevent things from running forever, the hack is to check # for mprov's status twice. If mprov is finished, then in most @@ -370,6 +535,26 @@ class ModuleManager(object): restarted_asm = True time.sleep(5) + def _wait_for_afm_ready(self): + """Waits specifically for AFM + + AFM can take longer to actually start up than all the previous checks take. + This check here is specifically waiting for the Security API to stop raising + errors. + :return: + """ + nops = 0 + while nops < 3: + try: + security = self.client.api.tm.security.get_collection() + if len(security) >= 0: + nops += 1 + else: + nops = 0 + except Exception as ex: + pass + time.sleep(5) + def _restart_asm(self): try: self.client.api.tm.util.bash.exec_cmd( @@ -427,7 +612,7 @@ class ArgumentSpec(object): choices=[ 'afm', 'am', 'sam', 'asm', 'avr', 'fps', 'gtm', 'lc', 'ltm', 'pem', 'swg', 'ilx', - 'apm', 'vcmp' + 'apm', 'vcmp', 'cgnat' ], aliases=['name'] ), diff --git a/lib/ansible/modules/network/f5/bigip_qkview.py b/lib/ansible/modules/network/f5/bigip_qkview.py index acce31c8c3..bfcd35640a 100644 --- a/lib/ansible/modules/network/f5/bigip_qkview.py +++ b/lib/ansible/modules/network/f5/bigip_qkview.py @@ -22,7 +22,7 @@ description: when dealing with F5 support. It may be required that you upload this qkview to the supported channels during resolution of an SRs that you may have opened. -version_added: "2.4" +version_added: 2.4 options: filename: description: @@ -46,7 +46,7 @@ options: complete_information: description: - Include complete information in the qkview. - default: yes + default: no type: bool exclude_core: description: @@ -105,30 +105,23 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import string_types from distutils.version import LooseVersion -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 @@ -434,7 +427,10 @@ class ArgumentSpec(object): type='bool' ), exclude=dict( - type='list' + type='list', + choices=[ + 'all', 'audit', 'secure', 'bash_history' + ] ), dest=dict( type='path', diff --git a/lib/ansible/modules/network/f5/bigip_remote_syslog.py b/lib/ansible/modules/network/f5/bigip_remote_syslog.py index fac67ba99c..6f189bcdc0 100644 --- a/lib/ansible/modules/network/f5/bigip_remote_syslog.py +++ b/lib/ansible/modules/network/f5/bigip_remote_syslog.py @@ -13,6 +13,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'supported_by': 'community'} DOCUMENTATION = r''' +--- module: bigip_remote_syslog short_description: Manipulate remote syslog settings on a BIG-IP description: @@ -93,30 +94,23 @@ import re from ansible.module_utils.basic import AnsibleModule 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 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_routedomain.py b/lib/ansible/modules/network/f5/bigip_routedomain.py index 9274bbe60d..2f19cd51bc 100644 --- a/lib/ansible/modules/network/f5/bigip_routedomain.py +++ b/lib/ansible/modules/network/f5/bigip_routedomain.py @@ -18,13 +18,11 @@ module: bigip_routedomain short_description: Manage route domains on a BIG-IP description: - Manage route domains on a BIG-IP. -version_added: "2.2" +version_added: 2.2 options: name: description: - The name of the route domain. - - When creating a new route domain, if this value is not specified, then the - value of C(id) will be used for it. version_added: 2.5 bwc_policy: description: @@ -51,8 +49,8 @@ options: become a required parameter. parent: description: - Specifies the route domain the system searches when it cannot - find a route in the configured domain. + - Specifies the route domain the system searches when it cannot + find a route in the configured domain. partition: description: - Partition to create the route domain on. Partitions cannot be updated @@ -83,14 +81,11 @@ options: - absent strict: description: - - Specifies whether the system enforces cross-routing restrictions - or not. - choices: - - enabled - - disabled + - Specifies whether the system enforces cross-routing restrictions or not. + type: bool vlans: description: - - VLANs for the system to use in the route domain + - VLANs for the system to use in the route domain. extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) @@ -122,82 +117,79 @@ EXAMPLES = r''' RETURN = r''' id: - description: The ID of the route domain that was changed + description: The ID of the route domain that was changed. returned: changed type: int sample: 2 description: - description: The description of the route domain + description: The description of the route domain. returned: changed type: string sample: route domain foo strict: - description: The new strict isolation setting + description: The new strict isolation setting. returned: changed type: string sample: enabled parent: - description: The new parent route domain + description: The new parent route domain. returned: changed type: int sample: 0 vlans: - description: List of new VLANs the route domain is applied to + description: List of new VLANs the route domain is applied to. returned: changed type: list sample: ['/Common/http-tunnel', '/Common/socks-tunnel'] routing_protocol: - description: List of routing protocols applied to the route domain + description: List of routing protocols applied to the route domain. returned: changed type: list sample: ['bfd', 'bgp'] bwc_policy: - description: The new bandwidth controller + description: The new bandwidth controller. returned: changed type: string sample: /Common/foo connection_limit: - description: The new connection limit for the route domain + description: The new connection limit for the route domain. returned: changed type: int sample: 100 flow_eviction_policy: - description: The new eviction policy to use with this route domain + description: The new eviction policy to use with this route domain. returned: changed type: string sample: /Common/default-eviction-policy service_policy: - description: The new service policy to use with this route domain + description: The new service policy to use with this route domain. returned: changed type: string sample: /Common-my-service-policy ''' -try: - from f5.bigip import ManagementRoot -except ImportError: - pass # Handled via f5_utils.HAS_F5SDK - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -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 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 fq_name from ansible.module_utils.network.f5.common import f5_argument_spec try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError @@ -205,375 +197,429 @@ except ImportError: HAS_F5SDK = False -PROTOCOLS = [ - 'BFD', 'BGP', 'IS-IS', 'OSPFv2', 'OSPFv3', 'PIM', 'RIP', 'RIPng' -] +class Parameters(AnsibleF5Parameters): + api_map = { + 'connectionLimit': 'connection_limit', + 'servicePolicy': 'service_policy', + 'bwcPolicy': 'bwc_policy', + 'flowEvictionPolicy': 'flow_eviction_policy', + 'routingProtocol': 'routing_protocol' + } -STRICTS = ['enabled', 'disabled'] + api_attributes = [ + 'connectionLimit', + 'description', + 'strict', + 'parent', + 'servicePolicy', + 'bwcPolicy', + 'flowEvictionPolicy', + 'routingProtocol', + 'vlans', + 'id' + ] + + returnables = [ + 'description', + 'strict', + 'parent', + 'service_policy', + 'bwc_policy', + 'flow_eviction_policy', + 'routing_protocol', + 'vlans', + 'connection_limit', + 'id' + ] + + updatables = [ + 'description', + 'strict', + 'parent', + 'service_policy', + 'bwc_policy', + 'flow_eviction_policy', + 'routing_protocol', + 'vlans', + 'connection_limit', + 'id' + ] + + @property + def connection_limit(self): + if self._values['connection_limit'] is None: + return None + return int(self._values['connection_limit']) + + @property + def id(self): + if self._values['id'] is None: + return None + return int(self._values['id']) -class BigIpRouteDomain(object): +class ApiParameters(Parameters): + @property + def strict(self): + if self._values['strict'] is None: + return None + if self._values['strict'] == 'enabled': + return True + return False + + @property + def domains(self): + domains = self.read_domains_from_device() + result = [x.fullPath for x in domains] + return result + + def read_domains_from_device(self): + collection = self.client.api.tm.net.route_domains.get_collection() + return collection + + +class ModuleParameters(Parameters): + @property + def bwc_policy(self): + if self._values['bwc_policy'] is None: + return None + return fq_name(self.partition, self._values['bwc_policy']) + + @property + def flow_eviction_policy(self): + if self._values['flow_eviction_policy'] is None: + return None + return fq_name(self.partition, self._values['flow_eviction_policy']) + + @property + def service_policy(self): + if self._values['service_policy'] is None: + return None + return fq_name(self.partition, self._values['service_policy']) + + @property + def parent(self): + if self._values['parent'] is None: + return None + result = fq_name(self.partition, self._values['parent']) + return result + + @property + def vlans(self): + if self._values['vlans'] is None: + return None + if len(self._values['vlans']) == 1 and self._values['vlans'][0] == '': + return '' + return [fq_name(self.partition, x) for x in self._values['vlans']] + + @property + def name(self): + if self._values['name'] is None: + return str(self.id) + return self._values['name'] + + @property + def routing_protocol(self): + if self._values['routing_protocol'] is None: + return None + if len(self._values['routing_protocol']) == 1 and self._values['routing_protocol'][0] == '': + return '' + return self._values['routing_protocol'] + + +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 strict(self): + if self._values['strict'] is None: + return None + if self._values['strict']: + return 'enabled' + return 'disabled' + + +class ReportableChanges(Changes): + @property + def strict(self): + if self._values['strict'] is None: + return None + if self._values['strict'] == 'enabled': + return 'yes' + return 'no' + + +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 routing_protocol(self): + if self.want.routing_protocol is None: + return None + if self.want.routing_protocol == '' and self.have.routing_protocol is None: + return None + if self.want.routing_protocol == '' and len(self.have.routing_protocol) > 0: + return [] + if self.have.routing_protocol is None: + return self.want.routing_protocol + want = set(self.want.routing_protocol) + have = set(self.have.routing_protocol) + if want != have: + return list(want) + + @property + def vlans(self): + if self.want.vlans is None: + return None + if self.want.vlans == '' and self.have.vlans is None: + return None + if self.want.vlans == '' and len(self.have.vlans) > 0: + return [] + if self.have.vlans is None: + return self.want.vlans + want = set(self.want.vlans) + have = set(self.have.vlans) + if want != have: + return list(want) + + +class ModuleManager(object): def __init__(self, *args, **kwargs): - if not HAS_F5SDK: - raise F5ModuleError("The python f5-sdk module is required") + self.module = kwargs.get('module', None) + self.client = kwargs.get('client', None) + self.want = ModuleParameters(params=self.module.params, client=self.client) + self.have = ApiParameters(client=self.client) + self.changes = UsableChanges() - # The params that change in the module - self.cparams = dict() + 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) - # Stores the params that are sent to the module - self.params = kwargs - self.api = ManagementRoot(kwargs['server'], - kwargs['user'], - kwargs['password'], - port=kwargs['server_port'], - token=True) - - def absent(self): - if not self.exists(): - return False - - if self.params['check_mode']: + 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 - if self.params['name'] is None: - self.params['name'] = str(self.params['id']) - - rd = self.api.tm.net.route_domains.route_domain.load( - name=self.params['name'], - partition=self.params['partition'] - ) - rd.delete() - - if self.exists(): - raise F5ModuleError("Failed to delete the route domain") - else: + def should_update(self): + result = self._update_changed_options() + if result: return True + return False + + def exec_module(self): + changed = False + result = dict() + state = self.want.state + + try: + if state == "present": + changed = self.present() + elif state == "absent": + changed = self.absent() + except iControlUnexpectedHTTPError as e: + raise F5ModuleError(str(e)) + + reportable = ReportableChanges(params=self.changes.to_return()) + changes = reportable.to_return() + result.update(**changes) + result.update(dict(changed=changed)) + self._announce_deprecations(result) + return result + + def _announce_deprecations(self, result): + warnings = result.pop('__warnings', []) + for warning in warnings: + self.client.module.deprecate( + msg=warning['msg'], + version=warning['version'] + ) def present(self): if self.exists(): return self.update() else: - if self.params['check_mode']: - return True return self.create() - def read(self): - """Read information and transform it - - The values that are returned by BIG-IP in the f5-sdk can have encoding - attached to them as well as be completely missing in some cases. - - Therefore, this method will transform the data from the BIG-IP into a - format that is more easily consumable by the rest of the class and the - parameters that are supported by the module. - """ - p = dict() - - if self.params['name'] is None: - self.params['name'] = str(self.params['id']) - - r = self.api.tm.net.route_domains.route_domain.load( - name=self.params['name'], - partition=self.params['partition'] + def exists(self): + result = self.client.api.tm.net.route_domains.route_domain.exists( + name=self.want.name, + partition=self.want.partition ) - - p['id'] = int(r.id) - p['name'] = str(r.name) - - if hasattr(r, 'connectionLimit'): - p['connection_limit'] = int(r.connectionLimit) - if hasattr(r, 'description'): - p['description'] = str(r.description) - if hasattr(r, 'strict'): - p['strict'] = str(r.strict) - if hasattr(r, 'parent'): - p['parent'] = r.parent - if hasattr(r, 'vlans'): - p['vlans'] = list(set([str(x) for x in r.vlans])) - if hasattr(r, 'routingProtocol'): - p['routing_protocol'] = list(set([str(x) for x in r.routingProtocol])) - if hasattr(r, 'flowEvictionPolicy'): - p['flow_eviction_policy'] = str(r.flowEvictionPolicy) - if hasattr(r, 'bwcPolicy'): - p['bwc_policy'] = str(r.bwcPolicy) - if hasattr(r, 'servicePolicy'): - p['service_policy'] = str(r.servicePolicy) - return p - - def domains(self): - result = [] - - domains = self.api.tm.net.route_domains.get_collection() - for domain in domains: - # Just checking for the addition of the partition here for - # different versions of BIG-IP - if '/' + self.params['partition'] + '/' in domain.name: - result.append(domain.name) - else: - full_name = '/%s/%s' % (self.params['partition'], domain.name) - result.append(full_name) return result - def create(self): - params = dict() - params['id'] = self.params['id'] - params['name'] = self.params['name'] - params['partition'] = self.params['partition'] + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.want.parent and self.want.parent not in self.have.domains: + raise F5ModuleError( + "The parent route domain was not found." + ) + if self.module.check_mode: + return True + self.update_on_device() + return True - if params['name'] is None: - self.params['name'] = str(self.params['id']) - elif params['id'] is None: + def remove(self): + if self.module.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource.") + return True + + def create(self): + if self.want.id is None: raise F5ModuleError( "The 'id' parameter is required when creating new route domains." ) - - partition = self.params['partition'] - description = self.params['description'] - strict = self.params['strict'] - parent = self.params['parent'] - bwc_policy = self.params['bwc_policy'] - vlans = self.params['vlans'] - routing_protocol = self.params['routing_protocol'] - connection_limit = self.params['connection_limit'] - flow_eviction_policy = self.params['flow_eviction_policy'] - service_policy = self.params['service_policy'] - - if description is not None: - params['description'] = description - - if strict is not None: - params['strict'] = strict - - if parent is not None: - parent = '/%s/%s' % (partition, parent) - if parent in self.domains(): - params['parent'] = parent - else: - raise F5ModuleError( - "The parent route domain was not found" - ) - - if bwc_policy is not None: - policy = '/%s/%s' % (partition, bwc_policy) - params['bwcPolicy'] = policy - - if vlans is not None: - params['vlans'] = [] - for vlan in vlans: - vname = '/%s/%s' % (partition, vlan) - params['vlans'].append(vname) - - if routing_protocol is not None: - params['routingProtocol'] = [] - for protocol in routing_protocol: - if protocol in PROTOCOLS: - params['routingProtocol'].append(protocol) - else: - raise F5ModuleError( - "routing_protocol must be one of: %s" % (PROTOCOLS) - ) - - if connection_limit is not None: - params['connectionLimit'] = connection_limit - - if flow_eviction_policy is not None: - policy = '/%s/%s' % (partition, flow_eviction_policy) - params['flowEvictionPolicy'] = policy - - if service_policy is not None: - policy = '/%s/%s' % (partition, service_policy) - params['servicePolicy'] = policy - - self.api.tm.net.route_domains.route_domain.create(**params) - exists = self.api.tm.net.route_domains.route_domain.exists( - name=self.params['name'], - partition=self.params['partition'] - ) - - if exists: - return True - else: + if self.want.parent and self.want.parent not in self.have.domains: raise F5ModuleError( - "An error occurred while creating the route domain" + "The parent route domain was not found." ) - - def update(self): - changed = False - params = dict() - current = self.read() - - if self.params['name'] is None: - self.params['name'] = str(self.params['id']) - - check_mode = self.params['check_mode'] - partition = self.params['partition'] - description = self.params['description'] - strict = self.params['strict'] - parent = self.params['parent'] - bwc_policy = self.params['bwc_policy'] - vlans = self.params['vlans'] - routing_protocol = self.params['routing_protocol'] - connection_limit = self.params['connection_limit'] - flow_eviction_policy = self.params['flow_eviction_policy'] - service_policy = self.params['service_policy'] - - if description is not None: - if 'description' in current: - if description != current['description']: - params['description'] = description - else: - params['description'] = description - - if strict is not None: - if strict != current['strict']: - params['strict'] = strict - - if parent is not None: - parent = '/%s/%s' % (partition, parent) - if 'parent' in current: - if parent != current['parent']: - params['parent'] = parent - else: - params['parent'] = parent - - if bwc_policy is not None: - policy = '/%s/%s' % (partition, bwc_policy) - if 'bwc_policy' in current: - if policy != current['bwc_policy']: - params['bwcPolicy'] = policy - else: - params['bwcPolicy'] = policy - - if vlans is not None: - tmp = set() - for vlan in vlans: - vname = '/%s/%s' % (partition, vlan) - tmp.add(vname) - tmp = list(tmp) - if 'vlans' in current: - if tmp != current['vlans']: - params['vlans'] = tmp - else: - params['vlans'] = tmp - - if routing_protocol is not None: - tmp = set() - for protocol in routing_protocol: - if protocol in PROTOCOLS: - tmp.add(protocol) - else: - raise F5ModuleError( - "routing_protocol must be one of: %s" % (PROTOCOLS) - ) - tmp = list(tmp) - if 'routing_protocol' in current: - if tmp != current['routing_protocol']: - params['routingProtocol'] = tmp - else: - params['routingProtocol'] = tmp - - if connection_limit is not None: - if connection_limit != current['connection_limit']: - params['connectionLimit'] = connection_limit - - if flow_eviction_policy is not None: - policy = '/%s/%s' % (partition, flow_eviction_policy) - if 'flow_eviction_policy' in current: - if policy != current['flow_eviction_policy']: - params['flowEvictionPolicy'] = policy - else: - params['flowEvictionPolicy'] = policy - - if service_policy is not None: - policy = '/%s/%s' % (partition, service_policy) - if 'service_policy' in current: - if policy != current['service_policy']: - params['servicePolicy'] = policy - else: - params['servicePolicy'] = policy - - if params: - changed = True - self.cparams = camel_dict_to_snake_dict(params) - if check_mode: - return changed - else: - return changed - - try: - rd = self.api.tm.net.route_domains.route_domain.load( - name=self.params['name'], - partition=self.params['partition'] - ) - rd.update(**params) - rd.refresh() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(e) - + self._set_changed_options() + if self.module.check_mode: + return True + self.create_on_device() return True - def exists(self): - if self.params['name'] is None: - self.params['name'] = str(self.params['id']) - return self.api.tm.net.route_domains.route_domain.exists( - name=self.params['name'], - partition=self.params['partition'] + def create_on_device(self): + params = self.changes.api_params() + self.client.api.tm.net.route_domains.route_domain.create( + name=self.want.name, + partition=self.want.partition, + **params ) - def exec_module(self): - result = dict() - state = self.params['state'] + def update_on_device(self): + params = self.changes.api_params() + resource = self.client.api.tm.net.route_domains.route_domain.load( + name=self.want.name, + partition=self.want.partition + ) + resource.modify(**params) - if state == "present": - changed = self.present() - current = self.read() - result.update(current) - elif state == "absent": - changed = self.absent() + def absent(self): + if self.exists(): + return self.remove() + return False - result.update(dict(changed=changed)) - return result + def remove_from_device(self): + resource = self.client.api.tm.net.route_domains.route_domain.load( + name=self.want.name, + partition=self.want.partition + ) + if resource: + resource.delete() + + def read_current_from_device(self): + resource = self.client.api.tm.net.route_domains.route_domain.load( + name=self.want.name, + partition=self.want.partition + ) + result = resource.attrs + return ApiParameters(params=result, client=self.client) + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + argument_spec = dict( + name=dict(), + id=dict(type='int'), + description=dict(), + strict=dict(type='bool'), + parent=dict(type='int'), + vlans=dict(type='list'), + routing_protocol=dict( + type='list', + choices=['BFD', 'BGP', 'IS-IS', 'OSPFv2', 'OSPFv3', 'PIM', 'RIP', 'RIPng'] + ), + bwc_policy=dict(), + connection_limit=dict(type='int'), + flow_eviction_policy=dict(), + service_policy=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + state=dict( + default='present', + choices=['present', 'absent'] + ) + ) + self.argument_spec = {} + self.argument_spec.update(f5_argument_spec) + self.argument_spec.update(argument_spec) + self.required_one_of = [ + ['name', 'id'] + ] def main(): - argument_spec = f5_argument_spec - - meta_args = dict( - name=dict(), - id=dict(type='int'), - description=dict(), - strict=dict(choices=STRICTS), - parent=dict(type='int'), - vlans=dict(type='list'), - routing_protocol=dict(type='list'), - bwc_policy=dict(), - connection_limit=dict(type='int',), - flow_eviction_policy=dict(), - service_policy=dict(), - partition=dict( - default='Common', - fallback=(env_fallback, ['F5_PARTITION']) - ), - state=dict( - default='present', - choices=['present', 'absent'] - ) - ) - argument_spec.update(meta_args) + spec = ArgumentSpec() module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=[['name', 'id']] + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode ) + if not HAS_F5SDK: + module.fail_json(msg="The python f5-sdk module is required") try: - obj = BigIpRouteDomain(check_mode=module.check_mode, **module.params) - result = obj.exec_module() - - module.exit_json(**result) - except F5ModuleError as e: - module.fail_json(msg=str(e)) + client = F5Client(**module.params) + mm = ModuleManager(module=module, client=client) + results = mm.exec_module() + cleanup_tokens(client) + module.exit_json(**results) + except F5ModuleError as ex: + cleanup_tokens(client) + module.fail_json(msg=str(ex)) if __name__ == '__main__': diff --git a/lib/ansible/modules/network/f5/bigip_security_address_list.py b/lib/ansible/modules/network/f5/bigip_security_address_list.py index 020c2b6620..123a554a86 100644 --- a/lib/ansible/modules/network/f5/bigip_security_address_list.py +++ b/lib/ansible/modules/network/f5/bigip_security_address_list.py @@ -19,7 +19,7 @@ short_description: Manage address lists on BIG-IP AFM description: - Manages the AFM address lists on a BIG-IP. This module can be used to add and remove address list entries. -version_added: "2.5" +version_added: 2.5 options: name: description: @@ -54,6 +54,7 @@ options: - Individual addresses that you want to add to the list. These addresses differ from ranges, and lists of lists such as what can be used in C(address_ranges) and C(address_lists) respectively. + - This list can also include networks that have CIDR notation. address_ranges: description: - A list of address ranges where the range starts with a port number, is followed @@ -152,30 +153,25 @@ import re 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 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 @@ -219,11 +215,6 @@ class Parameters(AnsibleF5Parameters): pass return result - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - class ApiParameters(Parameters): @property @@ -538,8 +529,15 @@ class ModuleParameters(Parameters): netaddr.IPAddress(x) except netaddr.core.AddrFormatError: raise F5ModuleError( - "Address {0} must be either an IPv4 or IPv6 address".format(x) + "Address {0} must be either an IPv4 or IPv6 address or network.".format(x) ) + except ValueError: + try: + netaddr.IPNetwork(x) + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "Address {0} must be either an IPv4 or IPv6 address or network.".format(x) + ) result = [str(x) for x in self._values['addresses']] result = sorted(result) return result @@ -573,7 +571,7 @@ class ModuleParameters(Parameters): return None result = [] for x in self._values['address_lists']: - item = self._fqdn_name(x) + item = fq_name(self.partition, x) result.append(item) result = sorted(result) return result diff --git a/lib/ansible/modules/network/f5/bigip_security_port_list.py b/lib/ansible/modules/network/f5/bigip_security_port_list.py index 395d768825..30e26c44c4 100644 --- a/lib/ansible/modules/network/f5/bigip_security_port_list.py +++ b/lib/ansible/modules/network/f5/bigip_security_port_list.py @@ -19,7 +19,7 @@ short_description: Manage port lists on BIG-IP AFM description: - Manages the AFM port lists on a BIG-IP. This module can be used to add and remove port list entries. -version_added: "2.5" +version_added: 2.5 options: name: description: @@ -29,7 +29,6 @@ options: description: - Device partition to manage resources on. default: Common - version_added: 2.5 description: description: - Description of the port list @@ -56,7 +55,6 @@ options: choices: - present - absent - version_added: 2.5 extends_documentation_fragment: f5 author: - Tim Rupp (@caphrim007) @@ -163,30 +161,25 @@ port_lists: 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 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 @@ -211,11 +204,6 @@ class Parameters(AnsibleF5Parameters): 'description', 'ports', 'port_ranges', 'port_lists' ] - def _fqdn_name(self, value): - if value is not None and not value.startswith('/'): - return '/{0}/{1}'.format(self.partition, value) - return value - class ApiParameters(Parameters): @property @@ -296,7 +284,7 @@ class ModuleParameters(Parameters): return None result = [] for x in self._values['port_lists']: - item = self._fqdn_name(x) + item = fq_name(self.partition, x) result.append(item) return result diff --git a/test/sanity/validate-modules/ignore.txt b/test/sanity/validate-modules/ignore.txt index 23c30d6bae..3c6b76cbcb 100644 --- a/test/sanity/validate-modules/ignore.txt +++ b/test/sanity/validate-modules/ignore.txt @@ -1081,10 +1081,6 @@ lib/ansible/modules/network/f5/bigip_monitor_snmp_dca.py E326 lib/ansible/modules/network/f5/bigip_policy.py E324 lib/ansible/modules/network/f5/bigip_pool.py E326 lib/ansible/modules/network/f5/bigip_profile_client_ssl.py E324 -lib/ansible/modules/network/f5/bigip_provision.py E326 -lib/ansible/modules/network/f5/bigip_qkview.py E324 -lib/ansible/modules/network/f5/bigip_qkview.py E326 -lib/ansible/modules/network/f5/bigip_routedomain.py E326 lib/ansible/modules/network/f5/bigip_selfip.py E324 lib/ansible/modules/network/f5/bigip_sys_global.py E326 lib/ansible/modules/network/f5/bigip_virtual_server.py E326 diff --git a/test/units/modules/network/f5/fixtures/load_net_route_domain_1.json b/test/units/modules/network/f5/fixtures/load_net_route_domain_1.json new file mode 100644 index 0000000000..6ff8c5dfc8 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_net_route_domain_1.json @@ -0,0 +1,36 @@ +{ + "kind": "tm:net:route-domain:route-domainstate", + "name": "0", + "partition": "Common", + "fullPath": "/Common/0", + "generation": 1, + "selfLink": "https://localhost/mgmt/tm/net/route-domain/~Common~0?ver=13.1.0", + "connectionLimit": 0, + "id": 0, + "strict": "enabled", + "throughputCapacity": 0, + "vlans": [ + "/Common/net1", + "/Common/internal", + "/Common/net2", + "/Common/socks-tunnel", + "/Common/http-tunnel" + ], + "vlansReference": [ + { + "link": "https://localhost/mgmt/tm/net/vlan/~Common~net1?ver=13.1.0" + }, + { + "link": "https://localhost/mgmt/tm/net/vlan/~Common~internal?ver=13.1.0" + }, + { + "link": "https://localhost/mgmt/tm/net/vlan/~Common~net2?ver=13.1.0" + }, + { + "link": "https://localhost/mgmt/tm/net/tunnels/tunnel/~Common~socks-tunnel?ver=13.1.0" + }, + { + "link": "https://localhost/mgmt/tm/net/tunnels/tunnel/~Common~http-tunnel?ver=13.1.0" + } + ] +} diff --git a/test/units/modules/network/f5/test_bigip_provision.py b/test/units/modules/network/f5/test_bigip_provision.py index 05e8815aaf..e7932395a8 100644 --- a/test/units/modules/network/f5/test_bigip_provision.py +++ b/test/units/modules/network/f5/test_bigip_provision.py @@ -20,9 +20,9 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_provision import Parameters - from library.bigip_provision import ModuleManager - from library.bigip_provision import ArgumentSpec + from library.modules.bigip_provision import Parameters + from library.modules.bigip_provision import ModuleManager + from library.modules.bigip_provision 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 @@ -107,6 +107,8 @@ class TestManager(unittest.TestCase): # Override methods to force specific logic in the module to happen mm.update_on_device = Mock(return_value=True) mm.read_current_from_device = Mock(return_value=current) + mm.reboot_device = Mock(return_value=True) + mm.save_on_device = Mock(return_value=True) # this forced sleeping can cause these tests to take 15 # or more seconds to run. This is deliberate. diff --git a/test/units/modules/network/f5/test_bigip_qkview.py b/test/units/modules/network/f5/test_bigip_qkview.py index ae45280439..74714f9718 100644 --- a/test/units/modules/network/f5/test_bigip_qkview.py +++ b/test/units/modules/network/f5/test_bigip_qkview.py @@ -20,11 +20,11 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_qkview import Parameters - from library.bigip_qkview import ModuleManager - from library.bigip_qkview import MadmLocationManager - from library.bigip_qkview import BulkLocationManager - from library.bigip_qkview import ArgumentSpec + from library.modules.bigip_qkview import Parameters + from library.modules.bigip_qkview import ModuleManager + from library.modules.bigip_qkview import MadmLocationManager + from library.modules.bigip_qkview import BulkLocationManager + from library.modules.bigip_qkview 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_remote_syslog.py b/test/units/modules/network/f5/test_bigip_remote_syslog.py index 4ea769d90d..53b996651b 100644 --- a/test/units/modules/network/f5/test_bigip_remote_syslog.py +++ b/test/units/modules/network/f5/test_bigip_remote_syslog.py @@ -20,11 +20,11 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_remote_syslog import Parameters - from library.bigip_remote_syslog import ModuleManager - from library.bigip_remote_syslog import ArgumentSpec - from library.bigip_remote_syslog import HAS_F5SDK - from library.bigip_remote_syslog import HAS_NETADDR + from library.modules.bigip_remote_syslog import Parameters + from library.modules.bigip_remote_syslog import ModuleManager + from library.modules.bigip_remote_syslog import ArgumentSpec + from library.modules.bigip_remote_syslog import HAS_F5SDK + from library.modules.bigip_remote_syslog import HAS_NETADDR 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_routedomain.py b/test/units/modules/network/f5/test_bigip_routedomain.py index e69de29bb2..5bb1894fb5 100644 --- a/test/units/modules/network/f5/test_bigip_routedomain.py +++ b/test/units/modules/network/f5/test_bigip_routedomain.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2017, F5 Networks Inc. +# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json +import pytest +import sys + +from nose.plugins.skip import SkipTest +if sys.version_info < (2, 7): + raise SkipTest("F5 Ansible modules require Python >= 2.7") + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import Mock +from ansible.compat.tests.mock import patch +from ansible.module_utils.basic import AnsibleModule + +try: + from library.modules.bigip_routedomain import ApiParameters + from library.modules.bigip_routedomain import ModuleParameters + from library.modules.bigip_routedomain import ModuleManager + from library.modules.bigip_routedomain 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_routedomain import ApiParameters + from ansible.modules.network.f5.bigip_routedomain import ModuleParameters + from ansible.modules.network.f5.bigip_routedomain import ModuleManager + from ansible.modules.network.f5.bigip_routedomain import ArgumentSpec + from ansible.module_utils.network.f5.common import F5ModuleError + from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError + from units.modules.utils import set_module_args + except ImportError: + raise SkipTest("F5 Ansible modules require the f5-sdk Python library") + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except Exception: + pass + + fixture_data[path] = data + return data + + +class TestParameters(unittest.TestCase): + def test_module_parameters(self): + args = dict( + name='foo', + id='1234', + description='my description', + strict=True, + parent='parent1', + vlans=['vlan1', 'vlan2'], + routing_protocol=['BFD', 'BGP'], + bwc_policy='bwc1', + connection_limit=200, + flow_eviction_policy='evict1', + service_policy='service1' + ) + + p = ModuleParameters(params=args) + assert p.name == 'foo' + assert p.id == 1234 + assert p.description == 'my description' + assert p.strict is True + assert p.connection_limit == 200 + + def test_api_parameters(self): + args = load_fixture('load_net_route_domain_1.json') + + p = ApiParameters(params=args) + assert len(p.vlans) == 5 + assert p.id == 0 + assert p.strict is True + assert p.connection_limit == 0 + + +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create(self, *args): + set_module_args(dict( + name='foo', + id=1234, + password='password', + server='localhost', + user='admin' + )) + + module = AnsibleModule( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode + ) + mm = ModuleManager(module=module) + + # Override methods to force specific logic in the module to happen + mm.exists = Mock(return_value=False) + mm.create_on_device = Mock(return_value=True) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['id'] == 1234 diff --git a/test/units/modules/network/f5/test_bigip_security_address_list.py b/test/units/modules/network/f5/test_bigip_security_address_list.py index eeff04d6f7..0688cdbdff 100644 --- a/test/units/modules/network/f5/test_bigip_security_address_list.py +++ b/test/units/modules/network/f5/test_bigip_security_address_list.py @@ -21,16 +21,15 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_security_address_list import ApiParameters - from library.bigip_security_address_list import ModuleParameters - from library.bigip_security_address_list import ModuleManager - from library.bigip_security_address_list import ArgumentSpec + from library.modules.bigip_security_address_list import ApiParameters + from library.modules.bigip_security_address_list import ModuleParameters + from library.modules.bigip_security_address_list import ModuleManager + from library.modules.bigip_security_address_list 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_security_address_list import Parameters from ansible.modules.network.f5.bigip_security_address_list import ApiParameters from ansible.modules.network.f5.bigip_security_address_list import ModuleParameters from ansible.modules.network.f5.bigip_security_address_list import ModuleManager diff --git a/test/units/modules/network/f5/test_bigip_security_port_list.py b/test/units/modules/network/f5/test_bigip_security_port_list.py index b0265a7677..563a19527a 100644 --- a/test/units/modules/network/f5/test_bigip_security_port_list.py +++ b/test/units/modules/network/f5/test_bigip_security_port_list.py @@ -21,16 +21,15 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.bigip_security_port_list import ApiParameters - from library.bigip_security_port_list import ModuleParameters - from library.bigip_security_port_list import ModuleManager - from library.bigip_security_port_list import ArgumentSpec + from library.modules.bigip_security_port_list import ApiParameters + from library.modules.bigip_security_port_list import ModuleParameters + from library.modules.bigip_security_port_list import ModuleManager + from library.modules.bigip_security_port_list 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_security_port_list import Parameters from ansible.modules.network.f5.bigip_security_port_list import ApiParameters from ansible.modules.network.f5.bigip_security_port_list import ModuleParameters from ansible.modules.network.f5.bigip_security_port_list import ModuleManager