1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

New module: manage Citrix Netscaler service configuration (network/netscaler/netscaler_service) (#25129)

* netscaler_service initial implementation

* Changes as requested by reviewers

* Skip some tests if under python2.6 and importing requests library

* Change option "operation" to "state"

* Remove print statements from netscaler module utils

* Catch all exceptions during login

* Fix fail message

* Add common option save_config
This commit is contained in:
George Nikolopoulos 2017-06-08 17:33:32 +03:00 committed by Chris Alfonso
parent 2220362a5f
commit a00089c341
24 changed files with 2328 additions and 0 deletions

View file

@ -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.

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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)) == "<class 'requests.exceptions.ConnectionError'>":
module.fail_json(msg='Connection error %s' % str(e))
elif str(type(e)) == "<class 'requests.exceptions.SSLError'>":
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()

View file

@ -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
'''

View file

@ -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']" }

View file

@ -0,0 +1,6 @@
---
testcase: "*"
test_cases: []
nitro_user: nsroot
nitro_pass: nsroot

View file

@ -0,0 +1,5 @@
[netscaler]
172.18.0.2 nsip=172.18.0.2 nitro_user=nsroot nitro_pass=nsroot

View file

@ -0,0 +1,2 @@
---
- { include: nitro.yaml, tags: ['nitro'] }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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 }}"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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')

View file

@ -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 <http://www.gnu.org/licenses/>.
#
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'
)