diff --git a/lib/ansible/modules/network/f5/bigip_virtual_server.py b/lib/ansible/modules/network/f5/bigip_virtual_server.py index 89daf30fec..7d446233e1 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 0000000000..aaf48ab87b --- /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 0000000000..297afc9187 --- /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 0000000000..712b19e3d9 --- /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 0000000000..e1b0c7c270 --- /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 0000000000..eeb381de32 --- /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