diff --git a/docs/docsite/rst/dev_guide/developing_module_utilities.rst b/docs/docsite/rst/dev_guide/developing_module_utilities.rst
index 3ecaa56dda..15a0b26ca6 100644
--- a/docs/docsite/rst/dev_guide/developing_module_utilities.rst
+++ b/docs/docsite/rst/dev_guide/developing_module_utilities.rst
@@ -31,6 +31,7 @@ The following is a list of module_utils files and a general description. The mod
- netapp.py - Functions and utilities for modules that work with the NetApp storage platforms.
- netcfg.py - Configuration utility functions for use by networking modules
- netcmd.py - Defines commands and comparison operators for use in networking modules
+- netscaler.py - Utilities specifically for the netscaler network modules.
- network.py - Functions for running commands on networking devices
- nxos.py - Contains definitions and helper functions specific to Cisco NXOS networking devices
- openstack.py - Utilities for modules that work with Openstack instances.
diff --git a/lib/ansible/module_utils/netscaler.py b/lib/ansible/module_utils/netscaler.py
new file mode 100644
index 0000000000..214a75f30c
--- /dev/null
+++ b/lib/ansible/module_utils/netscaler.py
@@ -0,0 +1,324 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017 Citrix Systems
+#
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is BSD licensed.
+# Modules you write using this snippet, which is embedded dynamically by Ansible
+# still belong to the author of the module, and may assign their own license
+# to the complete work.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+
+import json
+import re
+
+from ansible.module_utils.basic import env_fallback
+
+
+class ConfigProxy(object):
+
+ def __init__(self, actual, client, attribute_values_dict, readwrite_attrs, transforms={}, readonly_attrs=[], immutable_attrs=[], json_encodes=[]):
+
+ # Actual config object from nitro sdk
+ self.actual = actual
+
+ # nitro client
+ self.client = client
+
+ # ansible attribute_values_dict
+ self.attribute_values_dict = attribute_values_dict
+
+ self.readwrite_attrs = readwrite_attrs
+ self.readonly_attrs = readonly_attrs
+ self.immutable_attrs = immutable_attrs
+ self.json_encodes = json_encodes
+ self.transforms = transforms
+
+ self.attribute_values_processed = {}
+ for attribute, value in self.attribute_values_dict.items():
+ if attribute in transforms:
+ for transform in self.transforms[attribute]:
+ if transform == 'bool_yes_no':
+ value = 'YES' if value is True else 'NO'
+ elif transform == 'bool_on_off':
+ value = 'ON' if value is True else 'OFF'
+ elif callable(transform):
+ value = transform(value)
+ else:
+ raise Exception('Invalid transform %s' % transform)
+ self.attribute_values_processed[attribute] = value
+
+ self._copy_attributes_to_actual()
+
+ def _copy_attributes_to_actual(self):
+ for attribute in self.readwrite_attrs:
+ if attribute in self.attribute_values_processed:
+ attribute_value = self.attribute_values_processed[attribute]
+
+ if attribute_value is None:
+ continue
+
+ # Fallthrough
+ if attribute in self.json_encodes:
+ attribute_value = json.JSONEncoder().encode(attribute_value).strip('"')
+ setattr(self.actual, attribute, attribute_value)
+
+ def __getattr__(self, name):
+ if name in self.attribute_values_dict:
+ return self.attribute_values_dict[name]
+ else:
+ raise AttributeError('No attribute %s found' % name)
+
+ def add(self):
+ self.actual.__class__.add(self.client, self.actual)
+
+ def update(self):
+ return self.actual.__class__.update(self.client, self.actual)
+
+ def delete(self):
+ self.actual.__class__.delete(self.client, self.actual)
+
+ def get(self, *args, **kwargs):
+ result = self.actual.__class__.get(self.client, *args, **kwargs)
+
+ return result
+
+ def has_equal_attributes(self, other):
+ if self.diff_object(other) == {}:
+ return True
+ else:
+ return False
+
+ def diff_object(self, other):
+ diff_dict = {}
+ for attribute in self.attribute_values_processed:
+ # Skip readonly attributes
+ if attribute not in self.readwrite_attrs:
+ continue
+
+ # Skip attributes not present in module arguments
+ if self.attribute_values_processed[attribute] is None:
+ continue
+
+ # Check existence
+ if hasattr(other, attribute):
+ attribute_value = getattr(other, attribute)
+ else:
+ diff_dict[attribute] = 'missing from other'
+ continue
+
+ # Compare values
+ param_type = self.attribute_values_processed[attribute].__class__
+ if param_type(attribute_value) != self.attribute_values_processed[attribute]:
+ str_tuple = (
+ type(self.attribute_values_processed[attribute]),
+ self.attribute_values_processed[attribute],
+ type(attribute_value),
+ attribute_value,
+ )
+ diff_dict[attribute] = 'difference. ours: (%s) %s other: (%s) %s' % str_tuple
+ return diff_dict
+
+ def get_actual_rw_attributes(self, filter='name'):
+ if self.actual.__class__.count_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter])) == 0:
+ return {}
+ server_list = self.actual.__class__.get_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter]))
+ actual_instance = server_list[0]
+ ret_val = {}
+ for attribute in self.readwrite_attrs:
+ if not hasattr(actual_instance, attribute):
+ continue
+ ret_val[attribute] = getattr(actual_instance, attribute)
+ return ret_val
+
+ def get_actual_ro_attributes(self, filter='name'):
+ if self.actual.__class__.count_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter])) == 0:
+ return {}
+ server_list = self.actual.__class__.get_filtered(self.client, '%s:%s' % (filter, self.attribute_values_dict[filter]))
+ actual_instance = server_list[0]
+ ret_val = {}
+ for attribute in self.readonly_attrs:
+ if not hasattr(actual_instance, attribute):
+ continue
+ ret_val[attribute] = getattr(actual_instance, attribute)
+ return ret_val
+
+ def get_missing_rw_attributes(self):
+ return list(set(self.readwrite_attrs) - set(self.get_actual_rw_attributes().keys()))
+
+ def get_missing_ro_attributes(self):
+ return list(set(self.readonly_attrs) - set(self.get_actual_ro_attributes().keys()))
+
+
+def get_immutables_intersection(config_proxy, keys):
+ immutables_set = set(config_proxy.immutable_attrs)
+ keys_set = set(keys)
+ # Return list of sets' intersection
+ return list(immutables_set & keys_set)
+
+
+def ensure_feature_is_enabled(client, feature_str):
+ enabled_features = client.get_enabled_features()
+ if feature_str not in enabled_features:
+ client.enable_features(feature_str)
+ client.save_config()
+
+
+def get_nitro_client(module):
+ from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service
+
+ client = nitro_service(module.params['nsip'], module.params['nitro_protocol'])
+ client.set_credential(module.params['nitro_user'], module.params['nitro_pass'])
+ client.timeout = float(module.params['nitro_timeout'])
+ client.certvalidation = module.params['validate_certs']
+ return client
+
+
+netscaler_common_arguments = dict(
+ nsip=dict(
+ required=True,
+ fallback=(env_fallback, ['NETSCALER_NSIP']),
+ ),
+ nitro_user=dict(
+ required=True,
+ fallback=(env_fallback, ['NETSCALER_NITRO_USER']),
+ no_log=True
+ ),
+ nitro_pass=dict(
+ required=True,
+ fallback=(env_fallback, ['NETSCALER_NITRO_PASS']),
+ no_log=True
+ ),
+ nitro_protocol=dict(
+ choices=['http', 'https'],
+ fallback=(env_fallback, ['NETSCALER_NITRO_PROTOCOL']),
+ default='http'
+ ),
+ validate_certs=dict(
+ default=True,
+ type='bool'
+ ),
+ nitro_timeout=dict(default=310, type='float'),
+ state=dict(
+ choices=[
+ 'present',
+ 'absent',
+ ],
+ default='present',
+ ),
+ save_config=dict(
+ type='bool',
+ default=True,
+ ),
+)
+
+
+loglines = []
+
+
+def complete_missing_attributes(actual, attrs_list, fill_value=None):
+ for attribute in attrs_list:
+ if not hasattr(actual, attribute):
+ setattr(actual, attribute, fill_value)
+
+
+def log(msg):
+ loglines.append(msg)
+
+
+def get_ns_version(client):
+ from nssrc.com.citrix.netscaler.nitro.resource.config.ns.nsversion import nsversion
+ result = nsversion.get(client)
+ m = re.match(r'^.*NS(\d+)\.(\d+).*$', result[0].version)
+ if m is None:
+ return None
+ else:
+ return int(m.group(1)), int(m.group(2))
+
+
+def monkey_patch_nitro_api():
+
+ from nssrc.com.citrix.netscaler.nitro.resource.base.Json import Json
+
+ def new_resource_to_string_convert(self, resrc):
+ try:
+ # Line below is the actual patch
+ dict_valid_values = dict((k.replace('_', '', 1), v) for k, v in resrc.__dict__.items() if v)
+ return json.dumps(dict_valid_values)
+ except Exception as e:
+ raise e
+ Json.resource_to_string_convert = new_resource_to_string_convert
+
+ from nssrc.com.citrix.netscaler.nitro.util.nitro_util import nitro_util
+
+ @classmethod
+ def object_to_string_new(cls, obj):
+ try:
+ str_ = ""
+ flds = obj.__dict__
+ # Line below is the actual patch
+ flds = dict((k.replace('_', '', 1), v) for k, v in flds.items() if v)
+ if (flds):
+ for k, v in flds.items():
+ str_ = str_ + "\"" + k + "\":"
+ if type(v) is unicode:
+ v = v.encode('utf8')
+ if type(v) is bool:
+ str_ = str_ + v
+ elif type(v) is str:
+ str_ = str_ + "\"" + v + "\""
+ elif type(v) is int:
+ str_ = str_ + "\"" + str(v) + "\""
+ if str_:
+ str_ = str_ + ","
+ return str_
+ except Exception as e:
+ raise e
+
+ @classmethod
+ def object_to_string_withoutquotes_new(cls, obj):
+ try:
+ str_ = ""
+ flds = obj.__dict__
+ # Line below is the actual patch
+ flds = dict((k.replace('_', '', 1), v) for k, v in flds.items() if v)
+ i = 0
+ if (flds):
+ for k, v in flds.items():
+ str_ = str_ + k + ":"
+ if type(v) is unicode:
+ v = v.encode('utf8')
+ if type(v) is bool:
+ str_ = str_ + v
+ elif type(v) is str:
+ str_ = str_ + cls.encode(v)
+ elif type(v) is int:
+ str_ = str_ + str(v)
+ i = i + 1
+ if i != (len(flds.items())) and str_:
+ str_ = str_ + ","
+ return str_
+ except Exception as e:
+ raise e
+
+ nitro_util.object_to_string = object_to_string_new
+ nitro_util.object_to_string_withoutquotes = object_to_string_withoutquotes_new
diff --git a/lib/ansible/modules/network/netscaler/__init__.py b/lib/ansible/modules/network/netscaler/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/lib/ansible/modules/network/netscaler/netscaler_service.py b/lib/ansible/modules/network/netscaler/netscaler_service.py
new file mode 100644
index 0000000000..dbcb49dad4
--- /dev/null
+++ b/lib/ansible/modules/network/netscaler/netscaler_service.py
@@ -0,0 +1,944 @@
+#!/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 = {'metadata_version': '1.0',
+ 'status': ['preview'],
+ 'supported_by': 'community'}
+
+
+DOCUMENTATION = '''
+---
+module: netscaler_service
+short_description: Manage service configuration in Netscaler
+description:
+ - Manage service configuration in Netscaler.
+ - This module allows the creation, deletion and modification of Netscaler services.
+ - This module is intended to run either on the ansible control node or a bastion (jumpserver) with access to the actual netscaler instance.
+ - This module supports check mode.
+
+version_added: "2.4.0"
+
+author: George Nikolopoulos (@giorgos-nikolopoulos)
+
+options:
+
+ name:
+ description:
+ - >-
+ Name for the service. 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. Cannot be changed after the service has been created.
+ - "Minimum length = 1"
+
+ ip:
+ description:
+ - "IP to assign to the service."
+ - "Minimum length = 1"
+
+ servername:
+ description:
+ - "Name of the server that hosts the service."
+ - "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 in which data is exchanged with the service."
+
+ port:
+ description:
+ - "Port number of the service."
+ - "Range 1 - 65535"
+ - "* in CLI is represented as 65535 in NITRO API"
+
+ cleartextport:
+ description:
+ - >-
+ Port to which clear text data must be sent after the appliance decrypts incoming SSL traffic.
+ Applicable to transparent SSL services.
+ - "Minimum value = 1"
+
+ cachetype:
+ choices:
+ - 'TRANSPARENT'
+ - 'REVERSE'
+ - 'FORWARD'
+ description:
+ - "Cache type supported by the cache server."
+
+ maxclient:
+ description:
+ - "Maximum number of simultaneous open connections to the service."
+ - "Minimum value = 0"
+ - "Maximum value = 4294967294"
+
+ healthmonitor:
+ description:
+ - "Monitor the health of this service"
+ default: yes
+
+ maxreq:
+ description:
+ - "Maximum number of requests that can be sent on a persistent connection to the service."
+ - "Note: Connection requests beyond this value are rejected."
+ - "Minimum value = 0"
+ - "Maximum value = 65535"
+
+ cacheable:
+ description:
+ - "Use the transparent cache redirection virtual server to forward requests to the cache server."
+ - "Note: Do not specify this parameter if you set the Cache Type parameter."
+ default: no
+
+ cip:
+ choices:
+ - 'ENABLED'
+ - 'DISABLED'
+ description:
+ - >-
+ Before forwarding a request to the service, insert an HTTP header with the client's IPv4 or IPv6
+ address as its value. Used if the server needs the client's IP address for security, accounting, or
+ other purposes, and setting the Use Source IP parameter is not a viable option.
+
+ cipheader:
+ description:
+ - >-
+ Name for the HTTP header whose value must be set to the IP address of the client. Used with the
+ Client IP parameter. If you set the Client IP parameter, and you do not specify a name for the
+ header, the appliance uses the header name specified for the global Client IP Header parameter (the
+ cipHeader parameter in the set ns param CLI command or the Client IP Header parameter in the
+ Configure HTTP Parameters dialog box at System > Settings > Change HTTP parameters). If the global
+ Client IP Header parameter is not specified, the appliance inserts a header with the name
+ "client-ip.".
+ - "Minimum length = 1"
+
+ usip:
+ description:
+ - >-
+ Use the client's IP address as the source IP address when initiating a connection to the server. When
+ creating a service, if you do not set this parameter, the service inherits the global Use Source IP
+ setting (available in the enable ns mode and disable ns mode CLI commands, or in the System >
+ Settings > Configure modes > Configure Modes dialog box). However, you can override this setting
+ after you create the service.
+
+ 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 (USIP) parameter is set to YES."
+
+ sc:
+ description:
+ - "State of SureConnect for the service."
+ default: off
+
+ sp:
+ description:
+ - "Enable surge protection for the service."
+
+ rtspsessionidremap:
+ description:
+ - "Enable RTSP session ID mapping for the service."
+ default: off
+
+ clttimeout:
+ description:
+ - "Time, in seconds, after which to terminate an idle client connection."
+ - "Minimum value = 0"
+ - "Maximum value = 31536000"
+
+ svrtimeout:
+ description:
+ - "Time, in seconds, after which to terminate an idle server connection."
+ - "Minimum value = 0"
+ - "Maximum value = 31536000"
+
+ customserverid:
+ description:
+ - >-
+ Unique identifier for the service. Used when the persistency type for the virtual server is set to
+ Custom Server ID.
+ default: 'None'
+
+ serverid:
+ description:
+ - "The identifier for the service. This is used when the persistency type is set to Custom Server ID."
+
+ cka:
+ description:
+ - "Enable client keep-alive for the service."
+
+ tcpb:
+ description:
+ - "Enable TCP buffering for the service."
+
+ cmp:
+ description:
+ - "Enable compression for the service."
+
+ maxbandwidth:
+ description:
+ - "Maximum bandwidth, in Kbps, allocated to the service."
+ - "Minimum value = 0"
+ - "Maximum value = 4294967287"
+
+ accessdown:
+ description:
+ - >-
+ Use Layer 2 mode to bridge the packets sent to this service if it is marked as DOWN. If the service
+ is DOWN, and this parameter is disabled, the packets are dropped.
+ default: no
+
+ 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 = 0"
+ - "Maximum value = 65535"
+
+ downstateflush:
+ choices:
+ - 'ENABLED'
+ - 'DISABLED'
+ description:
+ - >-
+ Flush all active transactions associated with a service whose state transitions from UP to DOWN. Do
+ not enable this option for applications that must complete their transactions.
+ default: ENABLED
+
+ tcpprofilename:
+ description:
+ - "Name of the TCP profile that contains TCP configuration settings for the service."
+ - "Minimum length = 1"
+ - "Maximum length = 127"
+
+ httpprofilename:
+ description:
+ - "Name of the HTTP profile that contains HTTP configuration settings for the service."
+ - "Minimum length = 1"
+ - "Maximum length = 127"
+
+ hashid:
+ description:
+ - >-
+ A numerical identifier that can be used by hash based load balancing methods. Must be unique for each
+ service.
+ - "Minimum value = 1"
+
+ comment:
+ description:
+ - "Any information about the service."
+
+ appflowlog:
+ choices:
+ - 'ENABLED'
+ - 'DISABLED'
+ description:
+ - "Enable logging of AppFlow information."
+ default: ENABLED
+
+ netprofile:
+ description:
+ - "Network profile to use for the service."
+ - "Minimum length = 1"
+ - "Maximum length = 127"
+
+ td:
+ description:
+ - >-
+ Integer value that uniquely identifies the traffic domain in which you want to configure the entity.
+ If you do not specify an ID, the entity becomes part of the default traffic domain, which has an ID
+ of 0.
+ - "Minimum value = 0"
+ - "Maximum value = 4094"
+
+ processlocal:
+ choices:
+ - 'ENABLED'
+ - 'DISABLED'
+ description:
+ - >-
+ By turning on this option packets destined to a service in a cluster will not under go any steering.
+ Turn this option for single packet request response mode or when the upstream device is performing a
+ proper RSS for connection based distribution.
+ default: DISABLED
+
+ dnsprofilename:
+ description:
+ - >-
+ Name of the DNS profile to be associated with the service. DNS profile properties will applied to the
+ transactions processed by a service. This parameter is valid only for ADNS and ADNS-TCP services.
+ - "Minimum length = 1"
+ - "Maximum length = 127"
+
+ ipaddress:
+ description:
+ - "The new IP address of the service."
+
+ graceful:
+ description:
+ - >-
+ Shut down gracefully, not accepting any new connections, and disabling the service when all of its
+ connections are closed.
+ default: no
+
+ monitor_bindings:
+ description:
+ - A list of load balancing monitors to bind to this service.
+ - Each monitor entry is a dictionary which may contain the following options.
+ - Note that if not using the built in monitors they must first be setup.
+ suboptions:
+ monitorname:
+ description:
+ - Name of the monitor.
+ weight:
+ description:
+ - Weight to assign to the binding between the monitor and service.
+ dup_state:
+ choices:
+ - 'ENABLED'
+ - 'DISABLED'
+ description:
+ - State of the monitor.
+ - The state setting for a monitor of a given type affects all monitors of that type.
+ - For example, if an HTTP monitor is enabled, all HTTP monitors on the appliance are (or remain) enabled.
+ - If an HTTP monitor is disabled, all HTTP monitors on the appliance are disabled.
+ dup_weight:
+ description:
+ - Weight to assign to the binding between the monitor and service.
+
+extends_documentation_fragment: netscaler
+requirements:
+ - nitro python sdk
+'''
+
+EXAMPLES = '''
+# Monitor monitor-1 must have been already setup
+
+- name: Setup http service
+ gather_facts: False
+ delegate_to: localhost
+ netscaler_service:
+ nsip: 172.18.0.2
+ nitro_user: nsroot
+ nitro_pass: nsroot
+
+ state: present
+
+ name: service-http-1
+ servicetype: HTTP
+ ipaddress: 10.78.0.1
+ port: 80
+
+ monitor_bindings:
+ - monitor-1
+'''
+
+RETURN = '''
+loglines:
+ description: list of logged messages by the module
+ returned: always
+ type: list
+ sample: "['message 1', 'message 2']"
+
+diff:
+ description: A dictionary with a 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
+from ansible.module_utils.netscaler import (ConfigProxy, get_nitro_client, netscaler_common_arguments, log, loglines, get_immutables_intersection)
+import copy
+
+try:
+ from nssrc.com.citrix.netscaler.nitro.resource.config.basic.service import service
+ from nssrc.com.citrix.netscaler.nitro.resource.config.basic.service_lbmonitor_binding import service_lbmonitor_binding
+ from nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_service_binding import lbmonitor_service_binding
+ from nssrc.com.citrix.netscaler.nitro.exception.nitro_exception import nitro_exception
+ PYTHON_SDK_IMPORTED = True
+except ImportError as e:
+ PYTHON_SDK_IMPORTED = False
+
+
+def service_exists(client, module):
+ if service.count_filtered(client, 'name:%s' % module.params['name']) > 0:
+ return True
+ else:
+ return False
+
+
+def service_identical(client, module, service_proxy):
+ service_list = service.get_filtered(client, 'name:%s' % module.params['name'])
+ diff_dict = service_proxy.diff_object(service_list[0])
+ # the actual ip address is stored in the ipaddress attribute
+ # of the retrieved object
+ if 'ip' in diff_dict:
+ del diff_dict['ip']
+ if len(diff_dict) == 0:
+ return True
+ else:
+ return False
+
+
+def diff(client, module, service_proxy):
+ service_list = service.get_filtered(client, 'name:%s' % module.params['name'])
+ diff_object = service_proxy.diff_object(service_list[0])
+ if 'ip' in diff_object:
+ del diff_object['ip']
+ return diff_object
+
+
+def get_configured_monitor_bindings(client, module, monitor_bindings_rw_attrs):
+ bindings = {}
+ if module.params['monitor_bindings'] is not None:
+ for binding in module.params['monitor_bindings']:
+ attribute_values_dict = copy.deepcopy(binding)
+ # attribute_values_dict['servicename'] = module.params['name']
+ attribute_values_dict['servicegroupname'] = module.params['name']
+ binding_proxy = ConfigProxy(
+ actual=lbmonitor_service_binding(),
+ client=client,
+ attribute_values_dict=attribute_values_dict,
+ readwrite_attrs=monitor_bindings_rw_attrs,
+ )
+ key = binding_proxy.monitorname
+ bindings[key] = binding_proxy
+ return bindings
+
+
+def get_actual_monitor_bindings(client, module):
+ bindings = {}
+ if service_lbmonitor_binding.count(client, module.params['name']) == 0:
+ return bindings
+
+ # Fallthrough to rest of execution
+ for binding in service_lbmonitor_binding.get(client, module.params['name']):
+ # Excluding default monitors since we cannot operate on them
+ if binding.monitor_name in ('tcp-default', 'ping-default'):
+ continue
+ key = binding.monitor_name
+ actual = lbmonitor_service_binding()
+ actual.weight = binding.weight
+ actual.monitorname = binding.monitor_name
+ actual.dup_weight = binding.dup_weight
+ actual.servicename = module.params['name']
+ bindings[key] = actual
+
+ return bindings
+
+
+def monitor_bindings_identical(client, module, monitor_bindings_rw_attrs):
+ configured_proxys = get_configured_monitor_bindings(client, module, monitor_bindings_rw_attrs)
+ actual_bindings = get_actual_monitor_bindings(client, module)
+
+ configured_key_set = set(configured_proxys.keys())
+ actual_key_set = set(actual_bindings.keys())
+ symmetrical_diff = configured_key_set ^ actual_key_set
+ if len(symmetrical_diff) > 0:
+ return False
+
+ # Compare key to key
+ for monitor_name in configured_key_set:
+ proxy = configured_proxys[monitor_name]
+ actual = actual_bindings[monitor_name]
+ diff_dict = proxy.diff_object(actual)
+ if 'servicegroupname' in diff_dict:
+ if proxy.servicegroupname == actual.servicename:
+ del diff_dict['servicegroupname']
+ if len(diff_dict) > 0:
+ return False
+
+ # Fallthrought to success
+ return True
+
+
+def sync_monitor_bindings(client, module, monitor_bindings_rw_attrs):
+ configured_proxys = get_configured_monitor_bindings(client, module, monitor_bindings_rw_attrs)
+ actual_bindings = get_actual_monitor_bindings(client, module)
+ configured_keyset = set(configured_proxys.keys())
+ actual_keyset = set(actual_bindings.keys())
+
+ # Delete extra
+ delete_keys = list(actual_keyset - configured_keyset)
+ for monitor_name in delete_keys:
+ log('Deleting binding for monitor %s' % monitor_name)
+ lbmonitor_service_binding.delete(client, actual_bindings[monitor_name])
+
+ # Delete and re-add modified
+ common_keyset = list(configured_keyset & actual_keyset)
+ for monitor_name in common_keyset:
+ proxy = configured_proxys[monitor_name]
+ actual = actual_bindings[monitor_name]
+ if not proxy.has_equal_attributes(actual):
+ log('Deleting and re adding binding for monitor %s' % monitor_name)
+ lbmonitor_service_binding.delete(client, actual)
+ proxy.add()
+
+ # Add new
+ new_keys = list(configured_keyset - actual_keyset)
+ for monitor_name in new_keys:
+ log('Adding binding for monitor %s' % monitor_name)
+ configured_proxys[monitor_name].add()
+
+
+def all_identical(client, module, service_proxy, monitor_bindings_rw_attrs):
+ return service_identical(client, module, service_proxy) and monitor_bindings_identical(client, module, monitor_bindings_rw_attrs)
+
+
+def main():
+
+ module_specific_arguments = dict(
+ name=dict(type='str'),
+ ip=dict(type='str'),
+ servername=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'
+ ]
+ ),
+ port=dict(type='int'),
+ cleartextport=dict(type='int'),
+ cachetype=dict(
+ type='str',
+ choices=[
+ 'TRANSPARENT',
+ 'REVERSE',
+ 'FORWARD',
+ ]
+ ),
+ maxclient=dict(type='float'),
+ healthmonitor=dict(
+ type='bool',
+ default=True,
+ ),
+ maxreq=dict(type='float'),
+ cacheable=dict(
+ type='bool',
+ default=False,
+ ),
+ cip=dict(
+ type='str',
+ choices=[
+ 'ENABLED',
+ 'DISABLED',
+ ]
+ ),
+ cipheader=dict(type='str'),
+ usip=dict(type='bool'),
+ useproxyport=dict(type='bool'),
+ sc=dict(
+ type='bool',
+ default=False,
+ ),
+ sp=dict(type='bool'),
+ rtspsessionidremap=dict(
+ type='bool',
+ default=False,
+ ),
+ clttimeout=dict(type='float'),
+ svrtimeout=dict(type='float'),
+ customserverid=dict(
+ type='str',
+ default='None',
+ ),
+ cka=dict(type='bool'),
+ tcpb=dict(type='bool'),
+ cmp=dict(type='bool'),
+ maxbandwidth=dict(type='float'),
+ accessdown=dict(
+ type='bool',
+ default=False
+ ),
+ monthreshold=dict(type='float'),
+ downstateflush=dict(
+ type='str',
+ choices=[
+ 'ENABLED',
+ 'DISABLED',
+ ],
+ default='ENABLED',
+ ),
+ tcpprofilename=dict(type='str'),
+ httpprofilename=dict(type='str'),
+ hashid=dict(type='float'),
+ comment=dict(type='str'),
+ appflowlog=dict(
+ type='str',
+ choices=[
+ 'ENABLED',
+ 'DISABLED',
+ ],
+ default='ENABLED',
+ ),
+ netprofile=dict(type='str'),
+ processlocal=dict(
+ type='str',
+ choices=[
+ 'ENABLED',
+ 'DISABLED',
+ ],
+ default='DISABLED',
+ ),
+ dnsprofilename=dict(type='str'),
+ ipaddress=dict(type='str'),
+ graceful=dict(
+ type='bool',
+ default=False,
+ ),
+ )
+
+ hand_inserted_arguments = dict(
+ monitor_bindings=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')
+
+ 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))
+
+ # Fallthrough to rest of execution
+
+ # Instantiate Service Config object
+ readwrite_attrs = [
+ 'name',
+ 'ip',
+ 'servername',
+ 'servicetype',
+ 'port',
+ 'cleartextport',
+ 'cachetype',
+ 'maxclient',
+ 'healthmonitor',
+ 'maxreq',
+ 'cacheable',
+ 'cip',
+ 'cipheader',
+ 'usip',
+ 'useproxyport',
+ 'sc',
+ 'sp',
+ 'rtspsessionidremap',
+ 'clttimeout',
+ 'svrtimeout',
+ 'customserverid',
+ 'cka',
+ 'tcpb',
+ 'cmp',
+ 'maxbandwidth',
+ 'accessdown',
+ 'monthreshold',
+ 'downstateflush',
+ 'tcpprofilename',
+ 'httpprofilename',
+ 'hashid',
+ 'comment',
+ 'appflowlog',
+ 'netprofile',
+ 'processlocal',
+ 'dnsprofilename',
+ 'ipaddress',
+ 'graceful',
+ ]
+
+ readonly_attrs = [
+ 'numofconnections',
+ 'policyname',
+ 'serviceconftype',
+ 'serviceconftype2',
+ 'value',
+ 'gslb',
+ 'dup_state',
+ 'publicip',
+ 'publicport',
+ 'svrstate',
+ 'monitor_state',
+ 'monstatcode',
+ 'lastresponse',
+ 'responsetime',
+ 'riseapbrstatsmsgcode2',
+ 'monstatparam1',
+ 'monstatparam2',
+ 'monstatparam3',
+ 'statechangetimesec',
+ 'statechangetimemsec',
+ 'tickssincelaststatechange',
+ 'stateupdatereason',
+ 'clmonowner',
+ 'clmonview',
+ 'serviceipstr',
+ 'oracleserverversion',
+ ]
+
+ immutable_attrs = [
+ 'name',
+ 'ip',
+ 'servername',
+ 'servicetype',
+ 'port',
+ 'cleartextport',
+ 'cachetype',
+ 'cipheader',
+ 'serverid',
+ 'state',
+ 'td',
+ 'monitor_name_svc',
+ 'riseapbrstatsmsgcode',
+ 'graceful',
+ 'all',
+ 'Internal',
+ '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'],
+ 'graceful': ['bool_yes_no'],
+ 'usip': ['bool_yes_no'],
+ 'healthmonitor': ['bool_yes_no'],
+ 'useproxyport': ['bool_yes_no'],
+ 'rtspsessionidremap': ['bool_on_off'],
+ 'sc': ['bool_on_off'],
+ 'accessdown': ['bool_yes_no'],
+ 'cmp': ['bool_yes_no'],
+ }
+
+ monitor_bindings_rw_attrs = [
+ 'servicename',
+ 'servicegroupname',
+ 'dup_state',
+ 'dup_weight',
+ 'monitorname',
+ 'weight',
+ ]
+
+ # Translate module arguments to correspondign config oject attributes
+ if module.params['ip'] is None:
+ module.params['ip'] = module.params['ipaddress']
+
+ service_proxy = ConfigProxy(
+ actual=service(),
+ client=client,
+ attribute_values_dict=module.params,
+ readwrite_attrs=readwrite_attrs,
+ readonly_attrs=readonly_attrs,
+ immutable_attrs=immutable_attrs,
+ transforms=transforms,
+ )
+
+ try:
+
+ # Apply appropriate state
+ if module.params['state'] == 'present':
+ log('Applying actions for state present')
+ if not service_exists(client, module):
+ if not module.check_mode:
+ service_proxy.add()
+ sync_monitor_bindings(client, module, monitor_bindings_rw_attrs)
+ if module.params['save_config']:
+ client.save_config()
+ module_result['changed'] = True
+ elif not all_identical(client, module, service_proxy, monitor_bindings_rw_attrs):
+
+ # Check if we try to change value of immutable attributes
+ diff_dict = diff(client, module, service_proxy)
+ immutables_changed = get_immutables_intersection(service_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)
+
+ # Service sync
+ if not service_identical(client, module, service_proxy):
+ if not module.check_mode:
+ service_proxy.update()
+
+ # Monitor bindings sync
+ if not monitor_bindings_identical(client, module, monitor_bindings_rw_attrs):
+ if not module.check_mode:
+ sync_monitor_bindings(client, module, monitor_bindings_rw_attrs)
+
+ module_result['changed'] = True
+ if not module.check_mode:
+ if module.params['save_config']:
+ client.save_config()
+ else:
+ module_result['changed'] = False
+
+ # Sanity check for state
+ if not module.check_mode:
+ log('Sanity checks for state present')
+ if not service_exists(client, module):
+ module.fail_json(msg='Service does not exist', **module_result)
+
+ if not service_identical(client, module, service_proxy):
+ module.fail_json(msg='Service differs from configured', diff=diff(client, module, service_proxy), **module_result)
+
+ if not monitor_bindings_identical(client, module, monitor_bindings_rw_attrs):
+ module.fail_json(msg='Monitor bindings are not identical', **module_result)
+
+ elif module.params['state'] == 'absent':
+ log('Applying actions for state absent')
+ if service_exists(client, module):
+ if not module.check_mode:
+ service_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 service_exists(client, module):
+ module.fail_json(msg='Service still exists', **module_result)
+
+ except nitro_exception as e:
+ msg = "nitro exception errorcode=%s, message=%s" % (str(e.errorcode), e.message)
+ module.fail_json(msg=msg, **module_result)
+
+ client.logout()
+ module.exit_json(**module_result)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/lib/ansible/utils/module_docs_fragments/netscaler.py b/lib/ansible/utils/module_docs_fragments/netscaler.py
new file mode 100644
index 0000000000..025ee2328f
--- /dev/null
+++ b/lib/ansible/utils/module_docs_fragments/netscaler.py
@@ -0,0 +1,52 @@
+class ModuleDocFragment(object):
+ DOCUMENTATION = '''
+
+options:
+ nsip:
+ description:
+ - The ip address of the netscaler appliance where the nitro API calls will be made.
+ - "The port can be specified with the colon (:). E.g. 192.168.1.1:555."
+ required: True
+
+ nitro_user:
+ description:
+ - The username with which to authenticate to the netscaler node.
+ required: True
+
+ nitro_pass:
+ description:
+ - The password with which to authenticate to the netscaler node.
+ required: True
+
+ nitro_protocol:
+ choices: [ 'http', 'https' ]
+ default: http
+ description:
+ - Which protocol to use when accessing the nitro API objects.
+
+ validate_certs:
+ description:
+ - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates.
+ required: false
+ default: 'yes'
+
+ nitro_timeout:
+ description:
+ - Time in seconds until a timeout error is thrown when establishing a new session with Netscaler
+ default: 310
+
+ state:
+ choices: ['present', 'absent']
+ default: 'present'
+ description:
+ - The state of the resource being configured by the module on the netscaler node.
+ - When present the resource will be created if needed and configured according to the module's parameters.
+ - When absent the resource will be deleted from the netscaler node.
+
+ save_config:
+ description:
+ - If true the module will save the configuration on the netscaler node if it makes any changes.
+ - The module will not save the configuration on the netscaler node if it made no changes.
+ type: bool
+ default: true
+'''
diff --git a/test/integration/netscaler.yaml b/test/integration/netscaler.yaml
new file mode 100644
index 0000000000..262b0ad22a
--- /dev/null
+++ b/test/integration/netscaler.yaml
@@ -0,0 +1,11 @@
+- hosts: netscaler
+
+ gather_facts: no
+ connection: local
+
+ vars:
+ limit_to: "*"
+ debug: false
+
+ roles:
+ - { role: netscaler_service, when: "limit_to in ['*', 'netscaler_service']" }
diff --git a/test/integration/roles/netscaler_service/defaults/main.yaml b/test/integration/roles/netscaler_service/defaults/main.yaml
new file mode 100644
index 0000000000..641801f660
--- /dev/null
+++ b/test/integration/roles/netscaler_service/defaults/main.yaml
@@ -0,0 +1,6 @@
+---
+testcase: "*"
+test_cases: []
+
+nitro_user: nsroot
+nitro_pass: nsroot
diff --git a/test/integration/roles/netscaler_service/sample_inventory b/test/integration/roles/netscaler_service/sample_inventory
new file mode 100644
index 0000000000..7da2dbbdb8
--- /dev/null
+++ b/test/integration/roles/netscaler_service/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_service/tasks/main.yaml b/test/integration/roles/netscaler_service/tasks/main.yaml
new file mode 100644
index 0000000000..729619a17c
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tasks/main.yaml
@@ -0,0 +1,2 @@
+---
+- { include: nitro.yaml, tags: ['nitro'] }
diff --git a/test/integration/roles/netscaler_service/tasks/nitro.yaml b/test/integration/roles/netscaler_service/tasks/nitro.yaml
new file mode 100644
index 0000000000..00ab502dda
--- /dev/null
+++ b/test/integration/roles/netscaler_service/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_service/tests/nitro/adns_service.yaml b/test/integration/roles/netscaler_service/tests/nitro/adns_service.yaml
new file mode 100644
index 0000000000..97b9ccb0c9
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/adns_service.yaml
@@ -0,0 +1,57 @@
+---
+
+- include: "{{ role_path }}/tests/nitro/adns_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/adns_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
diff --git a/test/integration/roles/netscaler_service/tests/nitro/adns_service/remove.yaml b/test/integration/roles/netscaler_service/tests/nitro/adns_service/remove.yaml
new file mode 100644
index 0000000000..3b3e0f9e23
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/adns_service/remove.yaml
@@ -0,0 +1,14 @@
+---
+
+- name: Remove adns service
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: absent
+ name: service-adns
diff --git a/test/integration/roles/netscaler_service/tests/nitro/adns_service/setup.yaml b/test/integration/roles/netscaler_service/tests/nitro/adns_service/setup.yaml
new file mode 100644
index 0000000000..c44e1a8dd5
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/adns_service/setup.yaml
@@ -0,0 +1,17 @@
+---
+
+- name: Setup adns service
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+ state: present
+
+ name: service-adns
+ ipaddress: 192.168.1.3
+ port: 80
+ servicetype: ADNS
diff --git a/test/integration/roles/netscaler_service/tests/nitro/http_service.yaml b/test/integration/roles/netscaler_service/tests/nitro/http_service.yaml
new file mode 100644
index 0000000000..4111bd4a2a
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/http_service.yaml
@@ -0,0 +1,85 @@
+---
+
+- include: "{{ role_path }}/tests/nitro/http_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/update.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/update.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/update.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/update.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/http_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
diff --git a/test/integration/roles/netscaler_service/tests/nitro/http_service/remove.yaml b/test/integration/roles/netscaler_service/tests/nitro/http_service/remove.yaml
new file mode 100644
index 0000000000..0be82a9041
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/http_service/remove.yaml
@@ -0,0 +1,16 @@
+---
+
+- name: Remove htttp service
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: absent
+
+ name: service-http
+
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
diff --git a/test/integration/roles/netscaler_service/tests/nitro/http_service/setup.yaml b/test/integration/roles/netscaler_service/tests/nitro/http_service/setup.yaml
new file mode 100644
index 0000000000..c55aaac95b
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/http_service/setup.yaml
@@ -0,0 +1,53 @@
+---
+
+- name: Setup http service
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: present
+
+ name: service-http
+ ip: 192.168.1.1
+ ipaddress: 192.168.1.1
+ port: 80
+ servicetype: HTTP
+ cachetype: TRANSPARENT
+ maxclient: 100
+ healthmonitor: no
+ maxreq: 200
+ cacheable: no
+ cip: ENABLED
+ cipheader: client-ip
+ usip: yes
+ useproxyport: yes
+ sc: off
+ sp: off
+ rtspsessionidremap: off
+ clttimeout: 100
+ svrtimeout: 100
+ customserverid: 476
+ cka: yes
+ tcpb: yes
+ cmp: no
+ maxbandwidth: 10000
+ accessdown: "NO"
+ monthreshold: 100
+ downstateflush: ENABLED
+ hashid: 10
+ comment: some comment
+ appflowlog: ENABLED
+ processlocal: ENABLED
+ graceful: no
+
+ monitor_bindings:
+ - monitorname: ping
+ weight: 50
+ - monitorname: http
+ weight: 50
+
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
diff --git a/test/integration/roles/netscaler_service/tests/nitro/http_service/update.yaml b/test/integration/roles/netscaler_service/tests/nitro/http_service/update.yaml
new file mode 100644
index 0000000000..21640617a7
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/http_service/update.yaml
@@ -0,0 +1,51 @@
+---
+
+- name: Update http service
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: present
+
+ name: service-http
+ ip: 192.168.1.1
+ ipaddress: 192.168.1.1
+ port: 80
+ servicetype: HTTP
+ cachetype: TRANSPARENT
+ maxclient: 100
+ healthmonitor: no
+ maxreq: 200
+ cacheable: no
+ cip: ENABLED
+ cipheader: client-ip
+ usip: yes
+ useproxyport: yes
+ sc: off
+ sp: off
+ rtspsessionidremap: off
+ clttimeout: 100
+ svrtimeout: 100
+ customserverid: 476
+ cka: yes
+ tcpb: yes
+ cmp: no
+ maxbandwidth: 20000
+ accessdown: "NO"
+ monthreshold: 100
+ downstateflush: ENABLED
+ hashid: 10
+ comment: some comment
+ appflowlog: ENABLED
+ processlocal: ENABLED
+ netprofile: net-profile-1
+
+ monitor_bindings:
+ - monitorname: http
+ weight: 100
+
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
diff --git a/test/integration/roles/netscaler_service/tests/nitro/ssl_service.yaml b/test/integration/roles/netscaler_service/tests/nitro/ssl_service.yaml
new file mode 100644
index 0000000000..535918a544
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/ssl_service.yaml
@@ -0,0 +1,57 @@
+---
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/setup.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/setup.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/remove.yaml"
+ vars:
+ check_mode: yes
+
+- assert:
+ that: not result|changed
+
+- include: "{{ role_path }}/tests/nitro/ssl_service/remove.yaml"
+ vars:
+ check_mode: no
+
+- assert:
+ that: not result|changed
diff --git a/test/integration/roles/netscaler_service/tests/nitro/ssl_service/remove.yaml b/test/integration/roles/netscaler_service/tests/nitro/ssl_service/remove.yaml
new file mode 100644
index 0000000000..3f787d2224
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/ssl_service/remove.yaml
@@ -0,0 +1,14 @@
+---
+
+- name: Remove ssl service
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: absent
+ name: service-ssl
diff --git a/test/integration/roles/netscaler_service/tests/nitro/ssl_service/setup.yaml b/test/integration/roles/netscaler_service/tests/nitro/ssl_service/setup.yaml
new file mode 100644
index 0000000000..565816b5c6
--- /dev/null
+++ b/test/integration/roles/netscaler_service/tests/nitro/ssl_service/setup.yaml
@@ -0,0 +1,18 @@
+---
+
+- name: Setup ssl service
+ delegate_to: localhost
+ register: result
+ check_mode: "{{ check_mode }}"
+ netscaler_service:
+
+ nitro_user: "{{nitro_user}}"
+ nitro_pass: "{{nitro_pass}}"
+ nsip: "{{nsip}}"
+
+ state: present
+ name: service-ssl
+ ipaddress: 192.168.1.2
+ port: 80
+ servicetype: SSL
+ cleartextport: 88
diff --git a/test/units/modules/network/netscaler/__init__.py b/test/units/modules/network/netscaler/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/units/modules/network/netscaler/netscaler_module.py b/test/units/modules/network/netscaler/netscaler_module.py
new file mode 100644
index 0000000000..9b2b2e83e1
--- /dev/null
+++ b/test/units/modules/network/netscaler/netscaler_module.py
@@ -0,0 +1,69 @@
+import sys
+
+from ansible.compat.tests.mock import patch, Mock
+from ansible.compat.tests import unittest
+from ansible.module_utils import basic
+import json
+from ansible.module_utils._text import to_bytes
+
+base_modules_mock = Mock()
+nitro_service_mock = Mock()
+nitro_exception_mock = Mock()
+
+
+base_modules_to_mock = {
+ 'nssrc': base_modules_mock,
+ 'nssrc.com': base_modules_mock,
+ 'nssrc.com.citrix': base_modules_mock,
+ 'nssrc.com.citrix.netscaler': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.resource': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.exception': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.exception.nitro_exception': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.exception.nitro_exception.nitro_exception': nitro_exception_mock,
+ 'nssrc.com.citrix.netscaler.nitro.service': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.service.nitro_service': base_modules_mock,
+ 'nssrc.com.citrix.netscaler.nitro.service.nitro_service.nitro_service': nitro_service_mock,
+}
+
+nitro_base_patcher = patch.dict(sys.modules, base_modules_to_mock)
+
+
+def set_module_args(args):
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+ pass
+
+
+class AnsibleFailJson(Exception):
+ pass
+
+
+class TestModule(unittest.TestCase):
+ def failed(self):
+ def fail_json(*args, **kwargs):
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+ with patch.object(basic.AnsibleModule, 'fail_json', fail_json):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertTrue(result['failed'], result)
+ return result
+
+ def exited(self, changed=False):
+ def exit_json(*args, **kwargs):
+ raise AnsibleExitJson(kwargs)
+
+ with patch.object(basic.AnsibleModule, 'exit_json', exit_json):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ return result
diff --git a/test/units/modules/network/netscaler/test_netscaler_module_utils.py b/test/units/modules/network/netscaler/test_netscaler_module_utils.py
new file mode 100644
index 0000000000..2a2d62113b
--- /dev/null
+++ b/test/units/modules/network/netscaler/test_netscaler_module_utils.py
@@ -0,0 +1,175 @@
+
+# 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 import unittest
+from ansible.compat.tests.mock import Mock
+
+
+from ansible.module_utils.netscaler import ConfigProxy, get_immutables_intersection, ensure_feature_is_enabled, log, loglines
+
+
+class TestNetscalerConfigProxy(unittest.TestCase):
+
+ def test_values_copied_to_actual(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'some_key': 'some_value',
+ }
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['some_key']
+ )
+ self.assertEqual(actual.some_key, values['some_key'], msg='Failed to pass correct value from values dict')
+
+ def test_none_values_not_copied_to_actual(self):
+ actual = Mock()
+ client = Mock()
+ actual.key_for_none = 'initial'
+ print('actual %s' % actual.key_for_none)
+ values = {
+ 'key_for_none': None,
+ }
+ print('value %s' % actual.key_for_none)
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['key_for_none']
+ )
+ self.assertEqual(actual.key_for_none, 'initial')
+
+ def test_missing_from_values_dict_not_copied_to_actual(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'irrelevant_key': 'irrelevant_value',
+ }
+ print('value %s' % actual.key_for_none)
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['key_for_none']
+ )
+ print('none %s' % getattr(actual, 'key_for_none'))
+ self.assertIsInstance(actual.key_for_none, Mock)
+
+ def test_bool_yes_no_transform(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'yes_key': True,
+ 'no_key': False,
+ }
+ transforms = {
+ 'yes_key': ['bool_yes_no'],
+ 'no_key': ['bool_yes_no']
+ }
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['yes_key', 'no_key'],
+ transforms=transforms,
+ )
+ actual_values = [actual.yes_key, actual.no_key]
+ self.assertListEqual(actual_values, ['YES', 'NO'])
+
+ def test_bool_on_off_transform(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'on_key': True,
+ 'off_key': False,
+ }
+ transforms = {
+ 'on_key': ['bool_on_off'],
+ 'off_key': ['bool_on_off']
+ }
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['on_key', 'off_key'],
+ transforms=transforms,
+ )
+ actual_values = [actual.on_key, actual.off_key]
+ self.assertListEqual(actual_values, ['ON', 'OFF'])
+
+ def test_callable_transform(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'transform_key': 'hello',
+ 'transform_chain': 'hello',
+ }
+ transforms = {
+ 'transform_key': [lambda v: v.upper()],
+ 'transform_chain': [lambda v: v.upper(), lambda v: v[:4]]
+ }
+ ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['transform_key', 'transform_chain'],
+ transforms=transforms,
+ )
+ actual_values = [actual.transform_key, actual.transform_chain]
+ self.assertListEqual(actual_values, ['HELLO', 'HELL'])
+
+
+class TestNetscalerModuleUtils(unittest.TestCase):
+
+ def test_immutables_intersection(self):
+ actual = Mock()
+ client = Mock()
+ values = {
+ 'mutable_key': 'some value',
+ 'immutable_key': 'some other value',
+ }
+ proxy = ConfigProxy(
+ actual=actual,
+ client=client,
+ attribute_values_dict=values,
+ readwrite_attrs=['mutable_key', 'immutable_key'],
+ immutable_attrs=['immutable_key'],
+ )
+ keys_to_check = ['mutable_key', 'immutable_key', 'non_existant_key']
+ result = get_immutables_intersection(proxy, keys_to_check)
+ self.assertListEqual(result, ['immutable_key'])
+
+ def test_ensure_feature_is_enabled(self):
+ client = Mock()
+ attrs = {'get_enabled_features.return_value': ['GSLB']}
+ client.configure_mock(**attrs)
+ ensure_feature_is_enabled(client, 'GSLB')
+ ensure_feature_is_enabled(client, 'LB')
+ client.enable_features.assert_called_once_with('LB')
+
+ def test_log_function(self):
+ messages = [
+ 'First message',
+ 'Second message',
+ ]
+ log(messages[0])
+ log(messages[1])
+ self.assertListEqual(messages, loglines, msg='Log messages not recorded correctly')
diff --git a/test/units/modules/network/netscaler/test_netscaler_service.py b/test/units/modules/network/netscaler/test_netscaler_service.py
new file mode 100644
index 0000000000..9b9fb98e05
--- /dev/null
+++ b/test/units/modules/network/netscaler/test_netscaler_service.py
@@ -0,0 +1,343 @@
+
+# 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 TestNetscalerServiceModule(TestModule):
+
+ @classmethod
+ def setUpClass(cls):
+ m = MagicMock()
+ cls.service_mock = MagicMock()
+ cls.service_mock.__class__ = MagicMock()
+ cls.service_lbmonitor_binding_mock = MagicMock()
+ cls.lbmonitor_service_binding_mock = MagicMock()
+ nssrc_modules_mock = {
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.basic': m,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.service': m,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.service.service': cls.service_mock,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.service_lbmonitor_binding': cls.service_lbmonitor_binding_mock,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.service_lbmonitor_binding.service_lbmonitor_binding': m,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.lb': m,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_service_binding': m,
+ 'nssrc.com.citrix.netscaler.nitro.resource.config.lb.lbmonitor_service_binding.lbmonitor_service_binding': cls.lbmonitor_service_binding_mock,
+ }
+
+ 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_service
+ self.module = netscaler_service
+ 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_service
+
+ 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_service.get_nitro_client', m):
+ with patch('ansible.modules.network.netscaler.netscaler_service.nitro_exception', MockException):
+ self.module = netscaler_service
+ 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_service
+
+ class MockException(Exception):
+ pass
+ 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_service',
+ get_nitro_client=m,
+ nitro_exception=MockException,
+ ):
+ self.module = netscaler_service
+ 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_service
+
+ if sys.version_info[:2] == (2, 6):
+ self.skipTest('requests library not available under python2.6')
+
+ class MockException(Exception):
+ pass
+ 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_service',
+ get_nitro_client=m,
+ nitro_exception=MockException,
+ ):
+ self.module = netscaler_service
+ result = self.failed()
+ self.assertTrue(result['msg'].startswith('SSL Error'), msg='SSL Error was not handled gracefully')
+
+ def test_create_non_existing_service(self):
+ self.set_module_state('present')
+ from ansible.modules.network.netscaler import netscaler_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[False, True])
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+ ):
+ self.module = netscaler_service
+ result = self.exited()
+ service_proxy_mock.assert_has_calls([call.add()])
+ self.assertTrue(result['changed'], msg='Change not recorded')
+
+ def test_update_service_when_service_differs(self):
+ self.set_module_state('present')
+ from ansible.modules.network.netscaler import netscaler_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[True, True])
+ service_identical_mock = Mock(side_effect=[False, True])
+ monitor_bindings_identical_mock = Mock(side_effect=[True, True])
+ all_identical_mock = Mock(side_effect=[False])
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+ service_identical=service_identical_mock,
+ monitor_bindings_identical=monitor_bindings_identical_mock,
+ all_identical=all_identical_mock,
+ ):
+ self.module = netscaler_service
+ result = self.exited()
+ service_proxy_mock.assert_has_calls([call.update()])
+ self.assertTrue(result['changed'], msg='Change not recorded')
+
+ def test_update_service_when_monitor_bindings_differ(self):
+ self.set_module_state('present')
+ from ansible.modules.network.netscaler import netscaler_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[True, True])
+ service_identical_mock = Mock(side_effect=[True, True])
+ monitor_bindings_identical_mock = Mock(side_effect=[False, True])
+ all_identical_mock = Mock(side_effect=[False])
+ sync_monitor_bindings_mock = Mock()
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+ service_identical=service_identical_mock,
+ monitor_bindings_identical=monitor_bindings_identical_mock,
+ all_identical=all_identical_mock,
+ sync_monitor_bindings=sync_monitor_bindings_mock,
+ ):
+ self.module = netscaler_service
+ 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_no_change_to_module_when_all_identical(self):
+ self.set_module_state('present')
+ from ansible.modules.network.netscaler import netscaler_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[True, True])
+ service_identical_mock = Mock(side_effect=[True, True])
+ monitor_bindings_identical_mock = Mock(side_effect=[True, True])
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+ service_identical=service_identical_mock,
+ monitor_bindings_identical=monitor_bindings_identical_mock,
+ ):
+ self.module = netscaler_service
+ 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_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[True, False])
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+
+ ):
+ self.module = netscaler_service
+ result = self.exited()
+ service_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_service
+ service_proxy_mock = MagicMock()
+ attrs = {
+ 'diff_object.return_value': {},
+ }
+ service_proxy_mock.configure_mock(**attrs)
+
+ m = MagicMock(return_value=service_proxy_mock)
+ service_exists_mock = Mock(side_effect=[False, False])
+
+ with patch.multiple(
+ 'ansible.modules.network.netscaler.netscaler_service',
+ ConfigProxy=m,
+ service_exists=service_exists_mock,
+
+ ):
+ self.module = netscaler_service
+ result = self.exited()
+ service_proxy_mock.assert_not_called()
+ self.assertFalse(result['changed'], msg='Changed status not set correctly')
+
+ def test_graceful_nitro_exception_operation_present(self):
+ self.set_module_state('present')
+ from ansible.modules.network.netscaler import netscaler_service
+
+ 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_service',
+ service_exists=m,
+ nitro_exception=MockException
+ ):
+ self.module = netscaler_service
+ 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_service
+
+ 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_service',
+ service_exists=m,
+ nitro_exception=MockException
+ ):
+ self.module = netscaler_service
+ result = self.failed()
+ self.assertTrue(
+ result['msg'].startswith('nitro exception'),
+ msg='Nitro exception not caught on operation absent'
+ )