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'
+ )