diff --git a/lib/ansible/modules/network/netscaler/netscaler_save_config.py b/lib/ansible/modules/network/netscaler/netscaler_save_config.py new file mode 100644 index 00000000000..128afc67e0b --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_save_config.py @@ -0,0 +1,188 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Citrix Systems +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: netscaler_save_config +short_description: Save Netscaler configuration. +description: + - This module uncoditionally saves the configuration on the target netscaler node. + - This module does not support check mode. + - 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: + 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. C(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 + +requirements: + - nitro python sdk +''' + +EXAMPLES = ''' +--- +- name: Save netscaler configuration + delegate_to: localhost + netscaler_save_config: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + +- name: Setup server without saving configuration + delegate_to: localhost + notify: Save configuration + netscaler_server: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + + save_config: no + + name: server-1 + ipaddress: 192.168.1.1 + +# Under playbook's handlers + +- name: Save configuration + delegate_to: localhost + netscaler_save_config: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot +''' + +RETURN = ''' +loglines: + description: list of logged messages by the module + returned: always + type: list + sample: ['message 1', 'message 2'] + +msg: + description: Message detailing the failure reason + returned: failure + type: str + sample: "Action does not exist" + +''' + +import copy + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.netscaler import get_nitro_client, log, loglines, netscaler_common_arguments + +try: + 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 main(): + + argument_spec = copy.deepcopy(netscaler_common_arguments) + + # Delete common arguments irrelevant to this module + del argument_spec['state'] + del argument_spec['save_config'] + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False, + ) + + 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)) + + try: + log('Saving configuration') + client.save_config() + except nitro_exception as e: + msg = "nitro exception errorcode=" + str(e.errorcode) + ",message=" + e.message + module.fail_json(msg=msg, **module_result) + + client.logout() + module.exit_json(**module_result) + + +if __name__ == "__main__": + main() diff --git a/test/integration/roles/netscaler_save_config/defaults/main.yaml b/test/integration/roles/netscaler_save_config/defaults/main.yaml new file mode 100644 index 00000000000..641801f6600 --- /dev/null +++ b/test/integration/roles/netscaler_save_config/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +testcase: "*" +test_cases: [] + +nitro_user: nsroot +nitro_pass: nsroot diff --git a/test/integration/roles/netscaler_save_config/sample_inventory b/test/integration/roles/netscaler_save_config/sample_inventory new file mode 100644 index 00000000000..42635796914 --- /dev/null +++ b/test/integration/roles/netscaler_save_config/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_save_config/tasks/main.yaml b/test/integration/roles/netscaler_save_config/tasks/main.yaml new file mode 100644 index 00000000000..729619a17c8 --- /dev/null +++ b/test/integration/roles/netscaler_save_config/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: nitro.yaml, tags: ['nitro'] } diff --git a/test/integration/roles/netscaler_save_config/tasks/nitro.yaml b/test/integration/roles/netscaler_save_config/tasks/nitro.yaml new file mode 100644 index 00000000000..00ab502dda9 --- /dev/null +++ b/test/integration/roles/netscaler_save_config/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_save_config/tests/nitro/save_config.yaml b/test/integration/roles/netscaler_save_config/tests/nitro/save_config.yaml new file mode 100644 index 00000000000..dadce8de83e --- /dev/null +++ b/test/integration/roles/netscaler_save_config/tests/nitro/save_config.yaml @@ -0,0 +1,8 @@ +--- + +- name: Save configuration + delegate_to: localhost + netscaler_save_config: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" diff --git a/test/units/modules/network/netscaler/test_netscaler_save_config.py b/test/units/modules/network/netscaler/test_netscaler_save_config.py new file mode 100644 index 00000000000..98bed1a7eb0 --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_save_config.py @@ -0,0 +1,145 @@ + +# 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 TestNetscalerSaveConfigModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + + cls.MockException = MockException + + cls.nitro_base_patcher = nitro_base_patcher + + @classmethod + def tearDownClass(cls): + cls.nitro_base_patcher.stop() + + def setUp(self): + self.nitro_base_patcher.start() + + def tearDown(self): + self.nitro_base_patcher.stop() + + def test_graceful_nitro_error_on_login(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + )) + from ansible.modules.network.netscaler import netscaler_save_config + + 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_save_config.get_nitro_client', m): + with patch('ansible.modules.network.netscaler.netscaler_save_config.nitro_exception', MockException): + self.module = netscaler_save_config + 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', + )) + from ansible.modules.network.netscaler import netscaler_save_config + + 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_save_config', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_save_config + 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', + )) + from ansible.modules.network.netscaler import netscaler_save_config + + 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_save_config', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_save_config + result = self.failed() + self.assertTrue(result['msg'].startswith('SSL Error'), msg='SSL Error was not handled gracefully') + + def test_save_config_called(self): + set_module_args(dict( + nitro_user='user', + nitro_pass='pass', + nsip='1.1.1.1', + )) + + class MockException(Exception): + pass + + from ansible.modules.network.netscaler import netscaler_save_config + client_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_save_config', + get_nitro_client=Mock(return_value=client_mock), + nitro_exception=MockException, + ): + self.module = netscaler_save_config + self.exited() + call_sequence = [call.login(), call.save_config(), call.logout()] + client_mock.assert_has_calls(call_sequence)