diff --git a/lib/ansible/modules/network/netscaler/netscaler_servicegroup.py b/lib/ansible/modules/network/netscaler/netscaler_servicegroup.py new file mode 100644 index 0000000000..168b4ce4ed --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_servicegroup.py @@ -0,0 +1,973 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Citrix Systems +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} + + +DOCUMENTATION = ''' +--- +module: netscaler_servicegroup +short_description: Manage service group configuration in Netscaler +description: + - Manage service group configuration in Netscaler. + - This module is intended to run either on the ansible control node or a bastion (jumpserver) with access to the actual netscaler instance. + +version_added: "2.4" + +author: George Nikolopoulos (@giorgos-nikolopoulos) + +options: + servicegroupname: + description: + - >- + Name of the service group. Must begin with an ASCII alphabetic or underscore C(_) character, and must + contain only ASCII alphanumeric, underscore C(_), hash C(#), period C(.), space C( ), colon C(:), at C(@), equals + C(=), and hyphen C(-) characters. Can be changed after the name is created. + - "Minimum length = 1" + + servicetype: + choices: + - 'HTTP' + - 'FTP' + - 'TCP' + - 'UDP' + - 'SSL' + - 'SSL_BRIDGE' + - 'SSL_TCP' + - 'DTLS' + - 'NNTP' + - 'RPCSVR' + - 'DNS' + - 'ADNS' + - 'SNMP' + - 'RTSP' + - 'DHCPRA' + - 'ANY' + - 'SIP_UDP' + - 'SIP_TCP' + - 'SIP_SSL' + - 'DNS_TCP' + - 'ADNS_TCP' + - 'MYSQL' + - 'MSSQL' + - 'ORACLE' + - 'RADIUS' + - 'RADIUSListener' + - 'RDP' + - 'DIAMETER' + - 'SSL_DIAMETER' + - 'TFTP' + - 'SMPP' + - 'PPTP' + - 'GRE' + - 'SYSLOGTCP' + - 'SYSLOGUDP' + - 'FIX' + - 'SSL_FIX' + description: + - "Protocol used to exchange data with the service." + + cachetype: + choices: + - 'TRANSPARENT' + - 'REVERSE' + - 'FORWARD' + description: + - "Cache type supported by the cache server." + + maxclient: + description: + - "Maximum number of simultaneous open connections for the service group." + - "Minimum value = C(0)" + - "Maximum value = C(4294967294)" + + maxreq: + description: + - "Maximum number of requests that can be sent on a persistent connection to the service group." + - "Note: Connection requests beyond this value are rejected." + - "Minimum value = C(0)" + - "Maximum value = C(65535)" + + cacheable: + description: + - "Use the transparent cache redirection virtual server to forward the request to the cache server." + - "Note: Do not set this parameter if you set the Cache Type." + type: bool + + cip: + choices: + - 'ENABLED' + - 'DISABLED' + description: + - "Insert the Client IP header in requests forwarded to the service." + + cipheader: + description: + - >- + Name of the HTTP header whose value must be set to the IP address of the client. Used with the Client + IP parameter. If client IP insertion is enabled, and the client IP header is not specified, the value + of Client IP Header parameter or the value set by the set ns config command is used as client's IP + header name. + - "Minimum length = 1" + + usip: + description: + - >- + Use client's IP address as the source IP address when initiating connection to the server. With the + NO setting, which is the default, a mapped IP (MIP) address or subnet IP (SNIP) address is used as + the source IP address to initiate server side connections. + + pathmonitor: + description: + - "Path monitoring for clustering." + + pathmonitorindv: + description: + - "Individual Path monitoring decisions." + + useproxyport: + description: + - >- + Use the proxy port as the source port when initiating connections with the server. With the NO + setting, the client-side connection port is used as the source port for the server-side connection. + - "Note: This parameter is available only when the Use Source IP C(usip) parameter is set to C(yes)." + type: bool + + healthmonitor: + description: + - "Monitor the health of this service. Available settings function as follows:" + - "C(yes) - Send probes to check the health of the service." + - >- + C(no) - Do not send probes to check the health of the service. With the NO option, the appliance shows + the service as UP at all times. + type: bool + + sc: + description: + - "State of the SureConnect feature for the service group." + type: bool + + sp: + description: + - "Enable surge protection for the service group." + type: bool + + rtspsessionidremap: + description: + - "Enable RTSP session ID mapping for the service group." + type: bool + + clttimeout: + description: + - "Time, in seconds, after which to terminate an idle client connection." + - "Minimum value = C(0)" + - "Maximum value = C(31536000)" + + svrtimeout: + description: + - "Time, in seconds, after which to terminate an idle server connection." + - "Minimum value = C(0)" + - "Maximum value = C(31536000)" + + cka: + description: + - "Enable client keep-alive for the service group." + type: bool + + tcpb: + description: + - "Enable TCP buffering for the service group." + type: bool + + cmp: + description: + - "Enable compression for the specified service." + type: bool + + maxbandwidth: + description: + - "Maximum bandwidth, in Kbps, allocated for all the services in the service group." + - "Minimum value = C(0)" + - "Maximum value = C(4294967287)" + + monthreshold: + description: + - >- + Minimum sum of weights of the monitors that are bound to this service. Used to determine whether to + mark a service as UP or DOWN. + - "Minimum value = C(0)" + - "Maximum value = C(65535)" + + downstateflush: + choices: + - 'ENABLED' + - 'DISABLED' + description: + - >- + Flush all active transactions associated with all the services in the service group whose state + transitions from UP to DOWN. Do not enable this option for applications that must complete their + transactions. + + tcpprofilename: + description: + - "Name of the TCP profile that contains TCP configuration settings for the service group." + - "Minimum length = 1" + - "Maximum length = 127" + + httpprofilename: + description: + - "Name of the HTTP profile that contains HTTP configuration settings for the service group." + - "Minimum length = 1" + - "Maximum length = 127" + + comment: + description: + - "Any information about the service group." + + appflowlog: + choices: + - 'ENABLED' + - 'DISABLED' + description: + - "Enable logging of AppFlow information for the specified service group." + + netprofile: + description: + - "Network profile for the service group." + - "Minimum length = 1" + - "Maximum length = 127" + + autoscale: + choices: + - 'DISABLED' + - 'DNS' + - 'POLICY' + description: + - "Auto scale option for a servicegroup." + + memberport: + description: + - "member port." + + graceful: + description: + - "Wait for all existing connections to the service to terminate before shutting down the service." + type: bool + + servicemembers: + description: + - A list of dictionaries describing each service member of the service group. + suboptions: + ip: + description: + - IP address of the service. Must not overlap with an existing server entity defined by name. + + port: + description: + - Server port number. + - Range C(1) - C(65535) + - "* in CLI is represented as 65535 in NITRO API" + hashid: + description: + - The hash identifier for the service. + - This must be unique for each service. + - This parameter is used by hash based load balancing methods. + - Minimum value = C(1) + + serverid: + description: + - The identifier for the service. + - This is used when the persistency type is set to Custom Server ID. + + servername: + description: + - Name of the server to which to bind the service group. + - The server must already be configured as a named server. + - Minimum length = 1 + + customserverid: + description: + - The identifier for this IP:Port pair. + - Used when the persistency type is set to Custom Server ID. + + weight: + description: + - Weight to assign to the servers in the service group. + - Specifies the capacity of the servers relative to the other servers in the load balancing configuration. + - The higher the weight, the higher the percentage of requests sent to the service. + - Minimum value = C(1) + - Maximum value = C(100) + + monitorbindings: + description: + - A list of monitornames to bind to this service + - Note that the monitors must have already been setup possibly using the M(netscaler_lb_monitor) module or some other method + suboptions: + monitorname: + description: + - The monitor name to bind to this servicegroup. + weight: + description: + - Weight to assign to the binding between the monitor and servicegroup. + + +extends_documentation_fragment: netscaler +requirements: + - nitro python sdk +''' + +EXAMPLES = ''' +# The LB Monitors monitor-1 and monitor-2 must already exist +# Service members defined by C(ip) must not redefine an existing server's ip address. +# Service members defined by C(servername) must already exist. + +- name: Setup http service with ip members + delegate_to: localhost + netscaler_servicegroup: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + + state: present + + servicegroupname: service-group-1 + servicetype: HTTP + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 50 + - ip: 10.79.79.79 + port: 80 + weight: 40 + - servername: server-1 + port: 80 + weight: 10 + + monitorbindings: + - monitorname: monitor-1 + weight: 50 + - monitorname: monitor-2 + weight: 50 + +''' + +RETURN = ''' +loglines: + description: list of logged messages by the module + returned: always + type: list + sample: ['message 1', 'message 2'] + +msg: + description: Message detailing the failure reason + returned: failure + type: str + sample: "Action does not exist" + +diff: + description: List of differences between the actual configured object and the configuration specified in the module + returned: failure + type: dict + sample: { 'clttimeout': 'difference. ours: (float) 10.0 other: (float) 20.0' } +''' + +from ansible.module_utils.basic import AnsibleModule +import copy + +from ansible.module_utils.netscaler import ConfigProxy, get_nitro_client, netscaler_common_arguments, log, loglines, get_immutables_intersection +try: + from nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup import servicegroup + from nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_servicegroupmember_binding import servicegroup_servicegroupmember_binding + from nssrc.com.citrix.netscaler.nitro.exception.nitro_exception import nitro_exception + + from nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_lbmonitor_binding import servicegroup_lbmonitor_binding + from nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_servicegroup_binding import lbmonitor_servicegroup_binding + PYTHON_SDK_IMPORTED = True +except ImportError as e: + PYTHON_SDK_IMPORTED = False + + +def servicegroup_exists(client, module): + log('Checking if service group exists') + count = servicegroup.count_filtered(client, 'servicegroupname:%s' % module.params['servicegroupname']) + log('count is %s' % count) + if count > 0: + return True + else: + return False + + +def servicegroup_identical(client, module, servicegroup_proxy): + log('Checking if service group is identical') + servicegroups = servicegroup.get_filtered(client, 'servicegroupname:%s' % module.params['servicegroupname']) + if servicegroup_proxy.has_equal_attributes(servicegroups[0]): + return True + else: + return False + + +def get_configured_service_members(client, module): + log('get_configured_service_members') + readwrite_attrs = [ + 'servicegroupname', + 'ip', + 'port', + 'hashid', + 'serverid', + 'servername', + 'customserverid', + 'weight' + ] + readonly_attrs = [ + 'delay', + 'statechangetimesec', + 'svrstate', + 'tickssincelaststatechange', + 'graceful', + ] + + members = [] + if module.params['servicemembers'] is None: + return members + + for config in module.params['servicemembers']: + # Make a copy to update + config = copy.deepcopy(config) + config['servicegroupname'] = module.params['servicegroupname'] + member_proxy = ConfigProxy( + actual=servicegroup_servicegroupmember_binding(), + client=client, + attribute_values_dict=config, + readwrite_attrs=readwrite_attrs, + readonly_attrs=readonly_attrs + ) + members.append(member_proxy) + return members + + +def servicemembers_identical(client, module): + log('servicemembers_identical') + try: + # count() raises nitro exception instead of returning 0 + count = servicegroup_servicegroupmember_binding.count(client, module.params['servicegroupname']) + if count > 0: + servicegroup_members = servicegroup_servicegroupmember_binding.get(client, module.params['servicegroupname']) + else: + servicegroup_members = [] + except nitro_exception as e: + if e.errorcode == 258: + servicegroup_members = [] + else: + raise + + log('servicemembers %s' % servicegroup_members) + module_servicegroups = get_configured_service_members(client, module) + log('Number of service group members %s' % len(servicegroup_members)) + if len(servicegroup_members) != len(module_servicegroups): + return False + + # Fallthrough to member evaluation + identical_count = 0 + for actual_member in servicegroup_members: + for member in module_servicegroups: + if member.has_equal_attributes(actual_member): + identical_count += 1 + break + if identical_count != len(servicegroup_members): + return False + + # Fallthrough to success + return True + + +def sync_service_members(client, module): + log('sync_service_members') + delete_all_servicegroup_members(client, module) + + for member in get_configured_service_members(client, module): + member.add() + + +def delete_all_servicegroup_members(client, module): + log('delete_all_servicegroup_members') + if servicegroup_servicegroupmember_binding.count(client, module.params['servicegroupname']) == 0: + return + servicegroup_members = servicegroup_servicegroupmember_binding.get(client, module.params['servicegroupname']) + log('len %s' % len(servicegroup_members)) + log('count %s' % servicegroup_servicegroupmember_binding.count(client, module.params['servicegroupname'])) + for member in servicegroup_members: + log('%s' % dir(member)) + log('ip %s' % member.ip) + log('servername %s' % member.servername) + if all([ + hasattr(member, 'ip'), + member.ip is not None, + hasattr(member, 'servername'), + member.servername is not None, + ]): + member.ip = None + + member.servicegroupname = module.params['servicegroupname'] + servicegroup_servicegroupmember_binding.delete(client, member) + + +def get_configured_monitor_bindings(client, module): + log('Entering get_configured_monitor_bindings') + bindings = {} + if 'monitorbindings' in module.params and module.params['monitorbindings'] is not None: + for binding in module.params['monitorbindings']: + readwrite_attrs = [ + 'monitorname', + 'servicegroupname', + 'weight', + ] + readonly_attrs = [] + attribute_values_dict = copy.deepcopy(binding) + attribute_values_dict['servicegroupname'] = module.params['servicegroupname'] + binding_proxy = ConfigProxy( + actual=lbmonitor_servicegroup_binding(), + client=client, + attribute_values_dict=attribute_values_dict, + readwrite_attrs=readwrite_attrs, + readonly_attrs=readonly_attrs, + ) + key = attribute_values_dict['monitorname'] + bindings[key] = binding_proxy + return bindings + + +def get_actual_monitor_bindings(client, module): + log('Entering get_actual_monitor_bindings') + bindings = {} + try: + # count() raises nitro exception instead of returning 0 + count = servicegroup_lbmonitor_binding.count(client, module.params['servicegroupname']) + except nitro_exception as e: + if e.errorcode == 258: + return bindings + else: + raise + + if count == 0: + return bindings + + # Fallthrough to rest of execution + for binding in servicegroup_lbmonitor_binding.get(client, module.params['servicegroupname']): + log('Gettign actual monitor with name %s' % binding.monitor_name) + key = binding.monitor_name + bindings[key] = binding + + return bindings + + +def monitor_bindings_identical(client, module): + log('Entering monitor_bindings_identical') + configured_bindings = get_configured_monitor_bindings(client, module) + actual_bindings = get_actual_monitor_bindings(client, module) + + configured_key_set = set(configured_bindings.keys()) + actual_key_set = set(actual_bindings.keys()) + symmetrical_diff = configured_key_set ^ actual_key_set + for default_monitor in ('tcp-default', 'ping-default'): + if default_monitor in symmetrical_diff: + log('Excluding %s monitor from key comparison' % default_monitor) + symmetrical_diff.remove(default_monitor) + if len(symmetrical_diff) > 0: + return False + + # Compare key to key + for key in configured_key_set: + configured_proxy = configured_bindings[key] + log('configured_proxy %s' % [configured_proxy.monitorname, configured_proxy.servicegroupname, configured_proxy.weight]) + log('actual_bindings %s' % [actual_bindings[key].monitor_name, actual_bindings[key].servicegroupname, actual_bindings[key].weight]) + if any([configured_proxy.monitorname != actual_bindings[key].monitor_name, + configured_proxy.servicegroupname != actual_bindings[key].servicegroupname, + configured_proxy.weight != float(actual_bindings[key].weight)]): + return False + + # Fallthrought to success + return True + + +def sync_monitor_bindings(client, module): + log('Entering sync_monitor_bindings') + # Delete existing bindings + for binding in get_actual_monitor_bindings(client, module).values(): + b = lbmonitor_servicegroup_binding() + b.monitorname = binding.monitor_name + b.servicegroupname = module.params['servicegroupname'] + # Cannot remove default monitor bindings + if b.monitorname in ('tcp-default', 'ping-default'): + continue + lbmonitor_servicegroup_binding.delete(client, b) + + # Apply configured bindings + + for binding in get_configured_monitor_bindings(client, module).values(): + log('Adding %s' % binding.monitorname) + binding.add() + + +def diff(client, module, servicegroup_proxy): + servicegroup_list = servicegroup.get_filtered(client, 'servicegroupname:%s' % module.params['servicegroupname']) + diff_object = servicegroup_proxy.diff_object(servicegroup_list[0]) + return diff_object + + +def main(): + + module_specific_arguments = dict( + servicegroupname=dict(type='str'), + servicetype=dict( + type='str', + choices=[ + 'HTTP', + 'FTP', + 'TCP', + 'UDP', + 'SSL', + 'SSL_BRIDGE', + 'SSL_TCP', + 'DTLS', + 'NNTP', + 'RPCSVR', + 'DNS', + 'ADNS', + 'SNMP', + 'RTSP', + 'DHCPRA', + 'ANY', + 'SIP_UDP', + 'SIP_TCP', + 'SIP_SSL', + 'DNS_TCP', + 'ADNS_TCP', + 'MYSQL', + 'MSSQL', + 'ORACLE', + 'RADIUS', + 'RADIUSListener', + 'RDP', + 'DIAMETER', + 'SSL_DIAMETER', + 'TFTP', + 'SMPP', + 'PPTP', + 'GRE', + 'SYSLOGTCP', + 'SYSLOGUDP', + 'FIX', + 'SSL_FIX', + ] + ), + cachetype=dict( + type='str', + choices=[ + 'TRANSPARENT', + 'REVERSE', + 'FORWARD', + ] + ), + maxclient=dict(type='float'), + maxreq=dict(type='float'), + cacheable=dict(type='bool'), + cip=dict( + type='str', + choices=[ + 'ENABLED', + 'DISABLED', + ] + ), + cipheader=dict(type='str'), + usip=dict(type='bool'), + pathmonitor=dict(type='bool'), + pathmonitorindv=dict(type='bool'), + useproxyport=dict(type='bool'), + healthmonitor=dict(type='bool'), + sc=dict(type='bool'), + sp=dict(type='bool'), + rtspsessionidremap=dict(type='bool'), + clttimeout=dict(type='float'), + svrtimeout=dict(type='float'), + cka=dict(type='bool'), + tcpb=dict(type='bool'), + cmp=dict(type='bool'), + maxbandwidth=dict(type='float'), + monthreshold=dict(type='float'), + downstateflush=dict( + type='str', + choices=[ + 'ENABLED', + 'DISABLED', + ] + ), + tcpprofilename=dict(type='str'), + httpprofilename=dict(type='str'), + comment=dict(type='str'), + appflowlog=dict( + type='str', + choices=[ + 'ENABLED', + 'DISABLED', + ] + ), + netprofile=dict(type='str'), + autoscale=dict( + type='str', + choices=[ + 'DISABLED', + 'DNS', + 'POLICY', + ] + ), + memberport=dict(type='int'), + graceful=dict(type='bool'), + ) + + hand_inserted_arguments = dict( + servicemembers=dict(type='list'), + monitorbindings=dict(type='list'), + ) + + argument_spec = dict() + + argument_spec.update(netscaler_common_arguments) + argument_spec.update(module_specific_arguments) + argument_spec.update(hand_inserted_arguments) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + module_result = dict( + changed=False, + failed=False, + loglines=loglines, + ) + + # Fail the module if imports failed + if not PYTHON_SDK_IMPORTED: + module.fail_json(msg='Could not load nitro python sdk') + + # Fallthrough to rest of execution + client = get_nitro_client(module) + + try: + client.login() + except nitro_exception as e: + msg = "nitro exception during login. errorcode=%s, message=%s" % (str(e.errorcode), e.message) + module.fail_json(msg=msg) + except Exception as e: + if str(type(e)) == "": + module.fail_json(msg='Connection error %s' % str(e)) + elif str(type(e)) == "": + module.fail_json(msg='SSL Error %s' % str(e)) + else: + module.fail_json(msg='Unexpected error during login %s' % str(e)) + + # Instantiate service group configuration object + readwrite_attrs = [ + 'servicegroupname', + 'servicetype', + 'cachetype', + 'maxclient', + 'maxreq', + 'cacheable', + 'cip', + 'cipheader', + 'usip', + 'pathmonitor', + 'pathmonitorindv', + 'useproxyport', + 'healthmonitor', + 'sc', + 'sp', + 'rtspsessionidremap', + 'clttimeout', + 'svrtimeout', + 'cka', + 'tcpb', + 'cmp', + 'maxbandwidth', + 'monthreshold', + 'downstateflush', + 'tcpprofilename', + 'httpprofilename', + 'comment', + 'appflowlog', + 'netprofile', + 'autoscale', + 'memberport', + 'graceful', + ] + + readonly_attrs = [ + 'numofconnections', + 'serviceconftype', + 'value', + 'svrstate', + 'ip', + 'monstatcode', + 'monstatparam1', + 'monstatparam2', + 'monstatparam3', + 'statechangetimemsec', + 'stateupdatereason', + 'clmonowner', + 'clmonview', + 'groupcount', + 'riseapbrstatsmsgcode2', + 'serviceipstr', + 'servicegroupeffectivestate' + ] + + immutable_attrs = [ + 'servicegroupname', + 'servicetype', + 'cachetype', + 'td', + 'cipheader', + 'state', + 'autoscale', + 'memberport', + 'servername', + 'port', + 'serverid', + 'monitor_name_svc', + 'dup_weight', + 'riseapbrstatsmsgcode', + 'delay', + 'graceful', + 'includemembers', + 'newname', + ] + + transforms = { + 'pathmonitorindv': ['bool_yes_no'], + 'cacheable': ['bool_yes_no'], + 'cka': ['bool_yes_no'], + 'pathmonitor': ['bool_yes_no'], + 'tcpb': ['bool_yes_no'], + 'sp': ['bool_on_off'], + 'usip': ['bool_yes_no'], + 'healthmonitor': ['bool_yes_no'], + 'useproxyport': ['bool_yes_no'], + 'rtspsessionidremap': ['bool_on_off'], + 'sc': ['bool_on_off'], + 'graceful': ['bool_yes_no'], + 'cmp': ['bool_yes_no'], + } + + # Instantiate config proxy + servicegroup_proxy = ConfigProxy( + actual=servicegroup(), + client=client, + attribute_values_dict=module.params, + readwrite_attrs=readwrite_attrs, + readonly_attrs=readonly_attrs, + immutable_attrs=immutable_attrs, + transforms=transforms, + ) + + try: + if module.params['state'] == 'present': + log('Applying actions for state present') + if not servicegroup_exists(client, module): + if not module.check_mode: + log('Adding service group') + servicegroup_proxy.add() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + elif not servicegroup_identical(client, module, servicegroup_proxy): + + # Check if we try to change value of immutable attributes + diff_dict = diff(client, module, servicegroup_proxy) + immutables_changed = get_immutables_intersection(servicegroup_proxy, diff_dict.keys()) + if immutables_changed != []: + msg = 'Cannot update immutable attributes %s. Must delete and recreate entity.' % (immutables_changed,) + module.fail_json(msg=msg, diff=diff_dict, **module_result) + + if not module.check_mode: + servicegroup_proxy.update() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + else: + module_result['changed'] = False + + # Check bindings + if not monitor_bindings_identical(client, module): + if not module.check_mode: + sync_monitor_bindings(client, module) + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + + if not servicemembers_identical(client, module): + if not module.check_mode: + sync_service_members(client, module) + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + + # Sanity check for state + if not module.check_mode: + log('Sanity checks for state present') + if not servicegroup_exists(client, module): + module.fail_json(msg='Service group is not present', **module_result) + if not servicegroup_identical(client, module, servicegroup_proxy): + module.fail_json(msg='Service group is not identical to configuration', **module_result) + if not servicemembers_identical(client, module): + module.fail_json(msg='Service group members differ from configuration', **module_result) + if not monitor_bindings_identical(client, module): + module.fail_json(msg='Monitor bindings are not identical', **module_result) + + elif module.params['state'] == 'absent': + log('Applying actions for state absent') + if servicegroup_exists(client, module): + if not module.check_mode: + servicegroup_proxy.delete() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + else: + module_result['changed'] = False + + # Sanity check for state + if not module.check_mode: + log('Sanity checks for state absent') + if servicegroup_exists(client, module): + module.fail_json(msg='Service group is present', **module_result) + + except nitro_exception as e: + msg = "nitro exception errorcode=" + str(e.errorcode) + ",message=" + e.message + module.fail_json(msg=msg, **module_result) + + client.logout() + module.exit_json(**module_result) + + +if __name__ == "__main__": + main() diff --git a/test/integration/roles/netscaler_servicegroup/defaults/main.yaml b/test/integration/roles/netscaler_servicegroup/defaults/main.yaml new file mode 100644 index 0000000000..641801f660 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +testcase: "*" +test_cases: [] + +nitro_user: nsroot +nitro_pass: nsroot diff --git a/test/integration/roles/netscaler_servicegroup/sample_inventory b/test/integration/roles/netscaler_servicegroup/sample_inventory new file mode 100644 index 0000000000..7da2dbbdb8 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/sample_inventory @@ -0,0 +1,5 @@ + + +[netscaler] + +172.18.0.2 nsip=172.18.0.2 nitro_user=nsroot nitro_pass=nsroot diff --git a/test/integration/roles/netscaler_servicegroup/tasks/main.yaml b/test/integration/roles/netscaler_servicegroup/tasks/main.yaml new file mode 100644 index 0000000000..9a197e4d77 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tasks/main.yaml @@ -0,0 +1,6 @@ +--- +- { include: testbed.yaml, state: present } + +- { include: nitro.yaml, tags: ['nitro'] } + +- { include: testbed.yaml, state: absent } diff --git a/test/integration/roles/netscaler_servicegroup/tasks/nitro.yaml b/test/integration/roles/netscaler_servicegroup/tasks/nitro.yaml new file mode 100644 index 0000000000..00ab502dda --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tasks/nitro.yaml @@ -0,0 +1,14 @@ +- name: collect all nitro test cases + find: + paths: "{{ role_path }}/tests/nitro" + patterns: "{{ testcase }}.yaml" + register: test_cases + +- name: set test_items + set_fact: test_items="{{ test_cases.files | map(attribute='path') | list }}" + +- name: run test case + include: "{{ test_case_to_run }}" + with_items: "{{ test_items }}" + loop_control: + loop_var: test_case_to_run diff --git a/test/integration/roles/netscaler_servicegroup/tasks/testbed.yaml b/test/integration/roles/netscaler_servicegroup/tasks/testbed.yaml new file mode 100644 index 0000000000..09ecaf6401 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tasks/testbed.yaml @@ -0,0 +1,21 @@ +- name: setup monitor + delegate_to: localhost + netscaler_lb_monitor: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + state: "{{ state }}" + + monitorname: monitor-1 + type: HTTP + +- name: setup monitor + delegate_to: localhost + netscaler_lb_monitor: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + state: "{{ state }}" + + monitorname: monitor-2 + type: HTTP diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup.yaml new file mode 100644 index 0000000000..4d857cbfc0 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup.yaml @@ -0,0 +1,85 @@ +--- + +- include: "{{ role_path }}/tests/nitro/servicegroup/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/update.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/remove.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/remove.yaml new file mode 100644 index 0000000000..3d76e7cf98 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/remove.yaml @@ -0,0 +1,16 @@ +--- + +- name: Remove servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + + servicegroupname: service-group-1 + servicetype: HTTP diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/setup.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/setup.yaml new file mode 100644 index 0000000000..7f5e43d54f --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/setup.yaml @@ -0,0 +1,46 @@ +--- + +- name: Setup servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + state: present + + servicegroupname: service-group-1 + servicetype: HTTP + cachetype: TRANSPARENT + maxclient: 100 + maxreq: 100 + cacheable: no + cip: ENABLED + cipheader: cip-header + usip: no + pathmonitor: no + pathmonitorindv: no + useproxyport: no + healthmonitor: no + sc: off + sp: off + rtspsessionidremap: off + clttimeout: 2000 + svrtimeout: 2000 + cka: yes + tcpb: yes + cmp: no + maxbandwidth: 5000 + monthreshold: 100 + downstateflush: DISABLED + comment: some comment + appflowlog: ENABLED + autoscale: POLICY + memberport: 80 + graceful: no + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 100 diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/update.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/update.yaml new file mode 100644 index 0000000000..f4a418118c --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup/update.yaml @@ -0,0 +1,49 @@ +--- + +- name: Setup servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + state: present + + servicegroupname: service-group-1 + servicetype: HTTP + cachetype: TRANSPARENT + maxclient: 100 + maxreq: 100 + cacheable: no + cip: ENABLED + cipheader: cip-header + usip: no + pathmonitor: no + pathmonitorindv: no + useproxyport: no + healthmonitor: no + sc: off + sp: off + rtspsessionidremap: off + clttimeout: 1000 + svrtimeout: 1000 + cka: yes + tcpb: yes + cmp: no + maxbandwidth: 5000 + monthreshold: 100 + downstateflush: DISABLED + comment: some comment + appflowlog: ENABLED + autoscale: POLICY + memberport: 80 + graceful: no + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 50 + - ip: 10.79.79.79 + port: 80 + weight: 50 diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors.yaml new file mode 100644 index 0000000000..b3044119ec --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors.yaml @@ -0,0 +1,113 @@ +--- + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/update.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/default_only.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/default_only.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/default_only.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/default_only.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/servicegroup_monitors/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/default_only.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/default_only.yaml new file mode 100644 index 0000000000..ec555d8ab5 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/default_only.yaml @@ -0,0 +1,18 @@ +--- + +- name: Setup servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + servicegroupname: service-group-1 + servicetype: HTTP + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 100 diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/remove.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/remove.yaml new file mode 100644 index 0000000000..3d76e7cf98 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/remove.yaml @@ -0,0 +1,16 @@ +--- + +- name: Remove servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + + servicegroupname: service-group-1 + servicetype: HTTP diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/setup.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/setup.yaml new file mode 100644 index 0000000000..2f7f009c76 --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/setup.yaml @@ -0,0 +1,23 @@ +--- + +- name: Setup servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + servicegroupname: service-group-1 + servicetype: HTTP + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 100 + monitorbindings: + - monitorname: monitor-1 + weight: 50 + - monitorname: monitor-2 + weight: 50 diff --git a/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/update.yaml b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/update.yaml new file mode 100644 index 0000000000..3d1a861cbf --- /dev/null +++ b/test/integration/roles/netscaler_servicegroup/tests/nitro/servicegroup_monitors/update.yaml @@ -0,0 +1,23 @@ +--- + +- name: Setup servicegroup + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_servicegroup: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + servicegroupname: service-group-1 + servicetype: HTTP + servicemembers: + - ip: 10.78.78.78 + port: 80 + weight: 100 + monitorbindings: + - monitorname: monitor-1 + weight: 80 + - monitorname: monitor-2 + weight: 20 diff --git a/test/units/modules/network/netscaler/test_netscaler_servicegroup.py b/test/units/modules/network/netscaler/test_netscaler_servicegroup.py new file mode 100644 index 0000000000..7a51233762 --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_servicegroup.py @@ -0,0 +1,527 @@ + +# Copyright (c) 2017 Citrix Systems +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +from ansible.compat.tests.mock import patch, Mock, MagicMock, call + +import sys + +if sys.version_info[:2] != (2, 6): + import requests + + +from .netscaler_module import TestModule, nitro_base_patcher, set_module_args + + +class TestNetscalerServicegroupModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + cls.MockException = MockException + m = MagicMock() + cls.servicegroup_mock = MagicMock() + cls.servicegroup_mock.__class__ = MagicMock() + nssrc_modules_mock = { + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup.servicegroup': cls.servicegroup_mock, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_servicegroupmember_binding': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_servicegroupmember_binding.servicegroup_servicegroupmember_binding': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_lbmonitor_binding': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.servicegroup_lbmonitor_binding.servicegroup_lbmonitor_binding': m, + + 'nssrc.com.citrix.netscaler.nitro.resource.config.lb': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_servicegroup_binding': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_servicegroup_binding.lbmonitor_servicegroup_binding': m + } + + cls.nitro_specific_patcher = patch.dict(sys.modules, nssrc_modules_mock) + cls.nitro_base_patcher = nitro_base_patcher + + @classmethod + def tearDownClass(cls): + cls.nitro_base_patcher.stop() + cls.nitro_specific_patcher.stop() + + def set_module_state(self, state): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state=state, + )) + + def setUp(self): + self.nitro_base_patcher.start() + self.nitro_specific_patcher.start() + + # Setup minimal required arguments to pass AnsibleModule argument parsing + + def tearDown(self): + self.nitro_base_patcher.stop() + self.nitro_specific_patcher.stop() + + def test_graceful_nitro_api_import_error(self): + # Stop nitro api patching to cause ImportError + self.set_module_state('present') + self.nitro_base_patcher.stop() + self.nitro_specific_patcher.stop() + from ansible.modules.network.netscaler import netscaler_servicegroup + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Could not load nitro python sdk') + + def test_graceful_nitro_error_on_login(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + + class MockException(Exception): + def __init__(self, *args, **kwargs): + self.errorcode = 0 + self.message = '' + + client_mock = Mock() + client_mock.login = Mock(side_effect=MockException) + m = Mock(return_value=client_mock) + with patch('ansible.modules.network.netscaler.netscaler_servicegroup.get_nitro_client', m): + with patch('ansible.modules.network.netscaler.netscaler_servicegroup.nitro_exception', MockException): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue(result['msg'].startswith('nitro exception'), msg='nitro exception during login not handled properly') + + def test_graceful_no_connection_error(self): + + if sys.version_info[:2] == (2, 6): + self.skipTest('requests library not available under python2.6') + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + + client_mock = Mock() + attrs = {'login.side_effect': requests.exceptions.ConnectionError} + client_mock.configure_mock(**attrs) + m = Mock(return_value=client_mock) + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + get_nitro_client=m, + nitro_exception=self.MockException, + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue(result['msg'].startswith('Connection error'), msg='Connection error was not handled gracefully') + + def test_graceful_login_error(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + + if sys.version_info[:2] == (2, 6): + self.skipTest('requests library not available under python2.6') + + client_mock = Mock() + attrs = {'login.side_effect': requests.exceptions.SSLError} + client_mock.configure_mock(**attrs) + m = Mock(return_value=client_mock) + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + get_nitro_client=m, + nitro_exception=self.MockException, + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue(result['msg'].startswith('SSL Error'), msg='SSL Error was not handled gracefully') + + def test_create_non_existing_servicegroup(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[False, True]) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + servicemembers_identical=Mock(side_effect=[False, True]), + nitro_exception=self.MockException, + ): + self.module = netscaler_servicegroup + result = self.exited() + servicegroup_proxy_mock.assert_has_calls([call.add()]) + self.assertTrue(result['changed'], msg='Change not recorded') + + def test_update_servicegroup_when_servicegroup_differs(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[True, True]) + servicegroup_identical_mock = Mock(side_effect=[False, True]) + monitor_bindings_identical_mock = Mock(side_effect=[True, True]) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + servicegroup_identical=servicegroup_identical_mock, + monitor_bindings_identical=monitor_bindings_identical_mock, + servicemembers_identical=Mock(side_effect=[True, True]), + nitro_exception=self.MockException, + ): + self.module = netscaler_servicegroup + result = self.exited() + servicegroup_proxy_mock.assert_has_calls([call.update()]) + self.assertTrue(result['changed'], msg='Change not recorded') + + def test_update_servicegroup_when_monitor_bindings_differ(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[True, True]) + servicegroup_identical_mock = Mock(side_effect=[True, True]) + monitor_bindings_identical_mock = Mock(side_effect=[False, True]) + sync_monitor_bindings_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + servicegroup_identical=servicegroup_identical_mock, + monitor_bindings_identical=monitor_bindings_identical_mock, + nitro_exception=self.MockException, + servicemembers_identical=Mock(side_effect=[True, True]), + sync_monitor_bindings=sync_monitor_bindings_mock, + ): + self.module = netscaler_servicegroup + result = self.exited() + # poor man's assert_called_once since python3.5 does not implement that mock method + self.assertEqual(len(sync_monitor_bindings_mock.mock_calls), 1, msg='sync monitor bindings not called once') + self.assertTrue(result['changed'], msg='Change not recorded') + + def test_update_servicegroup_when_service_members_differ(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + sync_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[True, True]), + servicegroup_identical=Mock(side_effect=[True, True]), + monitor_bindings_identical=Mock(side_effect=[True, True]), + sync_monitor_bindings=Mock(), + servicemembers_identical=Mock(side_effect=[False, True]), + sync_service_members=sync_mock, + + ): + self.module = netscaler_servicegroup + result = self.exited() + # poor man's assert_called_once since python3.5 does not implement that mock method + self.assertEqual(len(sync_mock.mock_calls), 1, msg='sync monitor bindings not called once') + self.assertTrue(result['changed'], msg='Change not recorded') + + def test_immutables_changed(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[True, True]), + servicegroup_identical=Mock(side_effect=[False, True]), + get_immutables_intersection=Mock(return_value=['some']), + nitro_exception=self.MockException, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue(result['msg'].startswith('Cannot update immutable attributes')) + + def test_servicegroup_exists_sanity(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + sync_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[False, False]), + servicegroup_identical=Mock(side_effect=[False, False]), + monitor_bindings_identical=Mock(side_effect=[True, True]), + sync_monitor_bindings=Mock(), + servicemembers_identical=Mock(side_effect=[False, True]), + nitro_exception=self.MockException, + sync_service_members=sync_mock, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Service group is not present') + + def test_servicegroup_differ_sanity(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + sync_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[True, True]), + servicegroup_identical=Mock(side_effect=[False, False]), + monitor_bindings_identical=Mock(side_effect=[True, True]), + sync_monitor_bindings=Mock(), + servicemembers_identical=Mock(side_effect=[False, True]), + nitro_exception=self.MockException, + sync_service_members=sync_mock, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Service group is not identical to configuration') + + def test_servicegroup_servicemembers_differ_sanity(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + sync_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[True, True]), + servicegroup_identical=Mock(side_effect=[True, True]), + monitor_bindings_identical=Mock(side_effect=[True, True]), + sync_monitor_bindings=Mock(), + servicemembers_identical=Mock(side_effect=[False, False]), + nitro_exception=self.MockException, + sync_service_members=sync_mock, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Service group members differ from configuration') + + def test_servicegroup_monitor_bindings_sanity(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + sync_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=Mock(side_effect=[True, True]), + servicegroup_identical=Mock(side_effect=[True, True]), + monitor_bindings_identical=Mock(side_effect=[False, False]), + sync_monitor_bindings=Mock(), + servicemembers_identical=Mock(side_effect=[True, True]), + nitro_exception=self.MockException, + sync_service_members=sync_mock, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Monitor bindings are not identical') + + def test_no_change_to_module_when_all_identical(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[True, True]) + servicegroup_identical_mock = Mock(side_effect=[True, True]) + monitor_bindings_identical_mock = Mock(side_effect=[True, True]) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + servicegroup_identical=servicegroup_identical_mock, + servicemembers_identical=Mock(side_effect=[True, True]), + monitor_bindings_identical=monitor_bindings_identical_mock, + nitro_exception=self.MockException, + ): + self.module = netscaler_servicegroup + result = self.exited() + self.assertFalse(result['changed'], msg='Erroneous changed status update') + + def test_absent_operation(self): + self.set_module_state('absent') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[True, False]) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + + ): + self.module = netscaler_servicegroup + result = self.exited() + servicegroup_proxy_mock.assert_has_calls([call.delete()]) + self.assertTrue(result['changed'], msg='Changed status not set correctly') + + def test_absent_operation_no_change(self): + self.set_module_state('absent') + from ansible.modules.network.netscaler import netscaler_servicegroup + servicegroup_proxy_mock = MagicMock() + attrs = { + 'diff_object.return_value': {}, + } + servicegroup_proxy_mock.configure_mock(**attrs) + + m = MagicMock(return_value=servicegroup_proxy_mock) + servicegroup_exists_mock = Mock(side_effect=[False, False]) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=m, + servicegroup_exists=servicegroup_exists_mock, + + ): + self.module = netscaler_servicegroup + result = self.exited() + servicegroup_proxy_mock.assert_not_called() + self.assertFalse(result['changed'], msg='Changed status not set correctly') + + def test_absent_operation_sanity(self): + self.set_module_state('absent') + from ansible.modules.network.netscaler import netscaler_servicegroup + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + ConfigProxy=MagicMock(), + servicegroup_exists=Mock(side_effect=[True, True]), + nitro_exception=self.MockException, + + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertEqual(result['msg'], 'Service group is present') + + def test_graceful_nitro_exception_operation_present(self): + self.set_module_state('present') + from ansible.modules.network.netscaler import netscaler_servicegroup + + class MockException(Exception): + def __init__(self, *args, **kwargs): + self.errorcode = 0 + self.message = '' + + m = Mock(side_effect=MockException) + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + servicegroup_exists=m, + nitro_exception=MockException + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation present' + ) + + def test_graceful_nitro_exception_operation_absent(self): + self.set_module_state('absent') + from ansible.modules.network.netscaler import netscaler_servicegroup + + class MockException(Exception): + def __init__(self, *args, **kwargs): + self.errorcode = 0 + self.message = '' + + m = Mock(side_effect=MockException) + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_servicegroup', + servicegroup_exists=m, + nitro_exception=MockException + ): + self.module = netscaler_servicegroup + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation absent' + )