From 11836a02963bbc38529fa661527fc5413e64854b Mon Sep 17 00:00:00 2001 From: George Nikolopoulos Date: Wed, 28 Jun 2017 16:46:46 +0300 Subject: [PATCH] Add module netscaler_cs_action (#26147) --- .../network/netscaler/netscaler_cs_action.py | 302 +++++++++ .../netscaler_cs_action/defaults/main.yaml | 6 + .../netscaler_cs_action/sample_inventory | 5 + .../roles/netscaler_cs_action/tasks/main.yaml | 6 + .../netscaler_cs_action/tasks/nitro.yaml | 14 + .../netscaler_cs_action/tasks/testbed.yaml | 16 + .../tests/nitro/target_expression.yaml | 57 ++ .../tests/nitro/target_expression/remove.yaml | 13 + .../tests/nitro/target_expression/setup.yaml | 14 + .../tests/nitro/target_lb_vserver.yaml | 85 +++ .../tests/nitro/target_lb_vserver/remove.yaml | 13 + .../tests/nitro/target_lb_vserver/setup.yaml | 15 + .../tests/nitro/target_lb_vserver/update.yaml | 14 + .../netscaler/test_netscaler_cs_action.py | 632 ++++++++++++++++++ 14 files changed, 1192 insertions(+) create mode 100644 lib/ansible/modules/network/netscaler/netscaler_cs_action.py create mode 100644 test/integration/roles/netscaler_cs_action/defaults/main.yaml create mode 100644 test/integration/roles/netscaler_cs_action/sample_inventory create mode 100644 test/integration/roles/netscaler_cs_action/tasks/main.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tasks/nitro.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tasks/testbed.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_expression.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/remove.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/setup.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/remove.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/setup.yaml create mode 100644 test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/update.yaml create mode 100644 test/units/modules/network/netscaler/test_netscaler_cs_action.py diff --git a/lib/ansible/modules/network/netscaler/netscaler_cs_action.py b/lib/ansible/modules/network/netscaler/netscaler_cs_action.py new file mode 100644 index 0000000000..f02ff4c239 --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_cs_action.py @@ -0,0 +1,302 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Citrix Systems +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'metadata_version': '1.0'} + + +DOCUMENTATION = ''' +--- +module: netscaler_cs_action +short_description: Manage content switching actions +description: + - Manage content switching actions + - This module is intended to run either on the ansible control node or a bastion (jumpserver) with access to the actual netscaler instance + +version_added: "2.4.0" + +author: George Nikolopoulos (@giorgos-nikolopoulos) + +options: + + name: + description: + - >- + Name for the content switching action. 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 sign C(@), equal sign C(=), and hyphen C(-) characters. Can be changed after the content + switching action is created. + + targetlbvserver: + description: + - "Name of the load balancing virtual server to which the content is switched." + + targetvserver: + description: + - "Name of the VPN virtual server to which the content is switched." + + targetvserverexpr: + description: + - "Information about this content switching action." + + comment: + description: + - "Comments associated with this cs action." + +extends_documentation_fragment: netscaler +requirements: + - nitro python sdk +''' + +EXAMPLES = ''' +# lb_vserver_1 must have been already created with the netscaler_lb_vserver module + +- name: Configure netscaler content switching action + delegate_to: localhost + netscaler_cs_action: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + validate_certs: no + + state: present + + name: action-1 + targetlbvserver: lb_vserver_1 +''' + +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' }" +''' + +from ansible.module_utils.basic import AnsibleModule +import json + +from ansible.module_utils.netscaler import ( + ConfigProxy, + get_nitro_client, + netscaler_common_arguments, + log, loglines, + ensure_feature_is_enabled, + get_immutables_intersection +) + +try: + from nssrc.com.citrix.netscaler.nitro.resource.config.cs.csaction import csaction + 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 action_exists(client, module): + if csaction.count_filtered(client, 'name:%s' % module.params['name']) > 0: + return True + else: + return False + + +def action_identical(client, module, csaction_proxy): + if len(diff_list(client, module, csaction_proxy)) == 0: + return True + else: + return False + + +def diff_list(client, module, csaction_proxy): + action_list = csaction.get_filtered(client, 'name:%s' % module.params['name']) + diff_list = csaction_proxy.diff_object(action_list[0]) + if False and 'targetvserverexpr' in diff_list: + json_value = json.loads(action_list[0].targetvserverexpr) + if json_value == module.params['targetvserverexpr']: + del diff_list['targetvserverexpr'] + return diff_list + + +def main(): + + module_specific_arguments = dict( + + name=dict(type='str'), + targetlbvserver=dict(type='str'), + targetvserverexpr=dict(type='str'), + comment=dict(type='str'), + ) + + argument_spec = dict() + + argument_spec.update(netscaler_common_arguments) + + argument_spec.update(module_specific_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 = [ + 'name', + 'targetlbvserver', + 'targetvserverexpr', + 'comment', + ] + readonly_attrs = [ + 'hits', + 'referencecount', + 'undefhits', + 'builtin', + ] + + immutable_attrs = [ + 'name', + 'targetvserverexpr', + ] + + transforms = { + } + + json_encodes = ['targetvserverexpr'] + + # Instantiate config proxy + csaction_proxy = ConfigProxy( + actual=csaction(), + client=client, + attribute_values_dict=module.params, + readwrite_attrs=readwrite_attrs, + readonly_attrs=readonly_attrs, + immutable_attrs=immutable_attrs, + transforms=transforms, + json_encodes=json_encodes, + ) + + try: + + ensure_feature_is_enabled(client, 'CS') + # Apply appropriate state + if module.params['state'] == 'present': + log('Applying actions for state present') + if not action_exists(client, module): + if not module.check_mode: + csaction_proxy.add() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + elif not action_identical(client, module, csaction_proxy): + + # Check if we try to change value of immutable attributes + immutables_changed = get_immutables_intersection(csaction_proxy, diff_list(client, module, csaction_proxy).keys()) + if immutables_changed != []: + module.fail_json( + msg='Cannot update immutable attributes %s' % (immutables_changed,), + diff=diff_list(client, module, csaction_proxy), + **module_result + ) + + if not module.check_mode: + csaction_proxy.update() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + else: + module_result['changed'] = False + + # Sanity check for state + log('Sanity checks for state present') + if not module.check_mode: + if not action_exists(client, module): + module.fail_json(msg='Content switching action does not exist', **module_result) + if not action_identical(client, module, csaction_proxy): + module.fail_json( + msg='Content switching action differs from configured', + diff=diff_list(client, module, csaction_proxy), + **module_result + ) + + elif module.params['state'] == 'absent': + log('Applying actions for state absent') + if action_exists(client, module): + if not module.check_mode: + csaction_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 action_exists(client, module): + module.fail_json(msg='Content switching action 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_cs_action/defaults/main.yaml b/test/integration/roles/netscaler_cs_action/defaults/main.yaml new file mode 100644 index 0000000000..641801f660 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +testcase: "*" +test_cases: [] + +nitro_user: nsroot +nitro_pass: nsroot diff --git a/test/integration/roles/netscaler_cs_action/sample_inventory b/test/integration/roles/netscaler_cs_action/sample_inventory new file mode 100644 index 0000000000..4263579691 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/sample_inventory @@ -0,0 +1,5 @@ + + +[netscaler] + +netscaler01 nsip=172.18.0.2 nitro_user=nsroot nitro_pass=nsroot diff --git a/test/integration/roles/netscaler_cs_action/tasks/main.yaml b/test/integration/roles/netscaler_cs_action/tasks/main.yaml new file mode 100644 index 0000000000..9a197e4d77 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tasks/main.yaml @@ -0,0 +1,6 @@ +--- +- { include: testbed.yaml, state: present } + +- { include: nitro.yaml, tags: ['nitro'] } + +- { include: testbed.yaml, state: absent } diff --git a/test/integration/roles/netscaler_cs_action/tasks/nitro.yaml b/test/integration/roles/netscaler_cs_action/tasks/nitro.yaml new file mode 100644 index 0000000000..00ab502dda --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/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_cs_action/tasks/testbed.yaml b/test/integration/roles/netscaler_cs_action/tasks/testbed.yaml new file mode 100644 index 0000000000..08214ce7f4 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tasks/testbed.yaml @@ -0,0 +1,16 @@ +--- + + +- name: Setup lb vserver + delegate_to: localhost + netscaler_lb_vserver: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: "{{ state }}" + name: lb-vserver-1 + + ipv46: 10.79.1.4 + port: 80 + servicetype: ANY diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression.yaml new file mode 100644 index 0000000000..9ae7fc99f2 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression.yaml @@ -0,0 +1,57 @@ +--- + +- include: "{{ role_path }}/tests/nitro/target_expression/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_expression/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/remove.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/remove.yaml new file mode 100644 index 0000000000..5a0f2072c2 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/remove.yaml @@ -0,0 +1,13 @@ +--- + +- name: Setup cs action + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_cs_action: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + name: action-2 diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/setup.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/setup.yaml new file mode 100644 index 0000000000..c42e0f9747 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_expression/setup.yaml @@ -0,0 +1,14 @@ +--- + + +- name: Setup cs action + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_cs_action: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + name: action-2 + targetvserverexpr: '"mylb_" + HTTP.REQ.URL.SUFFIX' diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver.yaml new file mode 100644 index 0000000000..60be84126f --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver.yaml @@ -0,0 +1,85 @@ +--- + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/update.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/target_lb_vserver/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/remove.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/remove.yaml new file mode 100644 index 0000000000..52cc42f753 --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/remove.yaml @@ -0,0 +1,13 @@ +--- + +- name: Setup cs action + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_cs_action: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + name: action-1 diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/setup.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/setup.yaml new file mode 100644 index 0000000000..3e2757805e --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/setup.yaml @@ -0,0 +1,15 @@ +--- + + +- name: Setup cs action + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_cs_action: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + name: action-1 + targetlbvserver: lb-vserver-1 + comment: some comment diff --git a/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/update.yaml b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/update.yaml new file mode 100644 index 0000000000..cf33e2e85d --- /dev/null +++ b/test/integration/roles/netscaler_cs_action/tests/nitro/target_lb_vserver/update.yaml @@ -0,0 +1,14 @@ +--- + +- name: Update cs action + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_cs_action: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + name: action-1 + targetlbvserver: lb-vserver-1 + comment: some other comment diff --git a/test/units/modules/network/netscaler/test_netscaler_cs_action.py b/test/units/modules/network/netscaler/test_netscaler_cs_action.py new file mode 100644 index 0000000000..4e7d5a3559 --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_cs_action.py @@ -0,0 +1,632 @@ + +# 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 TestNetscalerCSActionModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + + cls.MockException = MockException + + m = MagicMock() + cls.cs_action_mock = MagicMock() + cls.cs_action_mock.__class__ = MagicMock(add=Mock()) + nssrc_modules_mock = { + 'nssrc.com.citrix.netscaler.nitro.resource.config.cs': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.cs.csaction': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.cs.csaction': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.cs.csaction.csaction': cls.cs_action_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 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_cs_action + self.module = netscaler_cs_action + 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_cs_action + + 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_cs_action.get_nitro_client', m): + with patch('ansible.modules.network.netscaler.netscaler_cs_action.nitro_exception', MockException): + self.module = netscaler_cs_action + 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_cs_action + + 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_cs_action', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_cs_action + 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_cs_action + + 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_cs_action', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertTrue(result['msg'].startswith('SSL Error'), msg='SSL Error was not handled gracefully') + + 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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + cs_action_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + action_exists=Mock(side_effect=[False, True]), + ensure_feature_is_enabled=Mock(return_value=True), + diff_list=Mock(return_value={}), + ConfigProxy=Mock(return_value=cs_action_proxy_mock), + ): + self.module = netscaler_cs_action + 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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + cs_action_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + action_exists=Mock(side_effect=[True, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=Mock(return_value=cs_action_proxy_mock), + ): + self.module = netscaler_cs_action + 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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + cs_action_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + action_exists=Mock(side_effect=[False, True]), + diff_list=Mock(return_value={}), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=Mock(return_value=cs_action_proxy_mock), + ): + self.module = netscaler_cs_action + 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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + cs_action_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + action_exists=Mock(side_effect=[True, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=Mock(return_value=cs_action_proxy_mock), + ): + self.module = netscaler_cs_action + self.exited() + self.assertNotIn(call.save_config(), client_mock.mock_calls) + + def test_new_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + action_exists=Mock(side_effect=[False, True]), + action_identical=Mock(side_effect=[True]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + self.exited() + cs_action_proxy_mock.assert_has_calls([call.add()]) + + def test_modified_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[True, True]), + action_identical=Mock(side_effect=[False, True]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + self.exited() + cs_action_proxy_mock.assert_has_calls([call.update()]) + + def test_absent_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[True, False]), + action_identical=Mock(side_effect=[False, True]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + self.exited() + cs_action_proxy_mock.assert_has_calls([call.delete()]) + + def test_present_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[True, True]), + action_identical=Mock(side_effect=[True, True]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + self.exited() + cs_action_proxy_mock.assert_not_called() + + def test_absent_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[False, False]), + action_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + self.exited() + cs_action_proxy_mock.assert_not_called() + + def test_present_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[True, True]), + action_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertEqual(result['msg'], 'Content switching action differs from configured') + self.assertTrue(result['failed']) + + def test_present_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[False, False]), + action_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertEqual(result['msg'], 'Content switching action does not exist') + self.assertTrue(result['failed']) + + def test_present_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=['domain']), + action_exists=Mock(side_effect=[True, True]), + action_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertEqual(result['msg'], 'Cannot update immutable attributes [\'domain\']') + self.assertTrue(result['failed']) + + def test_absent_cs_action_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_cs_action + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + cs_action_proxy_mock = Mock() + cs_action_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=cs_action_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_cs_action', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + action_exists=Mock(side_effect=[True, True]), + action_identical=Mock(side_effect=[False, False]), + ensure_feature_is_enabled=Mock(return_value=True), + ConfigProxy=config_proxy_mock, + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertEqual(result['msg'], 'Content switching action 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_cs_action + + 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_cs_action', + action_exists=m, + ensure_feature_is_enabled=Mock(return_value=True), + nitro_exception=MockException + ): + self.module = netscaler_cs_action + 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_cs_action + + 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_cs_action', + action_exists=m, + ensure_feature_is_enabled=Mock(return_value=True), + nitro_exception=MockException + ): + self.module = netscaler_cs_action + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation absent' + )