From 31b4ae2e6aa4cb971551f7e2abccaff533281751 Mon Sep 17 00:00:00 2001 From: George Nikolopoulos Date: Thu, 3 Aug 2017 17:26:17 +0300 Subject: [PATCH] New module: manage Citrix Netscaler GSLB site configuration (network/netscaler/netscaler_gslb_site) (#27639) * Add netscaler_gslb_site * Lowercase enabled, disabled option values * Add fixes in netscaler module utils needed for unit test success --- .../network/netscaler/netscaler_gslb_site.py | 423 +++++++++++ .../netscaler_gslb_site/defaults/main.yaml | 6 + .../roles/netscaler_gslb_site/tasks/main.yaml | 3 + .../netscaler_gslb_site/tasks/nitro.yaml | 14 + .../tests/nitro/gslb_site.yaml | 85 +++ .../tests/nitro/gslb_site/remove.yaml | 13 + .../tests/nitro/gslb_site/setup.yaml | 19 + .../tests/nitro/gslb_site/update.yaml | 20 + .../netscaler/test_netscaler_gslb_site.py | 663 ++++++++++++++++++ 9 files changed, 1246 insertions(+) create mode 100644 lib/ansible/modules/network/netscaler/netscaler_gslb_site.py create mode 100644 test/integration/roles/netscaler_gslb_site/defaults/main.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tasks/main.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tasks/nitro.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/remove.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/setup.yaml create mode 100644 test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/update.yaml create mode 100644 test/units/modules/network/netscaler/test_netscaler_gslb_site.py diff --git a/lib/ansible/modules/network/netscaler/netscaler_gslb_site.py b/lib/ansible/modules/network/netscaler/netscaler_gslb_site.py new file mode 100644 index 00000000000..67f54c6e094 --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_gslb_site.py @@ -0,0 +1,423 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Citrix Systems +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} + + +DOCUMENTATION = ''' +--- +module: netscaler_gslb_site +short_description: Manage gslb site entities in Netscaler. +description: + - Manage gslb site entities in Netscaler. + +version_added: "2.4.0" + +author: George Nikolopoulos (@giorgos-nikolopoulos) + +options: + + sitename: + description: + - >- + Name for the GSLB site. Must begin with an ASCII alphanumeric 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 virtual server is created. + - "Minimum length = 1" + + sitetype: + choices: + - 'REMOTE' + - 'LOCAL' + description: + - >- + Type of site to create. If the type is not specified, the appliance automatically detects and sets + the type on the basis of the IP address being assigned to the site. If the specified site IP address + is owned by the appliance (for example, a MIP address or SNIP address), the site is a local site. + Otherwise, it is a remote site. + + siteipaddress: + description: + - >- + IP address for the GSLB site. The GSLB site uses this IP address to communicate with other GSLB + sites. For a local site, use any IP address that is owned by the appliance (for example, a SNIP or + MIP address, or the IP address of the ADNS service). + - "Minimum length = 1" + + publicip: + description: + - >- + Public IP address for the local site. Required only if the appliance is deployed in a private address + space and the site has a public IP address hosted on an external firewall or a NAT device. + - "Minimum length = 1" + + metricexchange: + choices: + - 'enabled' + - 'disabled' + description: + - >- + Exchange metrics with other sites. Metrics are exchanged by using Metric Exchange Protocol (MEP). The + appliances in the GSLB setup exchange health information once every second. + - >- + If you disable metrics exchange, you can use only static load balancing methods (such as round robin, + static proximity, or the hash-based methods), and if you disable metrics exchange when a dynamic load + balancing method (such as least connection) is in operation, the appliance falls back to round robin. + Also, if you disable metrics exchange, you must use a monitor to determine the state of GSLB + services. Otherwise, the service is marked as DOWN. + + nwmetricexchange: + choices: + - 'enabled' + - 'disabled' + description: + - >- + Exchange, with other GSLB sites, network metrics such as round-trip time (RTT), learned from + communications with various local DNS (LDNS) servers used by clients. RTT information is used in the + dynamic RTT load balancing method, and is exchanged every 5 seconds. + + sessionexchange: + choices: + - 'enabled' + - 'disabled' + description: + - "Exchange persistent session entries with other GSLB sites every five seconds." + + triggermonitor: + choices: + - 'ALWAYS' + - 'MEPDOWN' + - 'MEPDOWN_SVCDOWN' + description: + - >- + Specify the conditions under which the GSLB service must be monitored by a monitor, if one is bound. + Available settings function as follows: + - "* C(ALWAYS) - Monitor the GSLB service at all times." + - >- + * C(MEPDOWN) - Monitor the GSLB service only when the exchange of metrics through the Metrics Exchange + Protocol (MEP) is disabled. + - "C(MEPDOWN_SVCDOWN) - Monitor the service in either of the following situations:" + - "* The exchange of metrics through MEP is disabled." + - >- + * The exchange of metrics through MEP is enabled but the status of the service, learned through + metrics exchange, is DOWN. + + parentsite: + description: + - "Parent site of the GSLB site, in a parent-child topology." + + clip: + description: + - >- + Cluster IP address. Specify this parameter to connect to the remote cluster site for GSLB auto-sync. + Note: The cluster IP address is defined when creating the cluster. + + publicclip: + description: + - >- + IP address to be used to globally access the remote cluster when it is deployed behind a NAT. It can + be same as the normal cluster IP address. + + naptrreplacementsuffix: + description: + - >- + The naptr replacement suffix configured here will be used to construct the naptr replacement field in + NAPTR record. + - "Minimum length = 1" + + +extends_documentation_fragment: netscaler +requirements: + - nitro python sdk +''' + +EXAMPLES = ''' +- name: Setup gslb site + delegate_to: localhost + netscaler_gslb_site: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + + sitename: gslb-site-1 + siteipaddress: 192.168.1.1 + sitetype: LOCAL + publicip: 192.168.1.1 + metricexchange: enabled + nwmetricexchange: enabled + sessionexchange: enabled + triggermonitor: ALWAYS + +''' + +RETURN = ''' +loglines: + description: list of logged messages by the module + returned: always + type: list + sample: "['message 1', 'message 2']" + +msg: + description: Message detailing the failure reason + returned: failure + type: string + sample: "Action does not exist" + +diff: + description: List of differences between the actual configured object and the configuration specified in the module + returned: failure + type: dictionary + sample: "{ 'targetlbvserver': 'difference. ours: (str) server1 other: (str) server2' }" +''' + +try: + from nssrc.com.citrix.netscaler.nitro.resource.config.gslb.gslbsite import gslbsite + from nssrc.com.citrix.netscaler.nitro.exception.nitro_exception import nitro_exception + PYTHON_SDK_IMPORTED = True +except ImportError as e: + PYTHON_SDK_IMPORTED = False + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.netscaler import ( + ConfigProxy, + get_nitro_client, + netscaler_common_arguments, + log, + loglines, + ensure_feature_is_enabled, + get_immutables_intersection, +) + + +def gslb_site_exists(client, module): + if gslbsite.count_filtered(client, 'sitename:%s' % module.params['sitename']) > 0: + return True + else: + return False + + +def gslb_site_identical(client, module, gslb_site_proxy): + gslb_site_list = gslbsite.get_filtered(client, 'sitename:%s' % module.params['sitename']) + diff_dict = gslb_site_proxy.diff_object(gslb_site_list[0]) + if len(diff_dict) == 0: + return True + else: + return False + + +def diff_list(client, module, gslb_site_proxy): + gslb_site_list = gslbsite.get_filtered(client, 'sitename:%s' % module.params['sitename']) + return gslb_site_proxy.diff_object(gslb_site_list[0]) + + +def main(): + + module_specific_arguments = dict( + sitename=dict(type='str'), + sitetype=dict( + type='str', + choices=[ + 'REMOTE', + 'LOCAL', + ] + ), + siteipaddress=dict(type='str'), + publicip=dict(type='str'), + metricexchange=dict( + type='str', + choices=[ + 'enabled', + 'disabled', + ] + ), + nwmetricexchange=dict( + type='str', + choices=[ + 'enabled', + 'disabled', + ] + ), + sessionexchange=dict( + type='str', + choices=[ + 'enabled', + 'disabled', + ] + ), + triggermonitor=dict( + type='str', + choices=[ + 'ALWAYS', + 'MEPDOWN', + 'MEPDOWN_SVCDOWN', + ] + ), + parentsite=dict(type='str'), + clip=dict(type='str'), + publicclip=dict(type='str'), + naptrreplacementsuffix=dict(type='str'), + ) + + hand_inserted_arguments = dict( + ) + + argument_spec = dict() + + argument_spec.update(netscaler_common_arguments) + argument_spec.update(module_specific_arguments) + argument_spec.update(hand_inserted_arguments) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + module_result = dict( + changed=False, + failed=False, + loglines=loglines, + ) + + # Fail the module if imports failed + if not PYTHON_SDK_IMPORTED: + module.fail_json(msg='Could not load nitro python sdk') + + # Fallthrough to rest of execution + client = get_nitro_client(module) + + try: + client.login() + except nitro_exception as e: + msg = "nitro exception during login. errorcode=%s, message=%s" % (str(e.errorcode), e.message) + module.fail_json(msg=msg) + except Exception as e: + if str(type(e)) == "": + module.fail_json(msg='Connection error %s' % str(e)) + elif str(type(e)) == "": + module.fail_json(msg='SSL Error %s' % str(e)) + else: + module.fail_json(msg='Unexpected error during login %s' % str(e)) + + readwrite_attrs = [ + 'sitename', + 'sitetype', + 'siteipaddress', + 'publicip', + 'metricexchange', + 'nwmetricexchange', + 'sessionexchange', + 'triggermonitor', + 'parentsite', + 'clip', + 'publicclip', + 'naptrreplacementsuffix', + ] + + readonly_attrs = [ + 'status', + 'persistencemepstatus', + 'version', + '__count', + ] + + immutable_attrs = [ + 'sitename', + 'sitetype', + 'siteipaddress', + 'publicip', + 'parentsite', + 'clip', + 'publicclip', + ] + + transforms = { + 'metricexchange': [lambda v: v.upper()], + 'nwmetricexchange': [lambda v: v.upper()], + 'sessionexchange': [lambda v: v.upper()], + } + + # Instantiate config proxy + gslb_site_proxy = ConfigProxy( + actual=gslbsite(), + client=client, + attribute_values_dict=module.params, + readwrite_attrs=readwrite_attrs, + readonly_attrs=readonly_attrs, + immutable_attrs=immutable_attrs, + transforms=transforms, + ) + + try: + ensure_feature_is_enabled(client, 'GSLB') + + # Apply appropriate state + if module.params['state'] == 'present': + log('Applying actions for state present') + if not gslb_site_exists(client, module): + if not module.check_mode: + gslb_site_proxy.add() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + elif not gslb_site_identical(client, module, gslb_site_proxy): + + # Check if we try to change value of immutable attributes + immutables_changed = get_immutables_intersection(gslb_site_proxy, diff_list(client, module, gslb_site_proxy).keys()) + if immutables_changed != []: + module.fail_json( + msg='Cannot update immutable attributes %s' % (immutables_changed,), + diff=diff_list(client, module, gslb_site_proxy), + **module_result + ) + + if not module.check_mode: + gslb_site_proxy.update() + 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 present') + if not gslb_site_exists(client, module): + module.fail_json(msg='GSLB site does not exist', **module_result) + if not gslb_site_identical(client, module, gslb_site_proxy): + module.fail_json(msg='GSLB site differs from configured', diff=diff_list(client, module, gslb_site_proxy), **module_result) + + elif module.params['state'] == 'absent': + log('Applying actions for state absent') + if gslb_site_exists(client, module): + if not module.check_mode: + gslb_site_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 gslb_site_exists(client, module): + module.fail_json(msg='GSLB site 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/test/integration/roles/netscaler_gslb_site/defaults/main.yaml b/test/integration/roles/netscaler_gslb_site/defaults/main.yaml new file mode 100644 index 00000000000..641801f6600 --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +testcase: "*" +test_cases: [] + +nitro_user: nsroot +nitro_pass: nsroot diff --git a/test/integration/roles/netscaler_gslb_site/tasks/main.yaml b/test/integration/roles/netscaler_gslb_site/tasks/main.yaml new file mode 100644 index 00000000000..84af8dca9e9 --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/tasks/main.yaml @@ -0,0 +1,3 @@ +--- + +- { include: nitro.yaml, tags: ['nitro'] } diff --git a/test/integration/roles/netscaler_gslb_site/tasks/nitro.yaml b/test/integration/roles/netscaler_gslb_site/tasks/nitro.yaml new file mode 100644 index 00000000000..00ab502dda9 --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/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_gslb_site/tests/nitro/gslb_site.yaml b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site.yaml new file mode 100644 index 00000000000..e30833ea902 --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site.yaml @@ -0,0 +1,85 @@ +--- + +- include: "{{ role_path }}/tests/nitro/gslb_site/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/update.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/gslb_site/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/remove.yaml b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/remove.yaml new file mode 100644 index 00000000000..30f32012f8e --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/remove.yaml @@ -0,0 +1,13 @@ +--- + +- name: Setup gslb site + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_gslb_site: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + sitename: gslb-site-1 diff --git a/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/setup.yaml b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/setup.yaml new file mode 100644 index 00000000000..4cb3e9a909a --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/setup.yaml @@ -0,0 +1,19 @@ +--- + +- name: Setup gslb site + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_gslb_site: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + sitename: gslb-site-1 + siteipaddress: 192.168.1.1 + sitetype: LOCAL + publicip: 192.168.1.1 + metricexchange: enabled + nwmetricexchange: enabled + sessionexchange: enabled + triggermonitor: ALWAYS diff --git a/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/update.yaml b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/update.yaml new file mode 100644 index 00000000000..af7e0afab70 --- /dev/null +++ b/test/integration/roles/netscaler_gslb_site/tests/nitro/gslb_site/update.yaml @@ -0,0 +1,20 @@ +--- + +- name: Setup gslb site + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_gslb_site: + + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + sitename: gslb-site-1 + siteipaddress: 192.168.1.1 + sitetype: LOCAL + publicip: 192.168.1.1 + metricexchange: disabled + nwmetricexchange: enabled + sessionexchange: enabled + triggermonitor: ALWAYS diff --git a/test/units/modules/network/netscaler/test_netscaler_gslb_site.py b/test/units/modules/network/netscaler/test_netscaler_gslb_site.py new file mode 100644 index 00000000000..ba7413d879c --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_gslb_site.py @@ -0,0 +1,663 @@ + +# 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 +from .netscaler_module import TestModule, nitro_base_patcher, set_module_args + +import sys + +if sys.version_info[:2] != (2, 6): + import requests + + +class TestNetscalerGSLBSiteModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + + cls.MockException = MockException + + m = MagicMock() + nssrc_modules_mock = { + 'nssrc.com.citrix.netscaler.nitro.resource.config.gslb': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.gslb.gslbsite': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.gslb.gslbsite.gslbsite': m, + } + + cls.nitro_specific_patcher = patch.dict(sys.modules, nssrc_modules_mock) + cls.nitro_base_patcher = nitro_base_patcher + + @classmethod + def tearDownClass(cls): + cls.nitro_base_patcher.stop() + cls.nitro_specific_patcher.stop() + + def 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 + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + self.nitro_base_patcher.stop() + self.nitro_specific_patcher.stop() + from ansible.modules.network.netscaler import netscaler_gslb_site + self.module = netscaler_gslb_site + result = self.failed() + self.assertEqual(result['msg'], 'Could not load nitro python sdk') + + def test_graceful_nitro_error_on_login(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + 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_gslb_site.get_nitro_client', m): + with patch('ansible.modules.network.netscaler.netscaler_gslb_site.nitro_exception', MockException): + self.module = netscaler_gslb_site + 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') + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + 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_gslb_site', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertTrue(result['msg'].startswith('Connection error'), msg='Connection error was not handled gracefully') + + def test_graceful_login_error(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + 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_gslb_site', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertTrue(result['msg'].startswith('SSL Error'), msg='SSL Error was not handled gracefully') + + def test_ensure_feature_is_enabled_called(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + gslb_site_proxy_mock = Mock() + ensure_feature_is_enabled_mock = Mock() + client_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=Mock(return_value=client_mock), + gslb_site_exists=Mock(side_effect=[False, True]), + gslb_site_identical=Mock(side_effect=[True]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=ensure_feature_is_enabled_mock, + ConfigProxy=Mock(return_value=gslb_site_proxy_mock), + ): + self.module = netscaler_gslb_site + self.exited() + ensure_feature_is_enabled_mock.assert_called_with(client_mock, 'GSLB') + + def test_save_config_called_on_state_present(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + gslb_site_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + gslb_site_exists=Mock(side_effect=[False, True]), + gslb_site_identical=Mock(side_effect=[True]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=Mock(return_value=gslb_site_proxy_mock), + ): + self.module = netscaler_gslb_site + self.exited() + self.assertIn(call.save_config(), client_mock.mock_calls) + + def test_save_config_called_on_state_absent(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + gslb_site_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + gslb_site_exists=Mock(side_effect=[True, False]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=Mock(return_value=gslb_site_proxy_mock), + ): + self.module = netscaler_gslb_site + self.exited() + self.assertIn(call.save_config(), client_mock.mock_calls) + + def test_save_config_not_called_on_state_present(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + save_config=False, + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + gslb_site_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + gslb_site_exists=Mock(side_effect=[False, True]), + gslb_site_identical=Mock(side_effect=[True]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=Mock(return_value=gslb_site_proxy_mock), + ): + self.module = netscaler_gslb_site + self.exited() + self.assertNotIn(call.save_config(), client_mock.mock_calls) + + def test_save_config_not_called_on_state_absent(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + save_config=False, + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + gslb_site_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + gslb_site_exists=Mock(side_effect=[True, False]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=Mock(return_value=gslb_site_proxy_mock), + ): + self.module = netscaler_gslb_site + self.exited() + self.assertNotIn(call.save_config(), client_mock.mock_calls) + + def test_new_gslb_site_execution_flow(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + gslb_site_exists=Mock(side_effect=[False, True]), + gslb_site_identical=Mock(side_effect=[True]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + self.exited() + gslb_site_proxy_mock.assert_has_calls([call.add()]) + + def test_modified_gslb_site_execution_flow(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[True, True]), + gslb_site_identical=Mock(side_effect=[False, True]), + ensure_feature_is_enabled=Mock(), + nitro_exception=self.MockException, + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + self.exited() + gslb_site_proxy_mock.assert_has_calls([call.update()]) + + def test_absent_gslb_site_execution_flow(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[True, False]), + gslb_site_identical=Mock(side_effect=[False, True]), + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + self.exited() + gslb_site_proxy_mock.assert_has_calls([call.delete()]) + + def test_present_gslb_site_identical_flow(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[True, True]), + gslb_site_identical=Mock(side_effect=[True, True]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + self.exited() + gslb_site_proxy_mock.assert_not_called() + + def test_absent_gslb_site_noop_flow(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[False, False]), + gslb_site_identical=Mock(side_effect=[False, False]), + nitro_exception=self.MockException, + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + self.exited() + gslb_site_proxy_mock.assert_not_called() + + def test_present_gslb_site_failed_update(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[True, True]), + gslb_site_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertEqual(result['msg'], 'GSLB site differs from configured') + self.assertTrue(result['failed']) + + def test_present_gslb_site_failed_create(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[False, False]), + gslb_site_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertEqual(result['msg'], 'GSLB site does not exist') + self.assertTrue(result['failed']) + + def test_present_gslb_site_update_immutable_attribute(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=['domain']), + gslb_site_exists=Mock(side_effect=[True, True]), + gslb_site_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertEqual(result['msg'], 'Cannot update immutable attributes [\'domain\']') + self.assertTrue(result['failed']) + + def test_absent_gslb_site_failed_delete(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + glsb_site_proxy_attrs = { + 'diff_object.return_value': {}, + } + gslb_site_proxy_mock = Mock() + gslb_site_proxy_mock.configure_mock(**glsb_site_proxy_attrs) + config_proxy_mock = Mock(return_value=gslb_site_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_gslb_site', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + gslb_site_exists=Mock(side_effect=[True, True]), + gslb_site_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertEqual(result['msg'], 'GSLB site still exists') + self.assertTrue(result['failed']) + + def test_graceful_nitro_exception_state_present(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='present', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + 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_gslb_site', + gslb_site_exists=m, + ensure_feature_is_enabled=Mock(), + nitro_exception=MockException + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation absent' + ) + + def test_graceful_nitro_exception_state_absent(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + state='absent', + )) + from ansible.modules.network.netscaler import netscaler_gslb_site + + 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_gslb_site', + gslb_site_exists=m, + ensure_feature_is_enabled=Mock(), + nitro_exception=MockException + ): + self.module = netscaler_gslb_site + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation absent' + )