From 0dacc606b4e2e83ae29af32c466bf5980542c860 Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Mon, 27 Aug 2018 14:09:03 -0700 Subject: [PATCH] Various fixes for f5 modules (#44734) A number of bugfixes for the remaining 2.7 work on the F5 modules. --- .../modules/network/f5/bigip_command.py | 9 + .../modules/network/f5/bigip_device_facts.py | 2 +- lib/ansible/modules/network/f5/bigip_facts.py | 5 +- .../network/f5/bigip_firewall_policy.py | 6 +- .../network/f5/bigip_firewall_rule_list.py | 4 +- .../modules/network/f5/bigip_iapp_service.py | 31 ++ .../network/f5/bigip_iapplx_package.py | 1 - .../modules/network/f5/bigip_pool_member.py | 4 +- .../modules/network/f5/bigip_profile_http.py | 7 +- lib/ansible/modules/network/f5/bigip_user.py | 9 +- .../network/f5/bigip_virtual_address.py | 223 +++++++++--- .../network/f5/bigip_virtual_server.py | 330 ++++++++++++++---- lib/ansible/modules/network/f5/bigip_wait.py | 90 +++-- .../f5/bigiq_application_https_offload.py | 8 +- .../network/f5/test_bigip_virtual_address.py | 30 +- 15 files changed, 560 insertions(+), 199 deletions(-) diff --git a/lib/ansible/modules/network/f5/bigip_command.py b/lib/ansible/modules/network/f5/bigip_command.py index 81c166795f..15e666b410 100644 --- a/lib/ansible/modules/network/f5/bigip_command.py +++ b/lib/ansible/modules/network/f5/bigip_command.py @@ -207,6 +207,7 @@ from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.six import string_types from collections import deque + try: from library.module_utils.network.f5.bigip import HAS_F5SDK from library.module_utils.network.f5.bigip import F5Client @@ -239,6 +240,10 @@ except ImportError: HAS_CLI_TRANSPORT = False +if HAS_F5SDK: + from f5.sdk_exception import LazyAttributesRequired + + class NoChangeReporter(object): stdout_re = [ # A general error when a resource already exists @@ -635,6 +640,10 @@ class V2Manager(BaseManager): responses.append(output.strip()) except F5ModuleError: raise + except LazyAttributesRequired: + # This can happen if there is no "commandResult" attribute in + # the output variable above. + pass return responses diff --git a/lib/ansible/modules/network/f5/bigip_device_facts.py b/lib/ansible/modules/network/f5/bigip_device_facts.py index b5bd2c8ff7..67541402cd 100644 --- a/lib/ansible/modules/network/f5/bigip_device_facts.py +++ b/lib/ansible/modules/network/f5/bigip_device_facts.py @@ -11127,7 +11127,7 @@ class VirtualServersParameters(BaseParameters): @property def enabled(self): if self._values['enabled'] is None: - return None + return 'no' elif self._values['enabled'] is True: return 'yes' return 'no' diff --git a/lib/ansible/modules/network/f5/bigip_facts.py b/lib/ansible/modules/network/f5/bigip_facts.py index ee97eafb2b..05120a5b2b 100644 --- a/lib/ansible/modules/network/f5/bigip_facts.py +++ b/lib/ansible/modules/network/f5/bigip_facts.py @@ -29,6 +29,7 @@ notes: - Best run as a local_action in your playbook - Tested with manager and above account privilege level - C(provision) facts were added in 2.2 + - This module is deprecated. Use the C(bigip_device_facts) module instead. requirements: - bigsuds options: @@ -75,7 +76,9 @@ EXAMPLES = r''' server: lb.mydomain.com user: admin password: secret - include: interface,vlan + include: + - interface + - vlan delegate_to: localhost ''' diff --git a/lib/ansible/modules/network/f5/bigip_firewall_policy.py b/lib/ansible/modules/network/f5/bigip_firewall_policy.py index 76534ca1dd..5ece6620ce 100644 --- a/lib/ansible/modules/network/f5/bigip_firewall_policy.py +++ b/lib/ansible/modules/network/f5/bigip_firewall_policy.py @@ -374,11 +374,7 @@ class ModuleManager(object): response = self.client.api.delete(uri) if response.status == 200: return True - if 'code' in response and response['code'] == 400: - if 'message' in response: - raise F5ModuleError(response['message']) - else: - raise F5ModuleError(response.content) + raise F5ModuleError(response.content) def read_current_from_device(self): uri = "https://{0}:{1}/mgmt/tm/security/firewall/policy/{2}/?expandSubcollections=true".format( diff --git a/lib/ansible/modules/network/f5/bigip_firewall_rule_list.py b/lib/ansible/modules/network/f5/bigip_firewall_rule_list.py index 7c1931e6b4..26f7f3b68a 100644 --- a/lib/ansible/modules/network/f5/bigip_firewall_rule_list.py +++ b/lib/ansible/modules/network/f5/bigip_firewall_rule_list.py @@ -31,8 +31,8 @@ options: versions it will simply be ignored. state: description: - - When C(state) is C(present), ensures that the policy exists. - - When C(state) is C(absent), ensures that the policy is removed. + - When C(state) is C(present), ensures that the rule list exists. + - When C(state) is C(absent), ensures that the rule list is removed. choices: - present - absent diff --git a/lib/ansible/modules/network/f5/bigip_iapp_service.py b/lib/ansible/modules/network/f5/bigip_iapp_service.py index 45e627445e..6031b150e7 100644 --- a/lib/ansible/modules/network/f5/bigip_iapp_service.py +++ b/lib/ansible/modules/network/f5/bigip_iapp_service.py @@ -372,6 +372,26 @@ class Parameters(AnsibleF5Parameters): # This seems to happen only on 12.0.0 elif tmp['value'] == 'none': tmp['value'] = '' + elif tmp['value'] == 'True': + tmp['value'] = 'yes' + elif tmp['value'] == 'False': + tmp['value'] = 'no' + elif isinstance(tmp['value'], bool): + if tmp['value'] is True: + tmp['value'] = 'yes' + else: + tmp['value'] = 'no' + + if tmp['encrypted'] == 'True': + tmp['encrypted'] = 'yes' + elif tmp['encrypted'] == 'False': + tmp['encrypted'] = 'no' + elif isinstance(tmp['encrypted'], bool): + if tmp['encrypted'] is True: + tmp['encrypted'] = 'yes' + else: + tmp['encrypted'] = 'no' + result.append(tmp) result = sorted(result, key=lambda k: k['name']) return result @@ -390,6 +410,17 @@ class Parameters(AnsibleF5Parameters): # BIG-IP removes empty values entries, so mimic this behavior # for user-supplied values. tmp['value'] = [str(x) for x in list['value']] + + if tmp['encrypted'] == 'True': + tmp['encrypted'] = 'yes' + elif tmp['encrypted'] == 'False': + tmp['encrypted'] = 'no' + elif isinstance(tmp['encrypted'], bool): + if tmp['encrypted'] is True: + tmp['encrypted'] = 'yes' + else: + tmp['encrypted'] = 'no' + result.append(tmp) result = sorted(result, key=lambda k: k['name']) return result diff --git a/lib/ansible/modules/network/f5/bigip_iapplx_package.py b/lib/ansible/modules/network/f5/bigip_iapplx_package.py index 26ca18bdbb..43ba7badb8 100644 --- a/lib/ansible/modules/network/f5/bigip_iapplx_package.py +++ b/lib/ansible/modules/network/f5/bigip_iapplx_package.py @@ -85,7 +85,6 @@ RETURN = r''' ''' import os -import subprocess import time from ansible.module_utils.basic import AnsibleModule diff --git a/lib/ansible/modules/network/f5/bigip_pool_member.py b/lib/ansible/modules/network/f5/bigip_pool_member.py index d5bf6d33f6..155b629ccc 100644 --- a/lib/ansible/modules/network/f5/bigip_pool_member.py +++ b/lib/ansible/modules/network/f5/bigip_pool_member.py @@ -419,7 +419,7 @@ class ApiParameters(Parameters): @property def state(self): - if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr'] and self._values['session'] in ['user-enabled']: + if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and self._values['session'] in ['user-enabled']: return 'present' elif self._values['state'] in ['down', 'up'] and self._values['session'] == 'monitor-enabled': return 'present' @@ -476,7 +476,7 @@ class ReportableChanges(Changes): @property def state(self): - if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr'] and self._values['session'] in ['user-enabled']: + if self._values['state'] in ['user-up', 'unchecked', 'fqdn-up-no-addr', 'fqdn-up'] and self._values['session'] in ['user-enabled']: return 'present' elif self._values['state'] in ['down', 'up'] and self._values['session'] == 'monitor-enabled': return 'present' diff --git a/lib/ansible/modules/network/f5/bigip_profile_http.py b/lib/ansible/modules/network/f5/bigip_profile_http.py index e1259d8c4d..0455ee5329 100644 --- a/lib/ansible/modules/network/f5/bigip_profile_http.py +++ b/lib/ansible/modules/network/f5/bigip_profile_http.py @@ -592,12 +592,7 @@ class ModuleManager(object): response = self.client.api.delete(uri) if response.status == 200: return True - - if 'code' in response and response['code'] == 400: - if 'message' in response: - raise F5ModuleError(response['message']) - else: - raise F5ModuleError(response.content) + raise F5ModuleError(response.content) def read_current_from_device(self): uri = "https://{0}:{1}/mgmt/tm/ltm/profile/http/{2}".format( diff --git a/lib/ansible/modules/network/f5/bigip_user.py b/lib/ansible/modules/network/f5/bigip_user.py index 9c3ca2a2f7..657b4a1f97 100644 --- a/lib/ansible/modules/network/f5/bigip_user.py +++ b/lib/ansible/modules/network/f5/bigip_user.py @@ -18,9 +18,8 @@ module: bigip_user short_description: Manage user accounts and user attributes on a BIG-IP description: - Manage user accounts and user attributes on a BIG-IP. Typically this - module operates only on the REST API users and not the CLI users. There - is one exception though and that is if you specify the C(username_credential) - of C(root). When specifying C(root), you may only change the password. + module operates only on the REST API users and not the CLI users. + When specifying C(root), you may only change the password. Your other parameters will be ignored in this case. Changing the C(root) password is not an idempotent operation. Therefore, it will change it every time this module attempts to change it. @@ -31,8 +30,8 @@ options: - Full name of the user. username_credential: description: - - Name of the user to create, remove or modify. There is a special case - that exists for the user C(root). + - Name of the user to create, remove or modify. + - The C(root) user may not be removed. required: True aliases: - name diff --git a/lib/ansible/modules/network/f5/bigip_virtual_address.py b/lib/ansible/modules/network/f5/bigip_virtual_address.py index db757a3ea4..3ffe660a29 100644 --- a/lib/ansible/modules/network/f5/bigip_virtual_address.py +++ b/lib/ansible/modules/network/f5/bigip_virtual_address.py @@ -48,9 +48,23 @@ options: both ARP and ICMP Echo must be disabled in order for forwarding virtual servers using that virtual address to forward ICMP packets. If (enabled), then the packets are dropped. + - Deprecated. Use the C(arp) parameter instead. + - When creating a new virtual address, if this parameter is not specified, + the default value is C(enabled). choices: - enabled - disabled + arp: + description: + - Specifies whether the system accepts ARP requests. + - When C(no), specifies that the system does not accept ARP requests. + - When C(yes), then the packets are dropped. + - Note that both ARP and ICMP Echo must be disabled in order for forwarding + virtual servers using that virtual address to forward ICMP packets. + - When creating a new virtual address, if this parameter is not specified, + the default value is C(yes). + type: bool + version_added: 2.7 auto_delete: description: - Specifies whether the system automatically deletes the virtual @@ -58,9 +72,9 @@ options: When C(disabled), specifies that the system leaves the virtual address even when all associated virtual servers have been deleted. When creating the virtual address, the default value is C(enabled). - choices: - - enabled - - disabled + - C(enabled) and C(disabled) are deprecated and will be removed in + Ansible 2.11. Instead, use known Ansible booleans such as C(yes) and + C(no) icmp_echo: description: - Specifies how the systems sends responses to (ICMP) echo requests @@ -157,12 +171,22 @@ options: - The route domain of the C(address) that you want to use. - This value cannot be modified after it is set. version_added: 2.6 -notes: - - Requires the netaddr Python package on the host. This is as easy as pip - install netaddr. + spanning: + description: + - Enables all BIG-IP systems in a device group to listen for and process traffic + on the same virtual address. + - Spanning for a virtual address occurs when you enable the C(spanning) option on a + device and then sync the virtual address to the other members of the device group. + - Spanning also relies on the upstream router to distribute application flows to the + BIG-IP systems using ECMP routes. ECMP defines a route to the virtual address using + distinct Floating self-IP addresses configured on each BIG-IP system. + - You must also configure MAC masquerade addresses and disable C(arp) on the virtual + address when Spanning is enabled. + - When creating a new virtual address, if this parameter is not specified, the default + valus is C(no). + version_added: 2.7 + type: bool extends_documentation_fragment: f5 -requirements: - - netaddr author: - Tim Rupp (@caphrim007) ''' @@ -215,11 +239,11 @@ netmask: returned: created type: int sample: 2345 -arp_state: +arp: description: The new way the virtual address handles ARP requests. returned: changed - type: string - sample: disabled + type: bool + sample: yes address: description: The address of the virtual address. returned: created @@ -230,6 +254,11 @@ state: returned: changed type: string sample: disabled +spanning: + description: Whether spanning is enabled or not + returned: changed + type: string + sample: disabled ''' from ansible.module_utils.basic import AnsibleModule @@ -246,6 +275,7 @@ try: from library.module_utils.network.f5.common import cleanup_tokens from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec + from library.module_utils.network.f5.ipaddress import is_valid_ip try: from library.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: @@ -258,17 +288,12 @@ except ImportError: from ansible.module_utils.network.f5.common import cleanup_tokens from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec + from ansible.module_utils.network.f5.ipaddress import is_valid_ip try: from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError except ImportError: HAS_F5SDK = False -try: - import netaddr - HAS_NETADDR = True -except ImportError: - HAS_NETADDR = False - class Parameters(AnsibleF5Parameters): api_map = { @@ -278,23 +303,47 @@ class Parameters(AnsibleF5Parameters): 'connectionLimit': 'connection_limit', 'serverScope': 'availability_calculation', 'mask': 'netmask', - 'arp': 'arp_state', 'trafficGroup': 'traffic_group', } updatables = [ - 'route_advertisement_type', 'auto_delete', 'icmp_echo', 'connection_limit', - 'arp_state', 'enabled', 'availability_calculation', 'traffic_group' + 'route_advertisement_type', + 'auto_delete', + 'icmp_echo', + 'connection_limit', + 'arp', + 'enabled', + 'availability_calculation', + 'traffic_group', + 'spanning', ] returnables = [ - 'route_advertisement_type', 'auto_delete', 'icmp_echo', 'connection_limit', - 'netmask', 'arp_state', 'address', 'state', 'traffic_group', 'route_domain' + 'route_advertisement_type', + 'auto_delete', + 'icmp_echo', + 'connection_limit', + 'netmask', + 'arp', + 'address', + 'state', + 'traffic_group', + 'route_domain', + 'spanning', ] api_attributes = [ - 'routeAdvertisement', 'autoDelete', 'icmpEcho', 'connectionLimit', - 'advertiseRoute', 'arp', 'mask', 'enabled', 'serverScope', 'trafficGroup' + 'routeAdvertisement', + 'autoDelete', + 'icmpEcho', + 'connectionLimit', + 'advertiseRoute', + 'arp', + 'mask', + 'enabled', + 'serverScope', + 'trafficGroup', + 'spanning', ] @property @@ -331,10 +380,9 @@ class Parameters(AnsibleF5Parameters): def netmask(self): if self._values['netmask'] is None: return None - try: - ip = netaddr.IPAddress(self._values['netmask']) - return str(ip) - except netaddr.core.AddrFormatError: + if is_valid_ip(self._values['netmask']): + return self._values['netmask'] + else: raise F5ModuleError( "The provided 'netmask' is not a valid IP address" ) @@ -414,18 +462,41 @@ class Parameters(AnsibleF5Parameters): class ApiParameters(Parameters): - pass + @property + def arp(self): + if self._values['arp'] is None: + return None + elif self._values['arp'] == 'enabled': + return True + return False + + @property + def spanning(self): + if self._values['spanning'] is None: + return None + if self._values['spanning'] == 'enabled': + return True + return False class ModuleParameters(Parameters): + @property + def arp(self): + if self._values['arp'] is None: + if self.arp_state and self.arp_state == 'enabled': + return True + elif self.arp_state and self.arp_state == 'disabled': + return False + else: + return self._values['arp'] + @property def address(self): if self._values['address'] is None: return None - try: - ip = netaddr.IPAddress(self._values['address']) - return str(ip) - except netaddr.core.AddrFormatError: + if is_valid_ip(self._values['address']): + return self._values['address'] + else: raise F5ModuleError( "The provided 'address' is not a valid IP address" ) @@ -479,9 +550,31 @@ class UsableChanges(Changes): result = "{0}%{1}".format(self._values['address'], self._values['route_domain']) return result + @property + def arp(self): + if self._values['arp'] is None: + return None + elif self._values['arp'] is True: + return 'enabled' + elif self._values['arp'] is False: + return 'disabled' + + @property + def spanning(self): + if self._values['spanning'] is None: + return None + if self._values['spanning']: + return 'enabled' + return 'disabled' + class ReportableChanges(Changes): - pass + @property + def arp(self): + if self._values['arp'] == 'disabled': + return 'no' + elif self._values['arp'] == 'enabled': + return 'yes' class Difference(object): @@ -510,6 +603,20 @@ class Difference(object): if self.want.traffic_group != self.have.traffic_group: return self.want.traffic_group + @property + def spanning(self): + if self.want.spanning is None: + return None + if self.want.spanning != self.have.spanning: + return self.want.spanning + + @property + def arp_state(self): + if self.want.arp_state is None: + return None + if self.want.arp_state != self.have.arp_state: + return self.want.arp_state + class ModuleManager(object): def __init__(self, *args, **kwargs): @@ -617,6 +724,11 @@ class ModuleManager(object): "The address cannot be changed. Delete and recreate " "the virtual address if you need to do this." ) + if self.changes.arp and self.changes.spanning: + raise F5ModuleError( + "'arp' and 'spanning' cannot both be enabled on virtual address." + ) + if not self.should_update(): return False if self.module.check_mode: @@ -636,8 +748,18 @@ class ModuleManager(object): def create(self): self._set_changed_options() + if self.want.traffic_group is None: self.want.update({'traffic_group': '/Common/traffic-group-1'}) + if self.want.arp is None: + self.want.update({'arp': True}) + if self.want.spanning is None: + self.want.update({'spanning': False}) + + if self.want.arp and self.want.spanning: + raise F5ModuleError( + "'arp' and 'spanning' cannot both be enabled on virtual address." + ) if self.module.check_mode: return True self.create_on_device() @@ -690,12 +812,8 @@ class ArgumentSpec(object): connection_limit=dict( type='int' ), - arp_state=dict( - choices=['enabled', 'disabled'], - ), - auto_delete=dict( - choices=['enabled', 'disabled'], - ), + + auto_delete=dict(), icmp_echo=dict( choices=['enabled', 'disabled', 'selective'], ), @@ -703,6 +821,15 @@ class ArgumentSpec(object): choices=['always', 'when_all_available', 'when_any_available'], aliases=['advertise_route'] ), + traffic_group=dict(), + partition=dict( + default='Common', + fallback=(env_fallback, ['F5_PARTITION']) + ), + route_domain=dict(), + spanning=dict(type='bool'), + + # Deprecated pair - route advertisement use_route_advertisement=dict( type='bool', removed_in_version=2.9, @@ -717,12 +844,13 @@ class ArgumentSpec(object): 'all', ] ), - traffic_group=dict(), - partition=dict( - default='Common', - fallback=(env_fallback, ['F5_PARTITION']) + + # Deprecated pair - ARP + arp_state=dict( + choices=['enabled', 'disabled'], + removed_in_version=2.11, ), - route_domain=dict() + arp=dict(type='bool'), ) self.argument_spec = {} self.argument_spec.update(f5_argument_spec) @@ -731,7 +859,8 @@ class ArgumentSpec(object): ['name', 'address'] ] self.mutually_exclusive = [ - ['use_route_advertisement', 'route_advertisement'] + ['use_route_advertisement', 'route_advertisement'], + ['arp_state', 'arp'] ] @@ -744,8 +873,6 @@ def main(): ) if not HAS_F5SDK: module.fail_json(msg="The python f5-sdk module is required") - if not HAS_NETADDR: - module.fail_json(msg="The python netaddr module is required") try: client = F5Client(**module.params) diff --git a/lib/ansible/modules/network/f5/bigip_virtual_server.py b/lib/ansible/modules/network/f5/bigip_virtual_server.py index 7350623117..aec54a19fa 100644 --- a/lib/ansible/modules/network/f5/bigip_virtual_server.py +++ b/lib/ansible/modules/network/f5/bigip_virtual_server.py @@ -637,35 +637,31 @@ from ansible.module_utils.six import iteritems from collections import namedtuple try: - 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.bigip import F5RestClient from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import cleanup_tokens from library.module_utils.network.f5.common import fq_name from library.module_utils.network.f5.common import f5_argument_spec + from library.module_utils.network.f5.common import fail_json + from library.module_utils.network.f5.common import exit_json + from library.module_utils.network.f5.common import transform_name from library.module_utils.network.f5.ipaddress import is_valid_ip from library.module_utils.network.f5.ipaddress import ip_interface from library.module_utils.network.f5.ipaddress import validate_ip_v6_address - try: - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False except ImportError: - 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.bigip import F5RestClient from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import cleanup_tokens from ansible.module_utils.network.f5.common import fq_name from ansible.module_utils.network.f5.common import f5_argument_spec + from ansible.module_utils.network.f5.common import fail_json + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import transform_name from ansible.module_utils.network.f5.ipaddress import is_valid_ip from ansible.module_utils.network.f5.ipaddress import ip_interface from ansible.module_utils.network.f5.ipaddress import validate_ip_v6_address - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False class Parameters(AnsibleF5Parameters): @@ -913,20 +909,85 @@ class Parameters(AnsibleF5Parameters): return False def _read_current_message_routing_profiles_from_device(self): - collection1 = self.client.api.tm.ltm.profile.diameters.get_collection() - collection2 = self.client.api.tm.ltm.profile.sips.get_collection() - result = [x.name for x in collection1] - result += [x.name for x in collection2] + result = [] + result += self._read_diameter_profiles_from_device() + result += self._read_sip_profiles_from_device() + return result + + def _read_diameter_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/diameter/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [x['name'] for x in response['items']] + return result + + def _read_sip_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/sip/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [x['name'] for x in response['items']] return result def _read_current_fastl4_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.fastl4s.get_collection() - result = [x.name for x in collection] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [x['name'] for x in response['items']] return result def _read_current_fasthttp_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.fasthttps.get_collection() - result = [x.name for x in collection] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [x['name'] for x in response['items']] return result @@ -1246,6 +1307,12 @@ class ApiParameters(Parameters): return None return self._values['security_nat_policy']['policy'] + @property + def irules(self): + if self._values['irules'] is None: + return [] + return self._values['irules'] + class ModuleParameters(Parameters): services_map = { @@ -2268,25 +2335,104 @@ class VirtualServerValidator(object): ) def read_dhcp_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.dhcpv4s.get_collection() - result = [fq_name(self.want.partition, x.name) for x in collection] - collection = self.client.api.tm.ltm.profile.dhcpv6s.get_collection() - result += [fq_name(self.want.partition, x.name) for x in collection] + result = [] + result += self.read_dhcpv4_profiles_from_device() + result += self.read_dhcpv6_profiles_from_device() + return result + + def read_dhcpv4_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dhcpv4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] + return result + + def read_dhcpv6_profiles_from_device(self): + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/dhcpv6/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] return result def read_fastl4_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.fastl4s.get_collection() - result = [fq_name(self.want.partition, x.name) for x in collection] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fastl4/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] return result def read_fasthttp_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.fasthttps.get_collection() - result = [fq_name(self.want.partition, x.name) for x in collection] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/fasthttp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] return result def read_udp_profiles_from_device(self): - collection = self.client.api.tm.ltm.profile.udps.get_collection() - result = [fq_name(self.want.partition, x.name) for x in collection] + uri = "https://{0}:{1}/mgmt/tm/ltm/profile/udp/".format( + self.client.provider['server'], + self.client.provider['server_port'], + ) + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + result = [fq_name(self.want.partition, x['name']) for x in response['items']] return result @@ -2637,13 +2783,10 @@ class ModuleManager(object): result = dict() state = self.want.state - try: - if state in ['present', 'enabled', 'disabled']: - changed = self.present() - elif state == "absent": - changed = self.absent() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + if state in ['present', 'enabled', 'disabled']: + changed = self.present() + elif state == "absent": + changed = self.absent() reportable = ReportableChanges(params=self.changes.to_return()) changes = reportable.to_return() @@ -2664,7 +2807,9 @@ class ModuleManager(object): def update(self): self.have = self.read_current_from_device() - validator = VirtualServerValidator(module=self.module, client=self.client, have=self.have, want=self.want) + validator = VirtualServerValidator( + module=self.module, client=self.client, have=self.have, want=self.want + ) validator.check_update() if not self.should_update(): @@ -2719,14 +2864,24 @@ class ModuleManager(object): return False def exists(self): - result = self.client.api.tm.ltm.virtuals.virtual.exists( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError: + return False + if resp.status == 404 or 'code' in response and response['code'] == 404: + return False + return True def create(self): - validator = VirtualServerValidator(module=self.module, client=self.client, have=self.have, want=self.want) + validator = VirtualServerValidator( + module=self.module, client=self.client, have=self.have, want=self.want + ) validator.check_create() self._set_changed_options() @@ -2737,42 +2892,73 @@ class ModuleManager(object): def update_on_device(self): params = self.changes.api_params() - resource = self.client.api.tm.ltm.virtuals.virtual.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - resource.modify(**params) + resp = self.client.api.patch(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def read_current_from_device(self): - result = self.client.api.tm.ltm.virtuals.virtual.load( - name=self.want.name, - partition=self.want.partition, - requests_params=dict( - params=dict( - expandSubcollections='true' - ) - ) + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}?expandSubcollections=true".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - params = result.attrs - params.update(dict(kind=result.to_dict().get('kind', None))) - result = ApiParameters(params=params, client=self.client) - return result + resp = self.client.api.get(uri) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] == 400: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + return ApiParameters(params=response, client=self.client) def create_on_device(self): params = self.changes.api_params() - self.client.api.tm.ltm.virtuals.virtual.create( - name=self.want.name, - partition=self.want.partition, - **params + params['name'] = self.want.name + params['partition'] = self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/".format( + self.client.provider['server'], + self.client.provider['server_port'] ) + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) def remove_from_device(self): - resource = self.client.api.tm.ltm.virtuals.virtual.load( - name=self.want.name, - partition=self.want.partition + uri = "https://{0}:{1}/mgmt/tm/ltm/virtual/{2}".format( + self.client.provider['server'], + self.client.provider['server_port'], + transform_name(self.want.partition, self.want.name) ) - if resource: - resource.delete() + response = self.client.api.delete(uri) + if response.status == 200: + return True + raise F5ModuleError(response.content) class ArgumentSpec(object): @@ -2868,18 +3054,16 @@ def main(): supports_check_mode=spec.supports_check_mode, mutually_exclusive=spec.mutually_exclusive ) - if not HAS_F5SDK: - module.fail_json(msg="The python f5-sdk module is required") try: - client = F5Client(**module.params) + client = F5RestClient(**module.params) mm = ModuleManager(module=module, client=client) results = mm.exec_module() + exit_json(module, results, client) cleanup_tokens(client) - module.exit_json(**results) except F5ModuleError as ex: cleanup_tokens(client) - module.fail_json(msg=str(ex)) + fail_json(module, ex, client) if __name__ == '__main__': diff --git a/lib/ansible/modules/network/f5/bigip_wait.py b/lib/ansible/modules/network/f5/bigip_wait.py index 5e4af0a0e5..0acbe82d3e 100644 --- a/lib/ansible/modules/network/f5/bigip_wait.py +++ b/lib/ansible/modules/network/f5/bigip_wait.py @@ -81,25 +81,19 @@ import time from ansible.module_utils.basic import AnsibleModule try: - 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.bigip import F5RestClient from library.module_utils.network.f5.common import F5ModuleError from library.module_utils.network.f5.common import AnsibleF5Parameters from library.module_utils.network.f5.common import f5_argument_spec - try: - from library.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from library.module_utils.network.f5.common import exit_json + from library.module_utils.network.f5.common import fail_json except ImportError: - 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.bigip import F5RestClient from ansible.module_utils.network.f5.common import F5ModuleError from ansible.module_utils.network.f5.common import AnsibleF5Parameters from ansible.module_utils.network.f5.common import f5_argument_spec - try: - from ansible.module_utils.network.f5.common import iControlUnexpectedHTTPError - except ImportError: - HAS_F5SDK = False + from ansible.module_utils.network.f5.common import exit_json + from ansible.module_utils.network.f5.common import fail_json def hard_timeout(module, want, start): @@ -158,10 +152,7 @@ class ModuleManager(object): def exec_module(self): result = dict() - try: - changed = self.execute() - except iControlUnexpectedHTTPError as e: - raise F5ModuleError(str(e)) + changed = self.execute() changes = self.changes.to_return() result.update(**changes) @@ -178,7 +169,7 @@ class ModuleManager(object): ) def _get_client_connection(self): - return F5Client(**self.module.params) + return F5RestClient(**self.module.params) def execute(self): signal.signal( @@ -210,7 +201,7 @@ class ModuleManager(object): if self._is_mprov_running_on_device(): self._wait_for_module_provisioning() break - except Exception: + except Exception as ex: # The types of exception's we're handling here are "REST API is not # ready" exceptions. # @@ -248,15 +239,28 @@ class ModuleManager(object): return False def _device_is_rebooting(self): - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "runlevel"' + params = { + "command": "run", + "utilCmdArgs": '-c "runlevel"' + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] ) + resp = self.client.api.post(uri, json=params) try: - if '6' in output.commandResult: - return True - except AttributeError: - return False + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if 'commandResult' in response and '6' in response['commandResult']: + return True + return False def _wait_for_module_provisioning(self): # To prevent things from running forever, the hack is to check @@ -271,17 +275,32 @@ class ModuleManager(object): nops += 1 else: nops = 0 - except Exception: + except Exception as ex: # This can be caused by restjavad restarting. pass time.sleep(10) def _is_mprov_running_on_device(self): - output = self.client.api.tm.util.bash.exec_cmd( - 'run', - utilCmdArgs='-c "ps aux | grep \'[m]prov\'"' + params = { + "command": "run", + "utilCmdArgs": '-c "ps aux | grep \'[m]prov\'"' + } + uri = "https://{0}:{1}/mgmt/tm/util/bash".format( + self.client.provider['server'], + self.client.provider['server_port'] ) - if hasattr(output, 'commandResult'): + resp = self.client.api.post(uri, json=params) + try: + response = resp.json() + except ValueError as ex: + raise F5ModuleError(str(ex)) + if 'code' in response and response['code'] in [400, 403]: + if 'message' in response: + raise F5ModuleError(response['message']) + else: + raise F5ModuleError(resp.content) + + if 'commandResult' in response: return True return False @@ -307,15 +326,14 @@ def main(): 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: - mm = ModuleManager(module=module) + client = F5RestClient(**module.params) + mm = ModuleManager(module=module, client=client) results = mm.exec_module() - module.exit_json(**results) - except F5ModuleError as e: - module.fail_json(msg=str(e)) + exit_json(module, results, client) + except F5ModuleError as ex: + fail_json(module, ex, client) if __name__ == '__main__': diff --git a/lib/ansible/modules/network/f5/bigiq_application_https_offload.py b/lib/ansible/modules/network/f5/bigiq_application_https_offload.py index bc40126909..fd9b3f4f61 100644 --- a/lib/ansible/modules/network/f5/bigiq_application_https_offload.py +++ b/lib/ansible/modules/network/f5/bigiq_application_https_offload.py @@ -56,7 +56,7 @@ options: - This parameter is required when creating a new application. netmask: description: - - Specifies the netmask to associate with the given C(destination). + - Specifies the netmask to associate with the given C(address). - This parameter is required when creating a new application. port: description: @@ -80,7 +80,7 @@ options: - This parameter is required when creating a new application. netmask: description: - - Specifies the netmask to associate with the given C(destination). + - Specifies the netmask to associate with the given C(address). - This parameter is required when creating a new application. port: description: @@ -130,8 +130,8 @@ options: - Passphrases are encrypted on the remote BIG-IP device. service_environment: description: - - Specifies the name of service environment or the hostname of the BIG-IP that the application will be - deployed to. + - Specifies the name of service environment or the hostname of the BIG-IP that + the application will be deployed to. - When creating a new application, this parameter is required. add_analytics: description: diff --git a/test/units/modules/network/f5/test_bigip_virtual_address.py b/test/units/modules/network/f5/test_bigip_virtual_address.py index 7fdd990433..b4ae6a919e 100644 --- a/test/units/modules/network/f5/test_bigip_virtual_address.py +++ b/test/units/modules/network/f5/test_bigip_virtual_address.py @@ -20,7 +20,7 @@ from ansible.compat.tests.mock import patch from ansible.module_utils.basic import AnsibleModule try: - from library.modules.bigip_virtual_address import Parameters + from library.modules.bigip_virtual_address import ApiParameters from library.modules.bigip_virtual_address import ModuleParameters from library.modules.bigip_virtual_address import ModuleManager from library.modules.bigip_virtual_address import ArgumentSpec @@ -29,7 +29,7 @@ try: from test.unit.modules.utils import set_module_args except ImportError: try: - from ansible.modules.network.f5.bigip_virtual_address import Parameters + from ansible.modules.network.f5.bigip_virtual_address import ApiParameters from ansible.modules.network.f5.bigip_virtual_address import ModuleParameters from ansible.modules.network.f5.bigip_virtual_address import ModuleManager from ansible.modules.network.f5.bigip_virtual_address import ArgumentSpec @@ -79,7 +79,7 @@ class TestParameters(unittest.TestCase): assert p.address == '1.1.1.1' assert p.netmask == '2.2.2.2' assert p.connection_limit == 10 - assert p.arp_state == 'enabled' + assert p.arp is True assert p.auto_delete is True assert p.icmp_echo == 'enabled' assert p.availability_calculation == 'none' @@ -87,10 +87,10 @@ class TestParameters(unittest.TestCase): def test_api_parameters(self): args = load_fixture('load_ltm_virtual_address_default.json') - p = Parameters(params=args) + p = ApiParameters(params=args) assert p.name == '1.1.1.1' assert p.address == '1.1.1.1' - assert p.arp_state == 'enabled' + assert p.arp is True assert p.auto_delete is True assert p.connection_limit == 0 assert p.state == 'enabled' @@ -103,42 +103,42 @@ class TestParameters(unittest.TestCase): args = dict( availability_calculation='when_all_available' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.availability_calculation == 'all' def test_module_parameters_advertise_route_any(self): args = dict( availability_calculation='when_any_available' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.availability_calculation == 'any' def test_module_parameters_icmp_echo_disabled(self): args = dict( icmp_echo='disabled' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.icmp_echo == 'disabled' def test_module_parameters_icmp_echo_selective(self): args = dict( icmp_echo='selective' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.icmp_echo == 'selective' def test_module_parameters_auto_delete_disabled(self): args = dict( auto_delete='disabled' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.auto_delete is False def test_module_parameters_arp_state_disabled(self): args = dict( arp_state='disabled' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.arp_state == 'disabled' def test_module_parameters_use_route_advert_disabled(self): @@ -152,7 +152,7 @@ class TestParameters(unittest.TestCase): args = dict( state='present' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.state == 'present' assert p.enabled == 'yes' @@ -160,14 +160,14 @@ class TestParameters(unittest.TestCase): args = dict( state='absent' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.state == 'absent' def test_module_parameters_state_enabled(self): args = dict( state='enabled' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.state == 'enabled' assert p.enabled == 'yes' @@ -175,7 +175,7 @@ class TestParameters(unittest.TestCase): args = dict( state='disabled' ) - p = Parameters(params=args) + p = ModuleParameters(params=args) assert p.state == 'disabled' assert p.enabled == 'no'