From c132b977e8515e36e14347feada3447490cde84a Mon Sep 17 00:00:00 2001 From: Tim Rupp Date: Wed, 3 Jan 2018 13:42:17 -0800 Subject: [PATCH] Refactors bigip_virtual_server (#34412) This refactors the bigip_virtual_server module to address several issues and missing features. Additionally it adds tests and updates the coding standards to the recent version used by f5. Finally, it removes deprecated items that were announced in 2.4 --- .../network/f5/bigip_virtual_server.py | 2422 ++++++++++++----- .../f5/fixtures/load_ltm_virtual_1.json | 43 + .../fixtures/load_ltm_virtual_1_address.json | 25 + .../f5/fixtures/load_ltm_virtual_2.json | 65 + .../f5/fixtures/load_ltm_virtual_3.json | 115 + .../network/f5/test_bigip_virtual_server.py | 773 ++++++ 6 files changed, 2761 insertions(+), 682 deletions(-) create mode 100644 test/units/modules/network/f5/fixtures/load_ltm_virtual_1.json create mode 100644 test/units/modules/network/f5/fixtures/load_ltm_virtual_1_address.json create mode 100644 test/units/modules/network/f5/fixtures/load_ltm_virtual_2.json create mode 100644 test/units/modules/network/f5/fixtures/load_ltm_virtual_3.json create mode 100644 test/units/modules/network/f5/test_bigip_virtual_server.py diff --git a/lib/ansible/modules/network/f5/bigip_virtual_server.py b/lib/ansible/modules/network/f5/bigip_virtual_server.py index 89daf30fec6..7d446233e18 100644 --- a/lib/ansible/modules/network/f5/bigip_virtual_server.py +++ b/lib/ansible/modules/network/f5/bigip_virtual_server.py @@ -1,794 +1,1852 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - -# Copyright: (c) 2017, F5 Networks Inc. -# Copyright: (c) 2015, Etienne Carriere +# +# 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 + + ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} -DOCUMENTATION = ''' +DOCUMENTATION = r''' --- module: bigip_virtual_server -short_description: Manages F5 BIG-IP LTM virtual servers +short_description: Manage LTM virtual servers on a BIG-IP description: - - Manages F5 BIG-IP LTM virtual servers via iControl SOAP API. + - Manage LTM virtual servers on a BIG-IP. version_added: "2.1" -author: - - Etienne Carriere (@Etienne-Carriere) - - Tim Rupp (@caphrim007) -notes: - - Requires BIG-IP software version >= 11. - - F5 developed module 'bigsuds' required, see U(http://devcentral.f5.com). - - Best run as a local_action in your playbook. -requirements: - - bigsuds options: state: description: - - Virtual Server state. - - Absent, delete the VS if present. - - C(present) (and its synonym enabled), create if needed the VS and set - state to enabled. - - C(disabled), create if needed the VS and set state to disabled. + - The virtual server state. If C(absent), delete the virtual server + if it exists. C(present) creates the virtual server and enable it. + If C(enabled), enable the virtual server if it exists. If C(disabled), + create the virtual server if needed, and set state to C(disabled). default: present - choices: [ absent, disabled, enabled, present ] - partition: - description: - - Partition. - default: Common + choices: + - present + - absent + - enabled + - disabled name: description: - Virtual server name. - required: true + required: True aliases: - vs destination: description: - - Destination IP of the virtual server (only host is currently supported). - Required when state=present and vs does not exist. - required: true + - Destination IP of the virtual server. + - Required when C(state) is C(present) and virtual server does not exist. + required: True aliases: - address - ip + source: + description: + - Specifies an IP address or network from which the virtual server accepts traffic. + - The virtual server accepts clients only from one of these IP addresses. + - For this setting to function effectively, specify a value other than 0.0.0.0/0 or ::/0 + (that is, any/0, any6/0). + - In order to maximize utility of this setting, specify the most specific address + prefixes covering all customer addresses and no others. + - Specify the IP address in Classless Inter-Domain Routing (CIDR) format; address/prefix, + where the prefix length is in bits. For example, for IPv4, 10.0.0.1/32 or 10.0.0.0/24, + and for IPv6, ffe1::0020/64 or 2001:ed8:77b5:2:10:10:100:42/64. + version_added: 2.5 port: description: - - Port of the virtual server. Required when state=present and vs does - not exist. If you specify a value for this field, it must be a number - between 0 and 65535. - all_profiles: + - Port of the virtual server. Required when C(state) is C(present) + and virtual server does not exist. + - If you do not want to specify a particular port, use the value C(0). + The result is that the virtual server will listen on any port. + profiles: description: - - List of all Profiles (HTTP,ClientSSL,ServerSSL,etc) that must be used - by the virtual server. - all_policies: - description: - - List of all policies enabled for the virtual server. - version_added: "2.3" - all_rules: + - List of profiles (HTTP, ClientSSL, ServerSSL, etc) to apply to both sides + of the connection (client-side and server-side). + - If you only want to apply a particular profile to the client-side of + the connection, specify C(client-side) for the profile's C(context). + - If you only want to apply a particular profile to the server-side of + the connection, specify C(server-side) for the profile's C(context). + - If C(context) is not provided, it will default to C(all). + suboptions: + name: + description: + - Name of the profile. + - If this is not specified, then it is assumed that the profile item is + only a name of a profile. + - This must be specified if a context is specified. + required: false + context: + description: + - The side of the connection on which the profile should be applied. + choices: + - all + - server-side + - client-side + default: all + aliases: + - all_profiles + irules: + version_added: "2.2" description: - List of rules to be applied in priority order. - version_added: "2.2" + - If you want to remove existing iRules, specify a single empty value; C(""). + See the documentation for an example. + aliases: + - all_rules enabled_vlans: - description: - - List of vlans to be enabled. When a VLAN named C(ALL) is used, all - VLANs will be allowed. version_added: "2.2" + description: + - List of VLANs to be enabled. When a VLAN named C(all) is used, all + VLANs will be allowed. VLANs can be specified with or without the + leading partition. If the partition is not specified in the VLAN, + then the C(partition) option of this module will be used. + - This parameter is mutually exclusive with the C(disabled_vlans) parameter. + disabled_vlans: + version_added: 2.5 + description: + - List of VLANs to be disabled. If the partition is not specified in the VLAN, + then the C(partition) option of this module will be used. + - This parameter is mutually exclusive with the C(enabled_vlans) parameters. pool: description: - Default pool for the virtual server. + - If you want to remove the existing pool, specify an empty value; C(""). + See the documentation for an example. + policies: + description: + - Specifies the policies for the virtual server + aliases: + - all_policies snat: description: - Source network address policy. + required: false choices: - None - Automap - - Name of a SNAT pool (eg "/Common/snat_pool_name") to enable SNAT with the specific pool + - Name of a SNAT pool (eg "/Common/snat_pool_name") to enable SNAT + with the specific pool default_persistence_profile: description: - Default Profile which manages the session persistence. - fallback_persistence_profile: - description: - - Specifies the persistence profile you want the system to use if it - cannot use the specified default persistence profile. - version_added: "2.3" + - If you want to remove the existing default persistence profile, specify an + empty value; C(""). See the documentation for an example. route_advertisement_state: description: - Enable route advertisement for destination. - choices: [ disabled, enabled ] + - Deprecated in 2.4. Use the C(bigip_virtual_address) module instead. + choices: + - enabled + - disabled version_added: "2.3" description: description: - Virtual server description. + fallback_persistence_profile: + description: + - Specifies the persistence profile you want the system to use if it + cannot use the specified default persistence profile. + - If you want to remove the existing fallback persistence profile, specify an + empty value; C(""). See the documentation for an example. + version_added: 2.3 + partition: + description: + - Device partition to manage resources on. + default: Common + version_added: 2.5 + metdata: + description: + - Arbitrary key/value pairs that you can attach to a pool. This is useful in + situations where you might want to annotate a virtual to me managed by Ansible. + - Key names will be stored as strings; this includes names that are numbers. + - Values for all of the keys will be stored as strings; this includes values + that are numbers. + - Data will be persisted, not ephemeral. + version_added: 2.5 +notes: + - Requires BIG-IP software version >= 11 + - Requires the f5-sdk Python package on the host. This is as easy as pip + install f5-sdk. + - Requires the netaddr Python package on the host. This is as easy as pip + install netaddr. +requirements: + - f5-sdk + - netaddr extends_documentation_fragment: f5 +author: + - Tim Rupp (@caphrim007) ''' -EXAMPLES = ''' -- name: Add virtual server - bigip_virtual_server: - server: lb.mydomain.net - user: admin - password: secret - state: present - partition: MyPartition - name: myvirtualserver - destination: "{{ ansible_default_ipv4['address'] }}" - port: 443 - pool: "{{ mypool }}" - snat: Automap - description: Test Virtual Server - all_profiles: - - http - - clientssl - enabled_vlans: - - /Common/vlan2 - delegate_to: localhost - +EXAMPLES = r''' - name: Modify Port of the Virtual Server bigip_virtual_server: - server: lb.mydomain.net - user: admin - password: secret - state: present - partition: MyPartition - name: myvirtualserver - port: 8080 + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: Common + name: my-virtual-server + port: 8080 delegate_to: localhost - name: Delete virtual server bigip_virtual_server: - server: lb.mydomain.net - user: admin - password: secret - state: absent - partition: MyPartition - name: myvirtualserver + server: lb.mydomain.net + user: admin + password: secret + state: absent + partition: Common + name: my-virtual-server + delegate_to: localhost + +- name: Add virtual server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + state: present + partition: Common + name: my-virtual-server + destination: 10.10.10.10 + port: 443 + pool: my-pool + snat: Automap + description: Test Virtual Server + profiles: + - http + - fix + - name: clientssl + context: server-side + - name: ilx + context: client-side + policies: + - my-ltm-policy-for-asm + - ltm-uri-policy + - ltm-policy-2 + - ltm-policy-3 + enabled_vlans: + - /Common/vlan2 + delegate_to: localhost + +- name: Add FastL4 virtual server + bigip_virtual_server: + destination: 1.1.1.1 + name: fastl4_vs + port: 80 + profiles: + - fastL4 + state: present + +- name: Add iRules to the Virtual Server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + name: my-virtual-server + irules: + - irule1 + - irule2 + delegate_to: localhost + +- name: Remove one iRule from the Virtual Server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + name: my-virtual-server + irules: + - irule2 + delegate_to: localhost + +- name: Remove all iRules from the Virtual Server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + name: my-virtual-server + irules: "" + delegate_to: localhost + +- name: Remove pool from the Virtual Server + bigip_virtual_server: + server: lb.mydomain.net + user: admin + password: secret + name: my-virtual-server + pool: "" + delegate_to: localhost + +- name: Add metadata to virtual + bigip_pool: + server: lb.mydomain.com + user: admin + password: secret + state: absent + name: my-pool + partition: Common + metadata: + ansible: 2.4 + updated_at: 2017-12-20T17:50:46Z delegate_to: localhost ''' -RETURN = ''' ---- -deleted: - description: Name of a virtual server that was deleted - returned: changed - type: string - sample: "my-virtual-server" +RETURN = r''' +description: + description: New description of the virtual server. + returned: changed + type: string + sample: This is my description +default_persistence_profile: + description: Default persistence profile set on the virtual server. + returned: changed + type: string + sample: /Common/dest_addr +destination: + description: Destination of the virtual server. + returned: changed + type: string + sample: 1.1.1.1 +disabled: + description: Whether the virtual server is disabled, or not. + returned: changed + type: bool + sample: True +disabled_vlans: + description: List of VLANs that the virtual is disabled for. + returned: changed + type: list + sample: ['/Common/vlan1', '/Common/vlan2'] +enabled: + description: Whether the virtual server is enabled, or not. + returned: changed + type: bool + sample: False +enabled_vlans: + description: List of VLANs that the virtual is enabled for. + returned: changed + type: list + sample: ['/Common/vlan5', '/Common/vlan6'] +fallback_persistence_profile: + description: Fallback persistence profile set on the virtual server. + returned: changed + type: string + sample: /Common/source_addr +irules: + description: iRules set on the virtual server. + returned: changed + type: list + sample: ['/Common/irule1', '/Common/irule2'] +pool: + description: Pool that the virtual server is attached to. + returned: changed + type: string + sample: /Common/my-pool +policies: + description: List of policies attached to the virtual. + returned: changed + type: list + sample: ['/Common/policy1', '/Common/policy2'] +port: + description: Port that the virtual server is configured to listen on. + returned: changed + type: int + sample: 80 +profiles: + description: List of profiles set on the virtual server. + returned: changed + type: list + sample: [{'name': 'tcp', 'context': 'server-side'}, {'name': 'tcp-legacy', 'context': 'client-side'}] +snat: + description: SNAT setting of the virtual server. + returned: changed + type: string + sample: Automap +source: + description: Source address, in CIDR form, set on the virtual server. + returned: changed + type: string + sample: 1.2.3.4/32 +metadata: + description: The new value of the virtual. + returned: changed + type: dict + sample: {'key1': 'foo', 'key2': 'bar'} ''' -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.f5_utils import * +import re -# map of state values -STATES = { - 'enabled': 'STATE_ENABLED', - 'disabled': 'STATE_DISABLED' -} +from ansible.module_utils.f5_utils import AnsibleF5Client +from ansible.module_utils.f5_utils import AnsibleF5Parameters +from ansible.module_utils.f5_utils import HAS_F5SDK +from ansible.module_utils.f5_utils import F5ModuleError +from ansible.module_utils.six import iteritems +from collections import defaultdict +from collections import namedtuple -STATUSES = { - 'enabled': 'SESSION_STATUS_ENABLED', - 'disabled': 'SESSION_STATUS_DISABLED', - 'offline': 'SESSION_STATUS_FORCED_DISABLED' -} +try: + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError +except ImportError: + HAS_F5SDK = False + +try: + import netaddr + HAS_NETADDR = True +except ImportError: + HAS_NETADDR = False -def vs_exists(api, vs): - # hack to determine if pool exists - result = False - try: - api.LocalLB.VirtualServer.get_object_status(virtual_servers=[vs]) - result = True - except bigsuds.OperationFailed as e: - if "was not found" in str(e): - result = False - else: - # genuine exception - raise - return result +class Parameters(AnsibleF5Parameters): + def __init__(self, params=None): + self._values = defaultdict(lambda: None) + if params: + self.update(params=params) + def update(self, params=None): + if params: + for k, v in iteritems(params): + if self.api_map is not None and k in self.api_map: + map_key = self.api_map[k] + else: + map_key = k -def vs_create(api, name, destination, port, pool, profiles): - if profiles: - _profiles = [] - for profile in profiles: - _profiles.append( - dict( - profile_context='PROFILE_CONTEXT_TYPE_ALL', - profile_name=profile - ) - ) - else: - _profiles = [{'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': 'tcp'}] + # Handle weird API parameters like `dns.proxy.__iter__` by + # using a map provided by the module developer + class_attr = getattr(type(self), map_key, None) + if isinstance(class_attr, property): + # There is a mapped value for the api_map key + if class_attr.fset is None: + # If the mapped value does not have + # an associated setter + self._values[map_key] = v + else: + # The mapped value has a setter + setattr(self, map_key, v) + else: + # If the mapped value is not a @property + self._values[map_key] = v - # a bit of a hack to handle concurrent runs of this module. - # even though we've checked the vs doesn't exist, - # it may exist by the time we run create_vs(). - # this catches the exception and does something smart - # about it! - try: - api.LocalLB.VirtualServer.create( - definitions=[{'name': [name], 'address': [destination], 'port': port, 'protocol': 'PROTOCOL_TCP'}], - wildmasks=['255.255.255.255'], - resources=[{'type': 'RESOURCE_TYPE_POOL', 'default_pool_name': pool}], - profiles=[_profiles]) - created = True - return created - except bigsuds.OperationFailed as e: - raise Exception('Error on creating Virtual Server : %s' % e) + def to_return(self): + result = {} + for returnable in self.returnables: + try: + result[returnable] = getattr(self, returnable) + except Exception as ex: + pass + result = self._filter_params(result) + 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 -def vs_remove(api, name): - api.LocalLB.VirtualServer.delete_virtual_server( - virtual_servers=[name] - ) - - -def get_rules(api, name): - return api.LocalLB.VirtualServer.get_rule( - virtual_servers=[name] - )[0] - - -def set_rules(api, name, rules_list): - updated = False - if rules_list is None: - return False - rules_list = list(enumerate(rules_list)) - try: - current_rules = [(x['priority'], x['rule_name']) for x in get_rules(api, name)] - to_add_rules = [] - for i, x in rules_list: - if (i, x) not in current_rules: - to_add_rules.append({'priority': i, 'rule_name': x}) - to_del_rules = [] - for i, x in current_rules: - if (i, x) not in rules_list: - to_del_rules.append({'priority': i, 'rule_name': x}) - if len(to_del_rules) > 0: - api.LocalLB.VirtualServer.remove_rule( - virtual_servers=[name], - rules=[to_del_rules] - ) - updated = True - if len(to_add_rules) > 0: - api.LocalLB.VirtualServer.add_rule( - virtual_servers=[name], - rules=[to_add_rules] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting rules : %s' % e) - - -def get_profiles(api, name): - return api.LocalLB.VirtualServer.get_profile( - virtual_servers=[name] - )[0] - - -def set_profiles(api, name, profiles_list): - updated = False - - try: - if profiles_list is None: - return False - profiles_list = list(profiles_list) - current_profiles = list(map(lambda x: x['profile_name'], get_profiles(api, name))) - to_add_profiles = [] - for x in profiles_list: - if x not in current_profiles: - to_add_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - to_del_profiles = [] - for x in current_profiles: - if (x not in profiles_list) and (x != "/Common/tcp"): - to_del_profiles.append({'profile_context': 'PROFILE_CONTEXT_TYPE_ALL', 'profile_name': x}) - if len(to_del_profiles) > 0: - api.LocalLB.VirtualServer.remove_profile( - virtual_servers=[name], - profiles=[to_del_profiles] - ) - updated = True - if len(to_add_profiles) > 0: - api.LocalLB.VirtualServer.add_profile( - virtual_servers=[name], - profiles=[to_add_profiles] - ) - updated = True - current_profiles = list(map(lambda x: x['profile_name'], get_profiles(api, name))) - if len(current_profiles) == 0: - raise F5ModuleError( - "Virtual servers must has at least one profile" - ) - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting profiles : %s' % e) - - -def get_policies(api, name): - return api.LocalLB.VirtualServer.get_content_policy( - virtual_servers=[name] - )[0] - - -def set_policies(api, name, policies_list): - updated = False - try: - if policies_list is None: - return False - policies_list = list(policies_list) - current_policies = get_policies(api, name) - to_add_policies = [] - for x in policies_list: - if x not in current_policies: - to_add_policies.append(x) - to_del_policies = [] - for x in current_policies: - if x not in policies_list: - to_del_policies.append(x) - if len(to_del_policies) > 0: - api.LocalLB.VirtualServer.remove_content_policy( - virtual_servers=[name], - policies=[to_del_policies] - ) - updated = True - if len(to_add_policies) > 0: - api.LocalLB.VirtualServer.add_content_policy( - virtual_servers=[name], - policies=[to_add_policies] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting policies : %s' % e) - - -def get_vlan(api, name): - return api.LocalLB.VirtualServer.get_vlan( - virtual_servers=[name] - )[0] - - -def set_enabled_vlans(api, name, vlans_enabled_list): - updated = False - to_add_vlans = [] - try: - if vlans_enabled_list is None: - return updated - vlans_enabled_list = list(vlans_enabled_list) - current_vlans = get_vlan(api, name) - - # Set allowed list back to default ("all") - # - # This case allows you to undo what you may have previously done. - # The default case is "All VLANs and Tunnels". This case will handle - # that situation. - if 'ALL' in vlans_enabled_list: - # The user is coming from a situation where they previously - # were specifying a list of allowed VLANs - if len(current_vlans['vlans']) > 0 or \ - current_vlans['state'] is "STATE_ENABLED": - api.LocalLB.VirtualServer.set_vlan( - virtual_servers=[name], - vlans=[{'state': 'STATE_DISABLED', 'vlans': []}] - ) - updated = True - else: - if current_vlans['state'] is "STATE_DISABLED": - to_add_vlans = vlans_enabled_list + def api_params(self): + result = {} + for api_attribute in self.api_attributes: + if self.api_map is not None and api_attribute in self.api_map: + result[api_attribute] = getattr(self, self.api_map[api_attribute]) else: - for vlan in vlans_enabled_list: - if vlan not in current_vlans['vlans']: - updated = True - to_add_vlans = vlans_enabled_list - break - if updated: - api.LocalLB.VirtualServer.set_vlan( - virtual_servers=[name], - vlans=[{ - 'state': 'STATE_ENABLED', - 'vlans': [to_add_vlans] - }] + result[api_attribute] = getattr(self, api_attribute) + result = self._filter_params(result) + return result + + +class VirtualAddressParameters(Parameters): + api_map = { + 'routeAdvertisement': 'route_advertisement_state' + } + returnables = [ + 'route_advertisement_state' + ] + + updatables = [ + 'route_advertisement_state' + ] + + api_attributes = [ + 'routeAdvertisement' + ] + + +class VirtualAddressModuleParameters(VirtualAddressParameters): + @property + def route_advertisement_state(self): + # TODO: Remove in 2.5 + if self._values['route_advertisement_state'] is None: + return None + if self._values['__warnings'] is None: + self._values['__warnings'] = [] + self._values['__warnings'].append( + dict( + msg="Usage of the 'route_advertisement_state' parameter is deprecated. Use the bigip_virtual_address module instead", + version='2.4' + ) + ) + return str(self._values['route_advertisement_state']) + + +class VirtualAddressApiParameters(VirtualAddressParameters): + pass + + +class VirtualServerParameters(Parameters): + api_map = { + 'sourceAddressTranslation': 'snat', + 'fallbackPersistence': 'fallback_persistence_profile', + 'persist': 'default_persistence_profile', + 'vlansEnabled': 'vlans_enabled', + 'vlansDisabled': 'vlans_disabled', + 'profilesReference': 'profiles', + 'policiesReference': 'policies', + 'rules': 'irules' + } + + api_attributes = [ + 'description', + 'destination', + 'disabled', + 'enabled', + 'fallbackPersistence', + 'metadata', + 'persist', + 'policies', + 'pool', + 'profiles', + 'rules', + 'source', + 'sourceAddressTranslation', + 'vlans', + 'vlansEnabled', + 'vlansDisabled', + ] + + updatables = [ + 'description', + 'default_persistence_profile', + 'destination', + 'disabled_vlans', + 'enabled', + 'enabled_vlans', + 'fallback_persistence_profile', + 'irules', + 'metadata', + 'pool', + 'policies', + 'port', + 'profiles', + 'snat', + 'source' + ] + + returnables = [ + 'description', + 'default_persistence_profile', + 'destination', + 'disabled', + 'disabled_vlans', + 'enabled', + 'enabled_vlans', + 'fallback_persistence_profile', + 'irules', + 'metadata', + 'pool', + 'policies', + 'port', + 'profiles', + 'snat', + 'source', + 'vlans', + 'vlans_enabled', + 'vlans_disabled' + ] + + def __init__(self, params=None): + super(VirtualServerParameters, self).__init__(params) + self.profiles_mutex = [ + 'sip', 'sipsession', 'iiop', 'rtsp', 'http', 'diameter', + 'diametersession', 'radius', 'ftp', 'tftp', 'dns', 'pptp', 'fix' + ] + + def is_valid_ip(self, value): + try: + netaddr.IPAddress(value) + return True + except (netaddr.core.AddrFormatError, ValueError): + return False + + def _format_port_for_destination(self, ip, port): + addr = netaddr.IPAddress(ip) + if addr.version == 6: + if port == 0: + result = '.any' + else: + result = '.{0}'.format(port) + else: + result = ':{0}'.format(port) + return result + + def _format_destination(self, address, port, route_domain): + if port is None: + if route_domain is None: + result = '{0}'.format( + self._fqdn_name(address) ) - - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting enabled vlans : %s' % e) + else: + result = '{0}%{1}'.format( + self._fqdn_name(address), + route_domain + ) + else: + port = self._format_port_for_destination(address, port) + if route_domain is None: + result = '{0}{1}'.format( + self._fqdn_name(address), + port + ) + else: + result = '{0}%{1}{2}'.format( + self._fqdn_name(address), + route_domain, + port + ) + return result -def set_snat(api, name, snat): - updated = False - try: - current_state = get_snat_type(api, name) - current_snat_pool = get_snat_pool(api, name) - if snat is None: - return updated - elif snat == 'None' and current_state != 'SRC_TRANS_NONE': - api.LocalLB.VirtualServer.set_source_address_translation_none( - virtual_servers=[name] +class VirtualServerApiParameters(VirtualServerParameters): + @property + def destination(self): + if self._values['destination'] is None: + return None + destination = self.destination_tuple + result = self._format_destination(destination.ip, destination.port, destination.route_domain) + return result + + @property + def source(self): + if self._values['source'] is None: + return None + try: + addr = netaddr.IPNetwork(self._values['source']) + result = '{0}/{1}'.format(str(addr.ip), addr.prefixlen) + return result + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "The source IP address must be specified in CIDR format: address/prefix" ) - updated = True - elif snat == 'Automap' and current_state != 'SRC_TRANS_AUTOMAP': - api.LocalLB.VirtualServer.set_source_address_translation_automap( - virtual_servers=[name] + + @property + def destination_tuple(self): + Destination = namedtuple('Destination', ['ip', 'port', 'route_domain']) + + # Remove the partition + if self._values['destination'] is None: + result = Destination(ip=None, port=None, route_domain=None) + return result + destination = re.sub(r'^/[a-zA-Z_.-]+/', '', self._values['destination']) + + if self.is_valid_ip(destination): + result = Destination( + ip=destination, + port=None, + route_domain=None ) - updated = True - elif snat_settings_need_updating(snat, current_state, current_snat_pool): - api.LocalLB.VirtualServer.set_source_address_translation_snat_pool( - virtual_servers=[name], - pools=[snat] + return result + + # Covers the following examples + # + # /Common/2700:bc00:1f10:101::6%2.80 + # 2700:bc00:1f10:101::6%2.80 + # 1.1.1.1%2:80 + # /Common/1.1.1.1%2:80 + # /Common/2700:bc00:1f10:101::6%2.any + # + pattern = r'(?P[^%]+)%(?P[0-9]+)[:.](?P[0-9]+|any)' + matches = re.search(pattern, destination) + if matches: + try: + port = int(matches.group('port')) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + port = matches.group('port') + if port == 'any': + port = 0 + ip = matches.group('ip') + if not self.is_valid_ip(ip): + raise F5ModuleError( + "The provided destination is not a valid IP address" + ) + result = Destination( + ip=matches.group('ip'), + port=port, + route_domain=int(matches.group('route_domain')) ) - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting snat : %s' % e) + return result + pattern = r'(?P[^%]+)%(?P[0-9]+)' + matches = re.search(pattern, destination) + if matches: + ip = matches.group('ip') + if not self.is_valid_ip(ip): + raise F5ModuleError( + "The provided destination is not a valid IP address" + ) + result = Destination( + ip=matches.group('ip'), + port=None, + route_domain=int(matches.group('route_domain')) + ) + return result -def get_snat_type(api, name): - return api.LocalLB.VirtualServer.get_source_address_translation_type( - virtual_servers=[name] - )[0] + parts = destination.split('.') + if len(parts) == 4: + # IPv4 + ip, port = destination.split(':') + if not self.is_valid_ip(ip): + raise F5ModuleError( + "The provided destination is not a valid IP address" + ) + result = Destination( + ip=ip, + port=int(port), + route_domain=None + ) + return result + elif len(parts) == 2: + # IPv6 + ip, port = destination.split('.') + try: + port = int(port) + except ValueError: + # Can be a port of "any". This only happens with IPv6 + if port == 'any': + port = 0 + if not self.is_valid_ip(ip): + raise F5ModuleError( + "The provided destination is not a valid IP address" + ) + result = Destination( + ip=ip, + port=port, + route_domain=None + ) + return result + else: + result = Destination(ip=None, port=None, route_domain=None) + return result + @property + def port(self): + destination = self.destination_tuple + self._values['port'] = destination.port + return destination.port -def get_snat_pool(api, name): - return api.LocalLB.VirtualServer.get_source_address_translation_snat_pool( - virtual_servers=[name] - )[0] + @property + def route_domain(self): + destination = self.destination_tuple + self._values['route_domain'] = destination.route_domain + return destination.route_domain + @property + def profiles(self): + if 'items' not in self._values['profiles']: + return None + result = [] + for item in self._values['profiles']['items']: + context = item['context'] + name = item['name'] + if context in ['all', 'serverside', 'clientside']: + result.append(dict(name=name, context=context, fullPath=item['fullPath'])) + else: + raise F5ModuleError( + "Unknown profile context found: '{0}'".format(context) + ) + return result -def snat_settings_need_updating(snat, current_state, current_snat_pool): - if snat == 'None' or snat == 'Automap': + @property + def policies(self): + if 'items' not in self._values['policies']: + return None + result = [] + for item in self._values['policies']['items']: + name = item['name'] + partition = item['partition'] + result.append(dict(name=name, partition=partition)) + return result + + @property + def default_persistence_profile(self): + if self._values['default_persistence_profile'] is None: + return None + # These persistence profiles are always lists when we get them + # from the REST API even though there can only be one. We'll + # make it a list again when we get to the Difference engine. + return self._values['default_persistence_profile'][0] + + @property + def enabled(self): + if 'enabled' in self._values: + return True + else: + return False + + @property + def disabled(self): + if 'disabled' in self._values: + return True return False - elif snat and current_state != 'SRC_TRANS_SNATPOOL': + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + result = [] + for md in self._values['metadata']: + tmp = dict(name=str(md['name'])) + if 'value' in md: + tmp['value'] = str(md['value']) + else: + tmp['value'] = '' + result.append(tmp) + return result + + +class VirtualServerModuleParameters(VirtualServerParameters): + def _handle_profile_context(self, tmp): + if 'context' not in tmp: + tmp['context'] = 'all' + else: + if 'name' not in tmp: + raise F5ModuleError( + "A profile name must be specified when a context is specified." + ) + tmp['context'] = tmp['context'].replace('server-side', 'serverside') + tmp['context'] = tmp['context'].replace('client-side', 'clientside') + + def _handle_clientssl_profile_nuances(self, profile): + if profile['name'] != 'clientssl': + return + if profile['context'] != 'clientside': + profile['context'] = 'clientside' + + @property + def destination(self): + addr = self._values['destination'].split("%")[0] + if not self.is_valid_ip(addr): + raise F5ModuleError( + "The provided destination is not a valid IP address" + ) + result = self._format_destination(addr, self.port, self.route_domain) + return result + + @property + def destination_tuple(self): + Destination = namedtuple('Destination', ['ip', 'port', 'route_domain']) + if self._values['destination'] is None: + result = Destination(ip=None, port=None, route_domain=None) + return result + addr = self._values['destination'].split("%")[0] + result = Destination(ip=addr, port=self.port, route_domain=self.route_domain) + return result + + @property + def source(self): + if self._values['source'] is None: + return None + try: + addr = netaddr.IPNetwork(self._values['source']) + result = '{0}/{1}'.format(str(addr.ip), addr.prefixlen) + return result + except netaddr.core.AddrFormatError: + raise F5ModuleError( + "The source IP address must be specified in CIDR format: address/prefix" + ) + + @property + def port(self): + if self._values['port'] is None: + return None + if self._values['port'] in ['*', 'any']: + return 0 + self._check_port() + return int(self._values['port']) + + def _check_port(self): + try: + port = int(self._values['port']) + except ValueError: + raise F5ModuleError( + "The specified port was not a valid integer" + ) + if 0 <= port <= 65535: + return port + raise F5ModuleError( + "Valid ports must be in range 0 - 65535" + ) + + @property + def irules(self): + results = [] + if self._values['irules'] is None: + return None + if len(self._values['irules']) == 1 and self._values['irules'][0] == '': + return '' + for irule in self._values['irules']: + result = self._fqdn_name(irule) + results.append(result) + return results + + @property + def profiles(self): + if self._values['profiles'] is None: + return None + if len(self._values['profiles']) == 1 and self._values['profiles'][0] == '': + return '' + result = [] + for profile in self._values['profiles']: + tmp = dict() + if isinstance(profile, dict): + tmp.update(profile) + self._handle_profile_context(tmp) + if 'name' not in profile: + tmp['name'] = profile + tmp['fullPath'] = self._fqdn_name(tmp['name']) + self._handle_clientssl_profile_nuances(tmp) + else: + tmp['name'] = profile + tmp['context'] = 'all' + tmp['fullPath'] = self._fqdn_name(tmp['name']) + self._handle_clientssl_profile_nuances(tmp) + result.append(tmp) + mutually_exclusive = [x['name'] for x in result if x in self.profiles_mutex] + if len(mutually_exclusive) > 1: + raise F5ModuleError( + "Profiles {0} are mutually exclusive".format( + ', '.join(self.profiles_mutex).strip() + ) + ) + return result + + @property + def policies(self): + if self._values['policies'] is None: + return None + if len(self._values['policies']) == 1 and self._values['policies'][0] == '': + return '' + result = [] + policies = [self._fqdn_name(p) for p in self._values['policies']] + policies = set(policies) + for policy in policies: + parts = policy.split('/') + if len(parts) != 3: + raise F5ModuleError( + "The specified policy '{0}' is malformed".format(policy) + ) + tmp = dict( + name=parts[2], + partition=parts[1] + ) + result.append(tmp) + return result + + @property + def pool(self): + if self._values['pool'] is None: + return None + if self._values['pool'] == '': + return '' + return self._fqdn_name(self._values['pool']) + + @property + def vlans_enabled(self): + if self._values['enabled_vlans'] is None: + return None + elif self._values['vlans_enabled'] is False: + # This is a special case for "all" enabled VLANs + return False + if self._values['disabled_vlans'] is None: + return True + return False + + @property + def vlans_disabled(self): + if self._values['disabled_vlans'] is None: + return None + elif self._values['vlans_disabled'] is True: + # This is a special case for "all" enabled VLANs + return True + elif self._values['enabled_vlans'] is None: + return True + return False + + @property + def enabled_vlans(self): + if self._values['enabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['enabled_vlans'] if x.lower() in ['all', '*']): + result = [self._fqdn_name('all')] + if result[0].endswith('/all'): + if self._values['__warnings'] is None: + self._values['__warnings'] = [] + self._values['__warnings'].append( + dict( + msg="Usage of the 'ALL' value for 'enabled_vlans' parameter is deprecated. Use '*' instead", + version='2.5' + ) + ) + return result + results = list(set([self._fqdn_name(x) for x in self._values['enabled_vlans']])) + results.sort() + return results + + @property + def disabled_vlans(self): + if self._values['disabled_vlans'] is None: + return None + elif any(x.lower() for x in self._values['disabled_vlans'] if x.lower() in ['all', '*']): + raise F5ModuleError( + "You cannot disable all VLANs. You must name them individually." + ) + results = list(set([self._fqdn_name(x) for x in self._values['disabled_vlans']])) + results.sort() + return results + + @property + def vlans(self): + disabled = self.disabled_vlans + if disabled: + return self.disabled_vlans + return self.enabled_vlans + + @property + def state(self): + if self._values['state'] == 'present': + return 'enabled' + return self._values['state'] + + @property + def snat(self): + if self._values['snat'] is None: + return None + lowercase = self._values['snat'].lower() + if lowercase in ['automap', 'none']: + return dict(type=lowercase) + snat_pool = self._fqdn_name(self._values['snat']) + return dict(pool=snat_pool, type='snat') + + @property + def default_persistence_profile(self): + if self._values['default_persistence_profile'] is None: + return None + if self._values['default_persistence_profile'] == '': + return '' + profile = self._fqdn_name(self._values['default_persistence_profile']) + parts = profile.split('/') + if len(parts) != 3: + raise F5ModuleError( + "The specified 'default_persistence_profile' is malformed" + ) + result = dict( + name=parts[2], + partition=parts[1] + ) + return result + + @property + def fallback_persistence_profile(self): + if self._values['fallback_persistence_profile'] is None: + return None + if self._values['fallback_persistence_profile'] == '': + return '' + result = self._fqdn_name(self._values['fallback_persistence_profile']) + return result + + @property + def enabled(self): + if self._values['state'] == 'enabled': + return True + elif self._values['state'] == 'disabled': + return False + else: + return None + + @property + def disabled(self): + if self._values['state'] == 'enabled': + return False + elif self._values['state'] == 'disabled': + return True + else: + return None + + @property + def metadata(self): + if self._values['metadata'] is None: + return None + if self._values['metadata'] == '': + return [] + result = [] + try: + for k, v in iteritems(self._values['metadata']): + tmp = dict(name=str(k)) + if v: + tmp['value'] = str(v) + else: + tmp['value'] = '' + result.append(tmp) + except AttributeError: + raise F5ModuleError( + "The 'metadata' parameter must be a dictionary of key/value pairs." + ) + return result + + +class VirtualServerUsableChanges(VirtualServerParameters): + @property + def vlans(self): + if self._values['vlans'] is None: + return None + elif len(self._values['vlans']) == 0: + return [] + elif any(x for x in self._values['vlans'] if x.lower() in ['/common/all', 'all']): + return [] + return self._values['vlans'] + + +class VirtualAddressUsableChanges(VirtualAddressParameters): + pass + + +class VirtualServerReportableChanges(VirtualServerParameters): + @property + def snat(self): + if self._values['snat'] is None: + return None + result = self._values['snat'].get('type', None) + if result == 'automap': + return 'Automap' + elif result == 'none': + return 'none' + result = self._values['snat'].get('pool', None) + return result + + @property + def destination(self): + params = VirtualServerApiParameters(dict(destination=self._values['destination'])) + result = params.destination_tuple.ip + return result + + @property + def port(self): + params = VirtualServerApiParameters(dict(destination=self._values['destination'])) + result = params.destination_tuple.port + return result + + @property + def default_persistence_profile(self): + if len(self._values['default_persistence_profile']) == 0: + return [] + profile = self._values['default_persistence_profile'][0] + result = '/{0}/{1}'.format(profile['partition'], profile['name']) + return result + + @property + def policies(self): + if len(self._values['policies']) == 0: + return [] + result = ['/{0}/{1}'.format(x['partition'], x['name']) for x in self._values['policies']] + return result + + @property + def enabled_vlans(self): + if len(self._values['vlans']) == 0 and self._values['vlans_disabled'] is True: + return 'all' + elif len(self._values['vlans']) > 0 and self._values['vlans_enabled'] is True: + return self._values['vlans'] + + @property + def disabled_vlans(self): + if len(self._values['vlans']) > 0 and self._values['vlans_disabled'] is True: + return self._values['vlans'] + + +class VirtualAddressReportableChanges(VirtualAddressParameters): + pass + + +class Difference(object): + def __init__(self, want, have=None): + self.have = have + self.want = want + + 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 + + def to_tuple(self, items): + result = [] + for x in items: + tmp = [(str(k), str(v)) for k, v in iteritems(x)] + result += tmp + return result + + def _diff_complex_items(self, want, have): + if want == [] and have is None: + return None + if want is None: + return None + w = self.to_tuple(want) + h = self.to_tuple(have) + if set(w).issubset(set(h)): + return None + else: + return want + + def _update_vlan_status(self, result): + if self.want.vlans_disabled is not None: + if self.want.vlans_disabled != self.have.vlans_disabled: + result['vlans_disabled'] = self.want.vlans_disabled + result['vlans_enabled'] = not self.want.vlans_disabled + elif self.want.vlans_enabled is not None: + if any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans_enabled is True: + result['vlans_disabled'] = True + result['vlans_enabled'] = False + elif self.want.vlans_enabled != self.have.vlans_enabled: + result['vlans_disabled'] = not self.want.vlans_enabled + result['vlans_enabled'] = self.want.vlans_enabled + + @property + def destination(self): + addr_tuple = [self.want.destination, self.want.port, self.want.route_domain] + if all(x for x in addr_tuple if x is None): + return None + + have = self.have.destination_tuple + + if self.want.port is None: + self.want.update({'port': have.port}) + if self.want.route_domain is None: + self.want.update({'route_domain': have.route_domain}) + if self.want.destination_tuple.ip is None: + address = have.ip + else: + address = self.want.destination_tuple.ip + + want = self.want._format_destination(address, self.want.port, self.want.route_domain) + if want != self.have.destination: + return self.want._fqdn_name(want) + + @property + def source(self): + if self.want.source is None: + return None + want = netaddr.IPNetwork(self.want.source) + have = netaddr.IPNetwork(self.have.destination_tuple.ip) + if want.version != have.version: + raise F5ModuleError( + "The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)." + ) + if self.want.source != self.have.source: + return self.want.source + + @property + def vlans(self): + if self.want.vlans is None: + return None + elif self.want.vlans == [] and self.have.vlans is None: + return None + elif self.want.vlans == self.have.vlans: + return None + + # Specifically looking for /all because the vlans return value will be + # an FQDN list. This means that "all" will be returned as "/partition/all", + # ex, /Common/all. + # + # We do not want to accidentally match values that would end with the word + # "all", like "vlansall". Therefore we look for the forward slash because this + # is a path delimiter. + elif any(x.lower().endswith('/all') for x in self.want.vlans): + if self.have.vlans is None: + return None + else: + return [] + else: + return self.want.vlans + + @property + def enabled_vlans(self): + return self.vlan_status + + @property + def disabled_vlans(self): + return self.vlan_status + + @property + def vlan_status(self): + result = dict() + vlans = self.vlans + if vlans is not None: + result['vlans'] = vlans + self._update_vlan_status(result) + return result + + @property + def port(self): + result = self.destination + if result is not None: + return dict( + destination=result + ) + + @property + def profiles(self): + if self.want.profiles is None: + return None + if self.want.profiles == '' and len(self.have.profiles) > 0: + have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles]) + if len(self.have.profiles) == 1: + if not any(x[0] in ['tcp', 'udp', 'sctp'] for x in have): + return [] + else: + return None + else: + return [] + if self.want.profiles == '' and len(self.have.profiles) == 0: + return None + want = set([(p['name'], p['context'], p['fullPath']) for p in self.want.profiles]) + have = set([(p['name'], p['context'], p['fullPath']) for p in self.have.profiles]) + if len(have) == 0: + return self.want.profiles + elif len(have) == 1: + if want != have: + return self.want.profiles + else: + if not any(x[0] == 'tcp' for x in want): + have = set([x for x in have if x[0] != 'tcp']) + if not any(x[0] == 'udp' for x in want): + have = set([x for x in have if x[0] != 'udp']) + if not any(x[0] == 'sctp' for x in want): + have = set([x for x in have if x[0] != 'sctp']) + want = set([(p[2], p[1]) for p in want]) + have = set([(p[2], p[1]) for p in have]) + if want != have: + return self.want.profiles + + @property + def fallback_persistence_profile(self): + if self.want.fallback_persistence_profile is None: + return None + if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is not None: + return "" + if self.want.fallback_persistence_profile == '' and self.have.fallback_persistence_profile is None: + return None + if self.want.fallback_persistence_profile != self.have.fallback_persistence_profile: + return self.want.fallback_persistence_profile + + @property + def default_persistence_profile(self): + if self.want.default_persistence_profile is None: + return None + if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is not None: + return [] + if self.want.default_persistence_profile == '' and self.have.default_persistence_profile is None: + return None + if self.have.default_persistence_profile is None: + return [self.want.default_persistence_profile] + w_name = self.want.default_persistence_profile.get('name', None) + w_partition = self.want.default_persistence_profile.get('partition', None) + h_name = self.have.default_persistence_profile.get('name', None) + h_partition = self.have.default_persistence_profile.get('partition', None) + if w_name != h_name or w_partition != h_partition: + return [self.want.default_persistence_profile] + + @property + def policies(self): + if self.want.policies is None: + return None + if self.want.policies == '' and self.have.policies is None: + return None + if self.want.policies == '' and len(self.have.policies) > 0: + return [] + if not self.have.policies: + return self.want.policies + want = set([(p['name'], p['partition']) for p in self.want.policies]) + have = set([(p['name'], p['partition']) for p in self.have.policies]) + if not want == have: + return self.want.policies + + @property + def snat(self): + if self.want.snat is None: + return None + if self.want.snat['type'] != self.have.snat['type']: + result = dict(snat=self.want.snat) + return result + + if self.want.snat.get('pool', None) is None: + return None + + if self.want.snat['pool'] != self.have.snat['pool']: + result = dict(snat=self.want.snat) + return result + + @property + def enabled(self): + if self.want.state == 'enabled' and self.have.disabled: + result = dict( + enabled=True, + disabled=False + ) + return result + elif self.want.state == 'disabled' and self.have.enabled: + result = dict( + enabled=False, + disabled=True + ) + return result + + @property + def irules(self): + if self.want.irules is None: + return None + if self.want.irules == '' and len(self.have.irules) > 0: + return [] + if self.want.irules == '' and len(self.have.irules) == 0: + return None + if sorted(set(self.want.irules)) != sorted(set(self.have.irules)): + return self.want.irules + + @property + def pool(self): + if self.want.pool is None: + return None + if self.want.pool == '' and self.have.pool is not None: + return "" + if self.want.pool == '' and self.have.pool is None: + return None + if self.want.pool != self.have.pool: + return self.want.pool + + @property + def metadata(self): + if self.want.metadata is None: + return None + elif len(self.want.metadata) == 0 and self.have.metadata is None: + return None + elif len(self.want.metadata) == 0: + return [] + elif self.have.metadata is None: + return self.want.metadata + result = self._diff_complex_items(self.want.metadata, self.have.metadata) + return result + + +class ModuleManager(object): + def __init__(self, client): + self.client = client + + def exec_module(self): + managers = list() + managers.append(self.get_manager('virtual_server')) + if self.client.module.params['route_advertisement_state'] is not None: + managers.append(self.get_manager('virtual_address')) + result = self.execute_managers(managers) + return result + + def execute_managers(self, managers): + results = dict(changed=False) + for manager in managers: + result = manager.exec_module() + for k, v in iteritems(result): + if k == 'changed': + if v is True: + results['changed'] = True + else: + results[k] = v + return results + + def get_manager(self, type): + vsm = VirtualServerManager(self.client) + if type == 'virtual_server': + return vsm + elif type == 'virtual_address': + self.set_name_of_virtual_address() + result = VirtualAddressManager(self.client) + return result + + def set_name_of_virtual_address(self): + mgr = VirtualServerManager(self.client) + params = mgr.read_current_from_device() + destination = params.destination_tuple + self.client.module.params['name'] = destination.ip + + +class BaseManager(object): + def __init__(self, client): + self.client = client + self.have = None + + def exec_module(self): + changed = False + 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)) + + reportable = self.get_reportable_changes() + 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: + return self.create() + + def absent(self): + if self.exists(): + return self.remove() + return False + + def update(self): + self.have = self.read_current_from_device() + if not self.should_update(): + return False + if self.client.check_mode: + return True + self.update_on_device() return True - elif snat and current_state == 'SRC_TRANS_SNATPOOL' and current_snat_pool != snat: + + def create(self): + if self.client.check_mode: + return True + + # This must be changed back to a list to make a valid REST API + # value. The module manipulates this as a normal dictionary + if self.want.default_persistence_profile is not None: + self.want.update({'default_persistence_profile': [self.want.default_persistence_profile]}) + + self.create_on_device() return True - else: + + def should_update(self): + result = self._update_changed_options() + if result: + return True return False - -def get_pool(api, name): - return api.LocalLB.VirtualServer.get_default_pool_name( - virtual_servers=[name] - )[0] + def remove(self): + if self.client.check_mode: + return True + self.remove_from_device() + if self.exists(): + raise F5ModuleError("Failed to delete the resource") + return True -def set_pool(api, name, pool): - updated = False - try: - current_pool = get_pool(api, name) - if pool is not None and (pool != current_pool): - api.LocalLB.VirtualServer.set_default_pool_name( - virtual_servers=[name], - default_pools=[pool] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting pool : %s' % e) +class VirtualServerManager(BaseManager): + def __init__(self, client): + super(VirtualServerManager, self).__init__(client) + self.have = None + self.want = VirtualServerModuleParameters(self.client.module.params) + self.changes = VirtualServerUsableChanges() + def get_reportable_changes(self): + result = VirtualServerReportableChanges(self.changes.to_return()) + return result -def get_destination(api, name): - return api.LocalLB.VirtualServer.get_destination_v2( - virtual_servers=[name] - )[0] + def _set_changed_options(self): + changed = {} + for key in VirtualServerParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = VirtualServerUsableChanges(changed) - -def set_destination(api, name, destination): - updated = False - try: - current_destination = get_destination(api, name) - if destination is not None and destination != current_destination['address']: - api.LocalLB.VirtualServer.set_destination_v2( - virtual_servers=[name], - destinations=[{'address': destination, 'port': current_destination['port']}] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting destination : %s' % e) - - -def set_port(api, name, port): - updated = False - try: - current_destination = get_destination(api, name) - if port is not None and port != current_destination['port']: - api.LocalLB.VirtualServer.set_destination_v2( - virtual_servers=[name], - destinations=[{'address': current_destination['address'], 'port': port}] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting port : %s' % e) - - -def get_state(api, name): - return api.LocalLB.VirtualServer.get_enabled_state( - virtual_servers=[name] - )[0] - - -def set_state(api, name, state): - updated = False - try: - current_state = get_state(api, name) - # We consider that being present is equivalent to enabled - if state == 'present': - state = 'enabled' - if STATES[state] != current_state: - api.LocalLB.VirtualServer.set_enabled_state( - virtual_servers=[name], - states=[STATES[state]] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting state : %s' % e) - - -def get_description(api, name): - return api.LocalLB.VirtualServer.get_description( - virtual_servers=[name] - )[0] - - -def set_description(api, name, description): - updated = False - try: - current_description = get_description(api, name) - if description is not None and current_description != description: - api.LocalLB.VirtualServer.set_description( - virtual_servers=[name], - descriptions=[description] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting description : %s ' % e) - - -def get_persistence_profiles(api, name): - return api.LocalLB.VirtualServer.get_persistence_profile( - virtual_servers=[name] - )[0] - - -def set_default_persistence_profiles(api, name, persistence_profile): - updated = False - if persistence_profile is None: - return updated - try: - current_persistence_profiles = get_persistence_profiles(api, name) - default = None - for profile in current_persistence_profiles: - if profile['default_profile']: - default = profile['profile_name'] - break - if default is not None and default != persistence_profile: - api.LocalLB.VirtualServer.remove_persistence_profile( - virtual_servers=[name], - profiles=[[{'profile_name': default, 'default_profile': True}]] - ) - if default != persistence_profile: - api.LocalLB.VirtualServer.add_persistence_profile( - virtual_servers=[name], - profiles=[[{'profile_name': persistence_profile, 'default_profile': True}]] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting default persistence profile : %s' % e) - - -def get_fallback_persistence_profile(api, name): - return api.LocalLB.VirtualServer.get_fallback_persistence_profile( - virtual_servers=[name] - )[0] - - -def set_fallback_persistence_profile(api, partition, name, persistence_profile): - updated = False - if persistence_profile is None: - return updated - try: - # This is needed because the SOAP API expects this to be an "empty" - # value to set the fallback profile to "None". The fq_name function - # does not take "None" into account though, so I do that here. - if persistence_profile != "": - persistence_profile = fq_name(partition, persistence_profile) - - current_fallback_profile = get_fallback_persistence_profile(api, name) - - if current_fallback_profile != persistence_profile: - api.LocalLB.VirtualServer.set_fallback_persistence_profile( - virtual_servers=[name], - profile_names=[persistence_profile] - ) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting fallback persistence profile : %s' % e) - - -def get_route_advertisement_status(api, address): - result = None - results = api.LocalLB.VirtualAddressV2.get_route_advertisement_state(virtual_addresses=[address]) - if results: - result = results.pop(0) - result = result.split("STATE_")[-1].lower() - return result - - -def set_route_advertisement_state(api, destination, partition, route_advertisement_state): - updated = False - - if route_advertisement_state is None: + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = VirtualServerParameters.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 = VirtualServerUsableChanges(changed) + return True return False + def exists(self): + result = self.client.api.tm.ltm.virtuals.virtual.exists( + name=self.want.name, + partition=self.want.partition + ) + return result + + def create(self): + required_resources = ['destination', 'port'] + + self._set_changed_options() + if self.want.destination is None: + raise F5ModuleError( + "'destination' must be specified when creating a virtual server" + ) + if all(getattr(self.want, v) is None for v in required_resources): + raise F5ModuleError( + "You must specify both of " + ', '.join(required_resources) + ) + if self.want.enabled_vlans is not None: + if any(x for x in self.want.enabled_vlans if x.lower() in ['/common/all', 'all']): + self.want.update( + dict( + enabled_vlans=[], + vlans_disabled=True, + vlans_enabled=False + ) + ) + if self.want.source and self.want.destination: + want = netaddr.IPNetwork(self.want.source) + have = netaddr.IPNetwork(self.want.destination_tuple.ip) + if want.version != have.version: + raise F5ModuleError( + "The source and destination addresses for the virtual server must be be the same type (IPv4 or IPv6)." + ) + return super(VirtualServerManager, self).create() + + 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 + ) + resource.modify(**params) + + 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' + ) + ) + ) + params = result.attrs + params.update(dict(kind=result.to_dict().get('kind', None))) + result = VirtualServerApiParameters(params) + return result + + def create_on_device(self): + params = self.want.api_params() + self.client.api.tm.ltm.virtuals.virtual.create( + name=self.want.name, + partition=self.want.partition, + **params + ) + + def remove_from_device(self): + resource = self.client.api.tm.ltm.virtuals.virtual.load( + name=self.want.name, + partition=self.want.partition + ) + if resource: + resource.delete() + + +class VirtualAddressManager(BaseManager): + def __init__(self, client): + super(VirtualAddressManager, self).__init__(client) + self.want = VirtualAddressModuleParameters(self.client.module.params) + self.have = VirtualAddressApiParameters() + self.changes = VirtualAddressUsableChanges() + + def get_reportable_changes(self): + result = VirtualAddressReportableChanges(self.changes.to_return()) + return result + + def _set_changed_options(self): + changed = {} + for key in VirtualAddressParameters.returnables: + if getattr(self.want, key) is not None: + changed[key] = getattr(self.want, key) + if changed: + self.changes = VirtualAddressUsableChanges(changed) + + def _update_changed_options(self): + diff = Difference(self.want, self.have) + updatables = VirtualAddressParameters.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 = VirtualAddressUsableChanges(changed) + return True + return False + + def read_current_from_device(self): + result = self.client.api.tm.ltm.virtual_address_s.virtual_address.load( + name=self.want.name, + partition=self.want.partition + ) + result = VirtualAddressParameters(result.attrs) + return result + + def update_on_device(self): + params = self.want.api_params() + resource = self.client.api.tm.ltm.virtual_address_s.virtual_address.load( + name=self.want.name, + partition=self.want.partition + ) + resource.modify(**params) + + def exists(self): + result = self.client.api.tm.ltm.virtual_address_s.virtual_address.exists( + name=self.want.name, + partition=self.want.partition + ) + return result + + +class ArgumentSpec(object): + def __init__(self): + self.supports_check_mode = True + self.argument_spec = dict( + state=dict( + default='present', + choices=['present', 'absent', 'disabled', 'enabled'] + ), + name=dict( + required=True, + aliases=['vs'] + ), + destination=dict( + aliases=['address', 'ip'] + ), + port=dict( + type='int' + ), + profiles=dict( + type='list', + aliases=['all_profiles'], + options=dict( + name=dict(required=False), + context=dict(default='all', choices=['all', 'server-side', 'client-side']) + ) + ), + policies=dict( + type='list', + aliases=['all_policies'] + ), + irules=dict( + type='list', + aliases=['all_rules'] + ), + enabled_vlans=dict( + type='list' + ), + disabled_vlans=dict( + type='list' + ), + pool=dict(), + description=dict(), + snat=dict(), + route_advertisement_state=dict( + choices=['enabled', 'disabled'] + ), + default_persistence_profile=dict(), + fallback_persistence_profile=dict(), + source=dict(), + metadata=dict(type='raw') + ) + self.f5_product_name = 'bigip' + self.mutually_exclusive = [ + ['enabled_vlans', 'disabled_vlans'] + ] + + +def cleanup_tokens(client): try: - state = "STATE_%s" % route_advertisement_state.strip().upper() - address = fq_name(partition, destination,) - current_route_advertisement_state = get_route_advertisement_status(api, address) - if current_route_advertisement_state != route_advertisement_state: - api.LocalLB.VirtualAddressV2.set_route_advertisement_state(virtual_addresses=[address], states=[state]) - updated = True - return updated - except bigsuds.OperationFailed as e: - raise Exception('Error on setting profiles : %s' % e) + resource = client.api.shared.authz.tokens_s.token.load( + name=client.api.icrs.token + ) + resource.delete() + except Exception: + pass def main(): - argument_spec = f5_argument_spec() - argument_spec.update(dict( - state=dict(type='str', default='present', choices=['absent', 'disabled', 'enabled', 'present']), - name=dict(type='str', required=True, aliases=['vs']), - destination=dict(type='str', aliases=['address', 'ip']), - port=dict(type='str'), - all_policies=dict(type='list'), - all_profiles=dict(type='list'), - all_rules=dict(type='list'), - enabled_vlans=dict(type='list'), - pool=dict(type='str'), - description=dict(type='str'), - snat=dict(type='str'), - route_advertisement_state=dict(type='str', choices=['disabled', 'enabled']), - default_persistence_profile=dict(type='str'), - fallback_persistence_profile=dict(type='str'), - )) + if not HAS_F5SDK: + raise F5ModuleError("The python f5-sdk module is required") - module = AnsibleModule( - argument_spec=argument_spec, - supports_check_mode=True + if not HAS_NETADDR: + raise F5ModuleError("The python netaddr module is required") + + spec = ArgumentSpec() + + client = AnsibleF5Client( + argument_spec=spec.argument_spec, + supports_check_mode=spec.supports_check_mode, + f5_product_name=spec.f5_product_name, + mutually_exclusive=spec.mutually_exclusive ) - if not bigsuds_found: - module.fail_json(msg="the python bigsuds module is required") - - if module.params['validate_certs']: - import ssl - if not hasattr(ssl, 'SSLContext'): - module.fail_json( - msg='bigsuds does not support verifying certificates with python < 2.7.9. Either update python or set validate_certs=False on the task' - ) - - server = module.params['server'] - server_port = module.params['server_port'] - user = module.params['user'] - password = module.params['password'] - state = module.params['state'] - partition = module.params['partition'] - validate_certs = module.params['validate_certs'] - - name = fq_name(partition, module.params['name']) - destination = module.params['destination'] - port = module.params['port'] - if port == '' or port is None: - port = None - else: - port = int(port) - all_profiles = fq_list_names(partition, module.params['all_profiles']) - all_policies = fq_list_names(partition, module.params['all_policies']) - all_rules = fq_list_names(partition, module.params['all_rules']) - - enabled_vlans = module.params['enabled_vlans'] - if enabled_vlans is None or 'ALL' in enabled_vlans: - all_enabled_vlans = enabled_vlans - else: - all_enabled_vlans = fq_list_names(partition, enabled_vlans) - - pool = fq_name(partition, module.params['pool']) - description = module.params['description'] - snat = module.params['snat'] - route_advertisement_state = module.params['route_advertisement_state'] - default_persistence_profile = fq_name(partition, module.params['default_persistence_profile']) - fallback_persistence_profile = module.params['fallback_persistence_profile'] - - if 0 > port > 65535: - module.fail_json(msg="valid ports must be in range 0 - 65535") - try: - api = bigip_api(server, user, password, validate_certs, port=server_port) - result = {'changed': False} # default + mm = ModuleManager(client) + results = mm.exec_module() + cleanup_tokens(client) + client.module.exit_json(**results) + except F5ModuleError as e: + cleanup_tokens(client) + client.module.fail_json(msg=str(e)) - if state == 'absent': - if not module.check_mode: - if vs_exists(api, name): - # hack to handle concurrent runs of module - # pool might be gone before we actually remove - try: - vs_remove(api, name) - result = {'changed': True, 'deleted': name} - except bigsuds.OperationFailed as e: - if "was not found" in str(e): - result['changed'] = False - else: - raise - else: - # check-mode return value - result = {'changed': True} - - else: - update = False - if not vs_exists(api, name): - if (not destination) or (port is None): - module.fail_json(msg="both destination and port must be supplied to create a VS") - if not module.check_mode: - # a bit of a hack to handle concurrent runs of this module. - # even though we've checked the virtual_server doesn't exist, - # it may exist by the time we run virtual_server(). - # this catches the exception and does something smart - # about it! - try: - vs_create(api, name, destination, port, pool, all_profiles) - set_policies(api, name, all_policies) - set_enabled_vlans(api, name, all_enabled_vlans) - set_rules(api, name, all_rules) - set_snat(api, name, snat) - set_description(api, name, description) - set_default_persistence_profiles(api, name, default_persistence_profile) - set_fallback_persistence_profile(api, partition, name, fallback_persistence_profile) - set_state(api, name, state) - set_route_advertisement_state(api, destination, partition, route_advertisement_state) - result = {'changed': True} - except bigsuds.OperationFailed as e: - raise Exception('Error on creating Virtual Server : %s' % e) - else: - # check-mode return value - result = {'changed': True} - else: - update = True - if update: - # VS exists - if not module.check_mode: - # Have a transaction for all the changes - try: - api.System.Session.start_transaction() - result['changed'] |= set_destination(api, name, fq_name(partition, destination)) - result['changed'] |= set_port(api, name, port) - result['changed'] |= set_pool(api, name, pool) - result['changed'] |= set_description(api, name, description) - result['changed'] |= set_snat(api, name, snat) - result['changed'] |= set_profiles(api, name, all_profiles) - result['changed'] |= set_policies(api, name, all_policies) - result['changed'] |= set_enabled_vlans(api, name, all_enabled_vlans) - result['changed'] |= set_rules(api, name, all_rules) - result['changed'] |= set_default_persistence_profiles(api, name, default_persistence_profile) - result['changed'] |= set_fallback_persistence_profile(api, partition, name, fallback_persistence_profile) - result['changed'] |= set_state(api, name, state) - result['changed'] |= set_route_advertisement_state(api, destination, partition, route_advertisement_state) - api.System.Session.submit_transaction() - except Exception as e: - raise Exception("Error on updating Virtual Server : %s" % str(e)) - else: - # check-mode return value - result = {'changed': True} - - except Exception as e: - module.fail_json(msg="received exception: %s" % e) - - module.exit_json(**result) if __name__ == '__main__': main() diff --git a/test/units/modules/network/f5/fixtures/load_ltm_virtual_1.json b/test/units/modules/network/f5/fixtures/load_ltm_virtual_1.json new file mode 100644 index 00000000000..aaf48ab87bd --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_ltm_virtual_1.json @@ -0,0 +1,43 @@ +{ + "kind": "tm:ltm:virtual:virtualstate", + "name": "my-virtual-server", + "partition": "Common", + "fullPath": "/Common/my-virtual-server", + "generation": 65, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server?ver=12.1.2", + "addressStatus": "yes", + "autoLasthop": "default", + "cmpEnabled": "yes", + "connectionLimit": 0, + "destination": "/Common/10.10.10.10:443", + "enabled": true, + "gtmScore": 0, + "ipProtocol": "any", + "mask": "255.255.255.255", + "mirror": "disabled", + "mobileAppTunnel": "disabled", + "nat64": "disabled", + "rateLimit": "disabled", + "rateLimitDstMask": 0, + "rateLimitMode": "object", + "rateLimitSrcMask": 0, + "serviceDownImmediateAction": "none", + "source": "0.0.0.0/0", + "sourceAddressTranslation": { + "type": "automap" + }, + "sourcePort": "preserve", + "synCookieStatus": "not-activated", + "translateAddress": "enabled", + "translatePort": "enabled", + "vlansDisabled": true, + "vsIndex": 2, + "policiesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/policies?ver=12.1.2", + "isSubcollection": true + }, + "profilesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles?ver=12.1.2", + "isSubcollection": true + } +} diff --git a/test/units/modules/network/f5/fixtures/load_ltm_virtual_1_address.json b/test/units/modules/network/f5/fixtures/load_ltm_virtual_1_address.json new file mode 100644 index 00000000000..297afc9187a --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_ltm_virtual_1_address.json @@ -0,0 +1,25 @@ +{ + "kind": "tm:ltm:virtual-address:virtual-addressstate", + "name": "10.10.10.10", + "partition": "Common", + "fullPath": "/Common/10.10.10.10", + "generation": 116, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual-address/~Common~10.10.10.10?ver=12.1.2", + "address": "10.10.10.10", + "arp": "enabled", + "autoDelete": "true", + "connectionLimit": 0, + "enabled": "yes", + "floating": "enabled", + "icmpEcho": "enabled", + "inheritedTrafficGroup": "false", + "mask": "255.255.255.255", + "routeAdvertisement": "enabled", + "serverScope": "any", + "spanning": "disabled", + "trafficGroup": "/Common/traffic-group-1", + "trafficGroupReference": { + "link": "https://localhost/mgmt/tm/cm/traffic-group/~Common~traffic-group-1?ver=12.1.2" + }, + "unit": 1 +} diff --git a/test/units/modules/network/f5/fixtures/load_ltm_virtual_2.json b/test/units/modules/network/f5/fixtures/load_ltm_virtual_2.json new file mode 100644 index 00000000000..712b19e3d93 --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_ltm_virtual_2.json @@ -0,0 +1,65 @@ +{ + "kind": "tm:ltm:virtual:virtualstate", + "name": "my-virtual-server", + "partition": "Common", + "fullPath": "/Common/my-virtual-server", + "generation": 152, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server?ver=12.1.2", + "addressStatus": "yes", + "autoLasthop": "default", + "cmpEnabled": "yes", + "connectionLimit": 0, + "destination": "/Common/10.10.10.10:443", + "enabled": true, + "gtmScore": 0, + "ipProtocol": "any", + "mask": "255.255.255.255", + "mirror": "disabled", + "mobileAppTunnel": "disabled", + "nat64": "disabled", + "rateLimit": "disabled", + "rateLimitDstMask": 0, + "rateLimitMode": "object", + "rateLimitSrcMask": 0, + "serviceDownImmediateAction": "none", + "source": "0.0.0.0/0", + "sourceAddressTranslation": { + "type": "automap" + }, + "sourcePort": "preserve", + "synCookieStatus": "not-activated", + "translateAddress": "enabled", + "translatePort": "enabled", + "vlansDisabled": true, + "vsIndex": 19, + "vlans": [ + "/Common/net1" + ], + "vlansReference": [ + { + "link": "https://localhost/mgmt/tm/net/vlan/~Common~net1?ver=12.1.2" + } + ], + "policiesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/policies?ver=12.1.2", + "isSubcollection": true + }, + "profilesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles?ver=12.1.2", + "isSubcollection": true, + "items": [ + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "fastL4", + "partition": "Common", + "fullPath": "/Common/fastL4", + "generation": 148, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~fastL4?ver=12.1.2", + "context": "all", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/fastl4/~Common~fastL4?ver=12.1.2" + } + } + ] + } +} diff --git a/test/units/modules/network/f5/fixtures/load_ltm_virtual_3.json b/test/units/modules/network/f5/fixtures/load_ltm_virtual_3.json new file mode 100644 index 00000000000..e1b0c7c270b --- /dev/null +++ b/test/units/modules/network/f5/fixtures/load_ltm_virtual_3.json @@ -0,0 +1,115 @@ +{ + "kind": "tm:ltm:virtual:virtualstate", + "name": "my-virtual-server", + "partition": "Common", + "fullPath": "/Common/my-virtual-server", + "generation": 340, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server?expandSubcollections=true&ver=12.0.0", + "addressStatus": "yes", + "autoLasthop": "default", + "cmpEnabled": "yes", + "connectionLimit": 0, + "description": "Test Virtual Server", + "destination": "/Common/10.10.10.10:443", + "enabled": true, + "gtmScore": 0, + "ipProtocol": "tcp", + "mask": "255.255.255.255", + "mirror": "disabled", + "mobileAppTunnel": "disabled", + "nat64": "disabled", + "rateLimit": "disabled", + "rateLimitDstMask": 0, + "rateLimitMode": "object", + "rateLimitSrcMask": 0, + "serviceDownImmediateAction": "none", + "source": "0.0.0.0/0", + "sourceAddressTranslation": { + "type": "automap" + }, + "sourcePort": "preserve", + "synCookieStatus": "not-activated", + "translateAddress": "enabled", + "translatePort": "enabled", + "vlansDisabled": true, + "vsIndex": 38, + "rules": [ + "/Common/web_logging" + ], + "rulesReference": [ + { + "link": "https://localhost/mgmt/tm/ltm/rule/~Common~web_logging?ver=12.0.0" + } + ], + "policiesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/policies?ver=12.0.0", + "isSubcollection": true, + "items": [ + { + "kind": "tm:ltm:virtual:policies:policiesstate", + "name": "policy1", + "partition": "Common", + "fullPath": "/Common/policy1", + "generation": 340, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/policies/~Common~policy1?ver=12.0.0", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/policy/~Common~policy1?ver=12.0.0" + } + } + ] + }, + "profilesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles?ver=12.0.0", + "isSubcollection": true, + "items": [ + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "clientssl", + "partition": "Common", + "fullPath": "/Common/clientssl", + "generation": 338, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~clientssl?ver=12.0.0", + "context": "clientside", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/client-ssl/~Common~clientssl?ver=12.0.0" + } + }, + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "http", + "partition": "Common", + "fullPath": "/Common/http", + "generation": 338, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~http?ver=12.0.0", + "context": "all", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/http/~Common~http?ver=12.0.0" + } + }, + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "tcp", + "partition": "Common", + "fullPath": "/Common/tcp", + "generation": 338, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~tcp?ver=12.0.0", + "context": "clientside", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/tcp/~Common~tcp?ver=12.0.0" + } + }, + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "tcp-legacy", + "partition": "Common", + "fullPath": "/Common/tcp-legacy", + "generation": 338, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~tcp-legacy?ver=12.0.0", + "context": "serverside", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/tcp/~Common~tcp-legacy?ver=12.0.0" + } + } + ] + } +} diff --git a/test/units/modules/network/f5/test_bigip_virtual_server.py b/test/units/modules/network/f5/test_bigip_virtual_server.py new file mode 100644 index 00000000000..eeb381de325 --- /dev/null +++ b/test/units/modules/network/f5/test_bigip_virtual_server.py @@ -0,0 +1,773 @@ +# -*- 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 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.f5_utils import AnsibleF5Client + +try: + from library.bigip_virtual_server import VirtualAddressParameters + from library.bigip_virtual_server import VirtualServerModuleParameters + from library.bigip_virtual_server import VirtualServerApiParameters + from library.bigip_virtual_server import ModuleManager + from library.bigip_virtual_server import VirtualServerManager + from library.bigip_virtual_server import VirtualAddressManager + from library.bigip_virtual_server import ArgumentSpec + from ansible.module_utils.f5_utils import iControlUnexpectedHTTPError + from test.unit.modules.utils import set_module_args +except ImportError: + try: + from ansible.modules.network.f5.bigip_virtual_server import VirtualAddressParameters + from ansible.modules.network.f5.bigip_virtual_server import VirtualServerApiParameters + from ansible.modules.network.f5.bigip_virtual_server import VirtualServerModuleParameters + from ansible.modules.network.f5.bigip_virtual_server import ModuleManager + from ansible.modules.network.f5.bigip_virtual_server import VirtualServerManager + from ansible.modules.network.f5.bigip_virtual_server import VirtualAddressManager + from ansible.modules.network.f5.bigip_virtual_server import ArgumentSpec + from ansible.module_utils.f5_utils 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_destination_mutex_1(self): + args = dict( + destination='1.1.1.1' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + + def test_destination_mutex_2(self): + args = dict( + destination='1.1.1.1%2' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.route_domain == 2 + + def test_destination_mutex_3(self): + args = dict( + destination='1.1.1.1:80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.port == 80 + + def test_destination_mutex_4(self): + args = dict( + destination='1.1.1.1%2:80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.port == 80 + assert p.destination_tuple.route_domain == 2 + + def test_api_destination_mutex_5(self): + args = dict( + destination='/Common/1.1.1.1' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + + def test_api_destination_mutex_6(self): + args = dict( + destination='/Common/1.1.1.1%2' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.route_domain == 2 + + def test_api_destination_mutex_7(self): + args = dict( + destination='/Common/1.1.1.1:80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.port == 80 + + def test_api_destination_mutex_8(self): + args = dict( + destination='/Common/1.1.1.1%2:80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '1.1.1.1' + assert p.destination_tuple.port == 80 + assert p.destination_tuple.route_domain == 2 + + def test_destination_mutex_9(self): + args = dict( + destination='2700:bc00:1f10:101::6' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' + + def test_destination_mutex_10(self): + args = dict( + destination='2700:bc00:1f10:101::6%2' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' + assert p.destination_tuple.route_domain == 2 + + def test_destination_mutex_11(self): + args = dict( + destination='2700:bc00:1f10:101::6.80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' + assert p.destination_tuple.port == 80 + + def test_destination_mutex_12(self): + args = dict( + destination='2700:bc00:1f10:101::6%2.80' + ) + p = VirtualServerApiParameters(args) + assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' + assert p.destination_tuple.port == 80 + assert p.destination_tuple.route_domain == 2 + +# +# def test_destination_mutex_6(self): +# args = dict( +# destination='/Common/2700:bc00:1f10:101::6' +# ) +# p = VirtualServerParameters(args) +# assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' +# +# def test_destination_mutex_5(self): +# args = dict( +# destination='/Common/2700:bc00:1f10:101::6' +# ) +# p = VirtualServerParameters(args) +# assert p.destination_tuple.ip == '2700:bc00:1f10:101::6' + + def test_module_no_partition_prefix_parameters(self): + args = dict( + server='localhost', + user='admin', + password='secret', + state='present', + partition='Common', + name='my-virtual-server', + destination='10.10.10.10', + port=443, + pool='my-pool', + snat='Automap', + description='Test Virtual Server', + profiles=[ + dict( + name='fix', + context='all' + ) + ], + enabled_vlans=['vlan2'] + ) + p = VirtualServerModuleParameters(args) + assert p.name == 'my-virtual-server' + assert p.partition == 'Common' + assert p.port == 443 + assert p.server == 'localhost' + assert p.user == 'admin' + assert p.password == 'secret' + assert p.destination == '/Common/10.10.10.10:443' + assert p.pool == '/Common/my-pool' + assert p.snat == {'type': 'automap'} + assert p.description == 'Test Virtual Server' + assert len(p.profiles) == 1 + assert 'context' in p.profiles[0] + assert 'name' in p.profiles[0] + assert '/Common/vlan2' in p.enabled_vlans + + def test_module_partition_prefix_parameters(self): + args = dict( + server='localhost', + user='admin', + password='secret', + state='present', + partition='Common', + name='my-virtual-server', + destination='10.10.10.10', + port=443, + pool='/Common/my-pool', + snat='Automap', + description='Test Virtual Server', + profiles=[ + dict( + name='fix', + context='all' + ) + ], + enabled_vlans=['/Common/vlan2'] + ) + p = VirtualServerModuleParameters(args) + assert p.name == 'my-virtual-server' + assert p.partition == 'Common' + assert p.port == 443 + assert p.server == 'localhost' + assert p.user == 'admin' + assert p.password == 'secret' + assert p.destination == '/Common/10.10.10.10:443' + assert p.pool == '/Common/my-pool' + assert p.snat == {'type': 'automap'} + assert p.description == 'Test Virtual Server' + assert len(p.profiles) == 1 + assert 'context' in p.profiles[0] + assert 'name' in p.profiles[0] + assert '/Common/vlan2' in p.enabled_vlans + + def test_api_parameters_variables(self): + args = { + "kind": "tm:ltm:virtual:virtualstate", + "name": "my-virtual-server", + "partition": "Common", + "fullPath": "/Common/my-virtual-server", + "generation": 54, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server?expandSubcollections=true&ver=12.1.2", + "addressStatus": "yes", + "autoLasthop": "default", + "cmpEnabled": "yes", + "connectionLimit": 0, + "description": "Test Virtual Server", + "destination": "/Common/10.10.10.10:443", + "enabled": True, + "gtmScore": 0, + "ipProtocol": "tcp", + "mask": "255.255.255.255", + "mirror": "disabled", + "mobileAppTunnel": "disabled", + "nat64": "disabled", + "rateLimit": "disabled", + "rateLimitDstMask": 0, + "rateLimitMode": "object", + "rateLimitSrcMask": 0, + "serviceDownImmediateAction": "none", + "source": "0.0.0.0/0", + "sourceAddressTranslation": { + "type": "automap" + }, + "sourcePort": "preserve", + "synCookieStatus": "not-activated", + "translateAddress": "enabled", + "translatePort": "enabled", + "vlansEnabled": True, + "vsIndex": 3, + "vlans": [ + "/Common/net1" + ], + "vlansReference": [ + { + "link": "https://localhost/mgmt/tm/net/vlan/~Common~net1?ver=12.1.2" + } + ], + "policiesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/policies?ver=12.1.2", + "isSubcollection": True + }, + "profilesReference": { + "link": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles?ver=12.1.2", + "isSubcollection": True, + "items": [ + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "http", + "partition": "Common", + "fullPath": "/Common/http", + "generation": 54, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~http?ver=12.1.2", + "context": "all", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/http/~Common~http?ver=12.1.2" + } + }, + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "serverssl", + "partition": "Common", + "fullPath": "/Common/serverssl", + "generation": 54, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~serverssl?ver=12.1.2", + "context": "serverside", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/server-ssl/~Common~serverssl?ver=12.1.2" + } + }, + { + "kind": "tm:ltm:virtual:profiles:profilesstate", + "name": "tcp", + "partition": "Common", + "fullPath": "/Common/tcp", + "generation": 54, + "selfLink": "https://localhost/mgmt/tm/ltm/virtual/~Common~my-virtual-server/profiles/~Common~tcp?ver=12.1.2", + "context": "all", + "nameReference": { + "link": "https://localhost/mgmt/tm/ltm/profile/tcp/~Common~tcp?ver=12.1.2" + } + } + ] + } + } + p = VirtualServerApiParameters(args) + assert p.name == 'my-virtual-server' + assert p.partition == 'Common' + assert p.port == 443 + assert p.destination == '/Common/10.10.10.10:443' + assert p.snat == {'type': 'automap'} + assert p.description == 'Test Virtual Server' + assert 'context' in p.profiles[0] + assert 'name' in p.profiles[0] + assert 'fullPath' in p.profiles[0] + assert p.profiles[0]['context'] == 'all' + assert p.profiles[0]['name'] == 'http' + assert p.profiles[0]['fullPath'] == '/Common/http' + assert '/Common/net1' in p.vlans + + +@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', + return_value=True) +class TestManager(unittest.TestCase): + + def setUp(self): + self.spec = ArgumentSpec() + + def test_create_virtual_server(self, *args): + set_module_args(dict( + all_profiles=[ + dict( + name='http' + ), + dict( + name='clientssl' + ) + ], + description="Test Virtual Server", + destination="10.10.10.10", + name="my-snat-pool", + partition="Common", + password="secret", + port="443", + server="localhost", + snat="Automap", + state="present", + user="admin", + validate_certs="no" + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=False) + vsm.create_on_device = Mock(return_value=True) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + results = mm.exec_module() + + assert results['changed'] is True + + def test_delete_virtual_server(self, *args): + set_module_args(dict( + all_profiles=[ + 'http', 'clientssl' + ], + description="Test Virtual Server", + destination="10.10.10.10", + name="my-snat-pool", + partition="Common", + password="secret", + port="443", + server="localhost", + snat="Automap", + state="absent", + user="admin", + validate_certs="no" + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=False) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + + results = mm.exec_module() + + assert results['changed'] is False + + def test_enable_vs_that_is_already_enabled(self, *args): + set_module_args(dict( + all_profiles=[ + 'http', 'clientssl' + ], + description="Test Virtual Server", + destination="10.10.10.10", + name="my-snat-pool", + partition="Common", + password="secret", + port="443", + server="localhost", + snat="Automap", + state="absent", + user="admin", + validate_certs="no" + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters( + dict( + agent_status_traps='disabled' + ) + ) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=False) + vsm.update_on_device = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + results = mm.exec_module() + + assert results['changed'] is False + + def test_modify_port(self, *args): + set_module_args(dict( + name="my-virtual-server", + partition="Common", + password="secret", + port="10443", + server="localhost", + state="present", + user="admin", + validate_certs="no" + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_1.json')) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + vsm.update_on_device = Mock(return_value=True) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + results = mm.exec_module() + + assert results['changed'] is True + + def test_modify_port_idempotent(self, *args): + set_module_args(dict( + name="my-virtual-server", + partition="Common", + password="secret", + port="443", + server="localhost", + state="present", + user="admin", + validate_certs="no" + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_1.json')) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + results = mm.exec_module() + + assert results['changed'] is False + + def test_modify_vlans_idempotent(self, *args): + set_module_args(dict( + name="my-virtual-server", + partition="Common", + password="secret", + disabled_vlans=[ + "net1" + ], + server="localhost", + state="present", + user="admin", + validate_certs="no" + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_2.json')) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + + results = mm.exec_module() + + assert results['changed'] is False + + def test_modify_profiles(self, *args): + set_module_args(dict( + name="my-virtual-server", + partition="Common", + password="secret", + profiles=[ + 'http', 'clientssl' + ], + server="localhost", + state="present", + user="admin", + validate_certs="no" + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_2.json')) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + vsm.update_on_device = Mock(return_value=True) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + + results = mm.exec_module() + + assert results['changed'] is True + assert len(results['profiles']) == 2 + assert 'name' in results['profiles'][0] + assert 'context' in results['profiles'][0] + assert results['profiles'][0]['name'] == 'http' + assert results['profiles'][0]['context'] == 'all' + assert 'name' in results['profiles'][1] + assert 'context' in results['profiles'][1] + assert results['profiles'][1]['name'] == 'clientssl' + assert results['profiles'][1]['context'] == 'clientside' + + def test_update_virtual_server(self, *args): + set_module_args(dict( + profiles=[ + dict( + name='http' + ), + dict( + name='clientssl' + ) + ], + description="foo virtual", + destination="1.1.1.1", + name="my-virtual-server", + partition="Common", + password="secret", + port="8443", + server="localhost", + snat="snat-pool1", + state="disabled", + source='1.2.3.4/32', + user="admin", + validate_certs="no", + irules=[ + 'irule1', + 'irule2' + ], + policies=[ + 'policy1', + 'policy2' + ], + enabled_vlans=[ + 'vlan1', + 'vlan2' + ], + pool='my-pool', + default_persistence_profile='source_addr', + fallback_persistence_profile='dest_addr' + )) + + # Configure the parameters that would be returned by querying the + # remote device + current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_3.json')) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + # Override methods to force specific logic in the module to happen + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=current) + vsm.update_on_device = Mock(return_value=True) + + mm = ModuleManager(client) + mm.get_manager = Mock(return_value=vsm) + + results = mm.exec_module() + + assert results['changed'] is True + assert results['source'] == '1.2.3.4/32' + assert results['description'] == 'foo virtual' + assert results['snat'] == '/Common/snat-pool1' + assert results['destination'] == '1.1.1.1' + assert results['port'] == 8443 + assert results['default_persistence_profile'] == '/Common/source_addr' + assert results['fallback_persistence_profile'] == '/Common/dest_addr' + + # policies + assert len(results['policies']) == 2 + assert '/Common/policy1' in results['policies'] + assert '/Common/policy2' in results['policies'] + + # irules + assert len(results['irules']) == 2 + assert '/Common/irule1' in results['irules'] + assert '/Common/irule2' in results['irules'] + + # vlans + assert len(results['enabled_vlans']) == 2 + assert '/Common/vlan1' in results['enabled_vlans'] + assert '/Common/vlan2' in results['enabled_vlans'] + + # profiles + assert len(results['profiles']) == 2 + assert 'name' in results['profiles'][0] + assert 'context' in results['profiles'][0] + assert results['profiles'][0]['name'] == 'http' + assert results['profiles'][0]['context'] == 'all' + assert 'name' in results['profiles'][1] + assert 'context' in results['profiles'][1] + assert results['profiles'][1]['name'] == 'clientssl' + assert results['profiles'][1]['context'] == 'clientside' + + +@patch('ansible.module_utils.f5_utils.AnsibleF5Client._get_mgmt_root', + return_value=True) +class TestDeprecatedAnsible24Manager(unittest.TestCase): + def setUp(self): + self.spec = ArgumentSpec() + + def test_modify_port_idempotent(self, *args): + set_module_args(dict( + destination="10.10.10.10", + name="my-virtual-server", + route_advertisement_state="enabled", + partition="Common", + password="secret", + port="443", + server="localhost", + state="present", + user="admin", + validate_certs="no" + )) + + client = AnsibleF5Client( + argument_spec=self.spec.argument_spec, + supports_check_mode=self.spec.supports_check_mode, + f5_product_name=self.spec.f5_product_name + ) + + vsm_current = VirtualServerApiParameters(load_fixture('load_ltm_virtual_1.json')) + vam_current = VirtualAddressParameters(load_fixture('load_ltm_virtual_1_address.json')) + + vsm = VirtualServerManager(client) + vsm.exists = Mock(return_value=True) + vsm.read_current_from_device = Mock(return_value=vsm_current) + vam = VirtualAddressManager(client) + vam.exists = Mock(return_value=True) + vam.read_current_from_device = Mock(return_value=vam_current) + + mm = ModuleManager(client) + mm.get_manager = Mock(side_effect=[vsm, vam]) + + results = mm.exec_module() + + assert results['changed'] is False