diff --git a/lib/ansible/modules/network/netscaler/netscaler_server.py b/lib/ansible/modules/network/netscaler/netscaler_server.py new file mode 100644 index 00000000000..4af68f9b251 --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_server.py @@ -0,0 +1,386 @@ +#!/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_server +short_description: Manage server configuration +description: + - Manage server entities configuration. + - 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 server." + - >- + 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. + - "Can be changed after the name is created." + - "Minimum length = 1" + + ipaddress: + description: + - >- + IPv4 or IPv6 address of the server. If you create an IP address based server, you can specify the + name of the server, instead of its IP address, when creating a service. Note: If you do not create a + server entry, the server IP address that you enter when you create a service becomes the name of the + server. + + domain: + description: + - "Domain name of the server. For a domain based configuration, you must create the server first." + - "Minimum length = 1" + + translationip: + description: + - "IP address used to transform the server's DNS-resolved IP address." + + translationmask: + description: + - "The netmask of the translation ip." + + domainresolveretry: + description: + - >- + Time, in seconds, for which the NetScaler appliance must wait, after DNS resolution fails, before + sending the next DNS query to resolve the domain name. + - "Minimum value = C(5)" + - "Maximum value = C(20939)" + default: 5 + + ipv6address: + description: + - >- + Support IPv6 addressing mode. If you configure a server with the IPv6 addressing mode, you cannot use + the server in the IPv4 addressing mode. + default: false + type: bool + + comment: + description: + - "Any information about the server." + + 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 = C(0)" + - "Maximum value = C(4094)" + + disabled: + description: + - When set to C(true) the server state will be set to DISABLED. + - When set to C(false) the server state will be set to ENABLED. + - >- + Note that due to limitations of the underlying NITRO API a C(disabled) state change alone + does not cause the module result to report a changed status. + type: bool + default: false + +extends_documentation_fragment: netscaler +requirements: + - nitro python sdk +''' + +EXAMPLES = ''' +- name: Setup server + delegate_to: localhost + netscaler_server: + nsip: 172.18.0.2 + nitro_user: nsroot + nitro_pass: nsroot + + state: present + + name: server-1 + ipaddress: 192.168.1.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: str + 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: dict + sample: { 'targetlbvserver': 'difference. ours: (str) server1 other: (str) server2' } +''' + +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 + +try: + from nssrc.com.citrix.netscaler.nitro.resource.config.basic.server import server + 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 server_exists(client, module): + log('Checking if server exists') + if server.count_filtered(client, 'name:%s' % module.params['name']) > 0: + return True + else: + return False + + +def server_identical(client, module, server_proxy): + log('Checking if configured server is identical') + if server.count_filtered(client, 'name:%s' % module.params['name']) == 0: + return False + server_list = server.get_filtered(client, 'name:%s' % module.params['name']) + if server_proxy.has_equal_attributes(server_list[0]): + return True + else: + return False + + +def diff_list(client, module, server_proxy): + ret_val = server_proxy.diff_object(server.get_filtered(client, 'name:%s' % module.params['name'])[0]), + return ret_val[0] + + +def do_state_change(client, module, server_proxy): + if module.params['disabled']: + log('Disabling server') + result = server.disable(client, server_proxy.actual) + else: + log('Enabling server') + result = server.enable(client, server_proxy.actual) + return result + + +def main(): + + module_specific_arguments = dict( + name=dict(type='str'), + ipaddress=dict(type='str'), + domain=dict(type='str'), + translationip=dict(type='str'), + translationmask=dict(type='str'), + domainresolveretry=dict(type='int'), + ipv6address=dict( + type='bool', + default=False + ), + comment=dict(type='str'), + td=dict(type='float'), + ) + + hand_inserted_arguments = dict( + disabled=dict( + type='bool', + default=False, + ), + ) + + 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)) + + # Instantiate Server Config object + readwrite_attrs = [ + 'name', + 'ipaddress', + 'domain', + 'translationip', + 'translationmask', + 'domainresolveretry', + 'ipv6address', + 'comment', + 'td', + ] + + readonly_attrs = [ + 'statechangetimesec', + 'tickssincelaststatechange', + 'autoscale', + 'customserverid', + 'monthreshold', + 'maxclient', + 'maxreq', + 'maxbandwidth', + 'usip', + 'cka', + 'tcpb', + 'cmp', + 'clttimeout', + 'svrtimeout', + 'cipheader', + 'cip', + 'cacheable', + 'sc', + 'sp', + 'downstateflush', + 'appflowlog', + 'boundtd', + '__count', + ] + + immutable_attrs = [ + 'name', + 'domain', + 'ipv6address', + 'td', + ] + + transforms = { + 'ipv6address': ['bool_yes_no'], + } + + server_proxy = ConfigProxy( + actual=server(), + 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 server_exists(client, module): + if not module.check_mode: + server_proxy.add() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + elif not server_identical(client, module, server_proxy): + + # Check if we try to change value of immutable attributes + immutables_changed = get_immutables_intersection(server_proxy, diff_list(client, module, server_proxy).keys()) + if immutables_changed != []: + msg = 'Cannot update immutable attributes %s' % (immutables_changed,) + module.fail_json(msg=msg, diff=diff_list(client, module, server_proxy), **module_result) + if not module.check_mode: + server_proxy.update() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + else: + module_result['changed'] = False + + if not module.check_mode: + res = do_state_change(client, module, server_proxy) + if res.errorcode != 0: + msg = 'Error when setting disabled state. errorcode: %s message: %s' % (res.errorcode, res.message) + module.fail_json(msg=msg, **module_result) + + # Sanity check for result + log('Sanity checks for state present') + if not module.check_mode: + if not server_exists(client, module): + module.fail_json(msg='Server does not seem to exist', **module_result) + if not server_identical(client, module, server_proxy): + module.fail_json( + msg='Server is not configured according to parameters given', + diff=diff_list(client, module, server_proxy), + **module_result + ) + + elif module.params['state'] == 'absent': + log('Applying actions for state absent') + if server_exists(client, module): + if not module.check_mode: + server_proxy.delete() + if module.params['save_config']: + client.save_config() + module_result['changed'] = True + else: + module_result['changed'] = False + + # Sanity check for result + log('Sanity checks for state absent') + if not module.check_mode: + if server_exists(client, module): + module.fail_json(msg='Server seems to be present', **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/netscaler.yaml b/test/integration/netscaler.yaml index 262b0ad22a4..807c5ebccd5 100644 --- a/test/integration/netscaler.yaml +++ b/test/integration/netscaler.yaml @@ -1,3 +1,5 @@ +--- + - hosts: netscaler gather_facts: no @@ -9,3 +11,4 @@ roles: - { role: netscaler_service, when: "limit_to in ['*', 'netscaler_service']" } + - { role: netscaler_server, when: "limit_to in ['*', 'netscaler_server']" } diff --git a/test/integration/roles/netscaler_server/defaults/main.yaml b/test/integration/roles/netscaler_server/defaults/main.yaml new file mode 100644 index 00000000000..641801f6600 --- /dev/null +++ b/test/integration/roles/netscaler_server/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +testcase: "*" +test_cases: [] + +nitro_user: nsroot +nitro_pass: nsroot diff --git a/test/integration/roles/netscaler_server/sample_inventory b/test/integration/roles/netscaler_server/sample_inventory new file mode 100644 index 00000000000..42635796914 --- /dev/null +++ b/test/integration/roles/netscaler_server/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_server/tasks/main.yaml b/test/integration/roles/netscaler_server/tasks/main.yaml new file mode 100644 index 00000000000..729619a17c8 --- /dev/null +++ b/test/integration/roles/netscaler_server/tasks/main.yaml @@ -0,0 +1,2 @@ +--- +- { include: nitro.yaml, tags: ['nitro'] } diff --git a/test/integration/roles/netscaler_server/tasks/nitro.yaml b/test/integration/roles/netscaler_server/tasks/nitro.yaml new file mode 100644 index 00000000000..00ab502dda9 --- /dev/null +++ b/test/integration/roles/netscaler_server/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_server/tests/nitro/server.yaml b/test/integration/roles/netscaler_server/tests/nitro/server.yaml new file mode 100644 index 00000000000..4fe0ea4c83a --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server.yaml @@ -0,0 +1,82 @@ +--- + +- include: "{{ role_path }}/tests/nitro/server/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server/update.yaml" + vars: + check_mode: no + +- include: "{{ role_path }}/tests/nitro/server/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_server/tests/nitro/server/remove.yaml b/test/integration/roles/netscaler_server/tests/nitro/server/remove.yaml new file mode 100644 index 00000000000..5203bbf89f7 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server/remove.yaml @@ -0,0 +1,15 @@ +--- + +- name: Remove basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + + name: test-server-1 + ipaddress: 10.10.10.10 diff --git a/test/integration/roles/netscaler_server/tests/nitro/server/setup.yaml b/test/integration/roles/netscaler_server/tests/nitro/server/setup.yaml new file mode 100644 index 00000000000..2e3f09b7059 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server/setup.yaml @@ -0,0 +1,16 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-1 + ipaddress: 10.10.10.10 + comment: comment for server diff --git a/test/integration/roles/netscaler_server/tests/nitro/server/update.yaml b/test/integration/roles/netscaler_server/tests/nitro/server/update.yaml new file mode 100644 index 00000000000..543f77729d8 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server/update.yaml @@ -0,0 +1,16 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-1 + ipaddress: 11.11.11.11 + disabled: yes diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_domain.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_domain.yaml new file mode 100644 index 00000000000..7e5d3cb8313 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_domain.yaml @@ -0,0 +1,82 @@ +--- + +- include: "{{ role_path }}/tests/nitro/server_domain/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/update.yaml" + vars: + check_mode: no + +- include: "{{ role_path }}/tests/nitro/server_domain/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_domain/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_domain/remove.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_domain/remove.yaml new file mode 100644 index 00000000000..9c2b0f0a703 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_domain/remove.yaml @@ -0,0 +1,14 @@ +--- + +- name: Remove basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + + name: test-server-domain diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_domain/setup.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_domain/setup.yaml new file mode 100644 index 00000000000..94f1803b80e --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_domain/setup.yaml @@ -0,0 +1,16 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-domain + domain: example.com + ipv6address: no diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_domain/update.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_domain/update.yaml new file mode 100644 index 00000000000..821dd91b8f1 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_domain/update.yaml @@ -0,0 +1,18 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-domain + domain: example.com + translationip: 192.168.1.1 + translationmask: 255.255.255.0 + domainresolveretry: 10 diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_ipv6.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6.yaml new file mode 100644 index 00000000000..9a7b9521d2d --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6.yaml @@ -0,0 +1,82 @@ +--- + +- include: "{{ role_path }}/tests/nitro/server_ipv6/setup.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/setup.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/setup.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/setup.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/update.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/update.yaml" + vars: + check_mode: no + +- include: "{{ role_path }}/tests/nitro/server_ipv6/update.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/update.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/remove.yaml" + vars: + check_mode: yes + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/remove.yaml" + vars: + check_mode: no + +- assert: + that: result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/remove.yaml" + vars: + check_mode: yes + +- assert: + that: not result|changed + +- include: "{{ role_path }}/tests/nitro/server_ipv6/remove.yaml" + vars: + check_mode: no + +- assert: + that: not result|changed diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/remove.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/remove.yaml new file mode 100644 index 00000000000..a7d916283e3 --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/remove.yaml @@ -0,0 +1,14 @@ +--- + +- name: Remove basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: absent + + name: test-server-ipv6 diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/setup.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/setup.yaml new file mode 100644 index 00000000000..bf1e9e7283b --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/setup.yaml @@ -0,0 +1,15 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-ipv6 + ipaddress: ff::fa:0 diff --git a/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/update.yaml b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/update.yaml new file mode 100644 index 00000000000..8341e2b22dd --- /dev/null +++ b/test/integration/roles/netscaler_server/tests/nitro/server_ipv6/update.yaml @@ -0,0 +1,15 @@ +--- + +- name: Setup basic server + delegate_to: localhost + register: result + check_mode: "{{ check_mode }}" + netscaler_server: + nitro_user: "{{nitro_user}}" + nitro_pass: "{{nitro_pass}}" + nsip: "{{nsip}}" + + state: present + + name: test-server-ipv6 + ipaddress: ff::fb:0 diff --git a/test/units/modules/network/netscaler/test_netscaler_server.py b/test/units/modules/network/netscaler/test_netscaler_server.py new file mode 100644 index 00000000000..7bd026ec111 --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_server.py @@ -0,0 +1,654 @@ + +# 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 TestNetscalerServerModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + + cls.MockException = MockException + + m = MagicMock() + cls.server_mock = MagicMock() + cls.server_mock.__class__ = MagicMock(add=Mock()) + nssrc_modules_mock = { + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.server': m, + 'nssrc.com.citrix.netscaler.nitro.resource.config.basic.server.server': cls.server_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_server + self.module = netscaler_server + 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_server + + 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_server.get_nitro_client', m): + with patch('ansible.modules.network.netscaler.netscaler_server.nitro_exception', MockException): + self.module = netscaler_server + 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_server + + 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_server', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_server + 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_server + + 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_server', + get_nitro_client=m, + nitro_exception=MockException, + ): + self.module = netscaler_server + 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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + server_exists=Mock(side_effect=[False, True]), + ConfigProxy=Mock(return_value=server_proxy_mock), + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + 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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + server_exists=Mock(side_effect=[True, False]), + ConfigProxy=Mock(return_value=server_proxy_mock), + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + 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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + server_exists=Mock(side_effect=[False, True]), + ConfigProxy=Mock(return_value=server_proxy_mock), + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + 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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + server_exists=Mock(side_effect=[True, False]), + ConfigProxy=Mock(return_value=server_proxy_mock), + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + self.assertNotIn(call.save_config(), client_mock.mock_calls) + + def test_do_state_change_fail(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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_mock = Mock() + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + nitro_exception=self.MockException, + get_nitro_client=m, + server_exists=Mock(side_effect=[True, False]), + ConfigProxy=Mock(return_value=server_proxy_mock), + do_state_change=Mock(return_value=Mock(errorcode=1, message='Failed on purpose')) + ): + self.module = netscaler_server + result = self.failed() + self.assertEqual(result['msg'], 'Error when setting disabled state. errorcode: 1 message: Failed on purpose') + + def test_new_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + server_exists=Mock(side_effect=[False, True]), + server_identical=Mock(side_effect=[True]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + server_proxy_mock.assert_has_calls([call.add()]) + + def test_modified_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[True, True]), + server_identical=Mock(side_effect=[False, True]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + server_proxy_mock.assert_has_calls([call.update()]) + + def test_absent_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[True, False]), + server_identical=Mock(side_effect=[False, True]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + server_proxy_mock.assert_has_calls([call.delete()]) + + def test_present_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[True, True]), + server_identical=Mock(side_effect=[True, True]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + server_proxy_mock.assert_not_called() + + def test_absent_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[False, False]), + server_identical=Mock(side_effect=[False, False]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + self.exited() + server_proxy_mock.assert_not_called() + + def test_present_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[True, True]), + server_identical=Mock(side_effect=[False, False]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + result = self.failed() + self.assertEqual(result['msg'], 'Server is not configured according to parameters given') + self.assertTrue(result['failed']) + + def test_present_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[False, False]), + server_identical=Mock(side_effect=[False, False]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + result = self.failed() + self.assertEqual(result['msg'], 'Server does not seem to exist') + self.assertTrue(result['failed']) + + def test_present_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=['domain']), + server_exists=Mock(side_effect=[True, True]), + server_identical=Mock(side_effect=[False, False]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + result = self.failed() + self.assertEqual(result['msg'], 'Cannot update immutable attributes [\'domain\']') + self.assertTrue(result['failed']) + + def test_absent_server_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_server + + client_mock = Mock() + + m = Mock(return_value=client_mock) + + server_proxy_attrs = { + 'diff_object.return_value': {}, + } + server_proxy_mock = Mock() + server_proxy_mock.configure_mock(**server_proxy_attrs) + config_proxy_mock = Mock(return_value=server_proxy_mock) + + with patch.multiple( + 'ansible.modules.network.netscaler.netscaler_server', + nitro_exception=self.MockException, + get_nitro_client=m, + diff_list=Mock(return_value={}), + get_immutables_intersection=Mock(return_value=[]), + server_exists=Mock(side_effect=[True, True]), + server_identical=Mock(side_effect=[False, False]), + ConfigProxy=config_proxy_mock, + do_state_change=Mock(return_value=Mock(errorcode=0)) + ): + self.module = netscaler_server + result = self.failed() + self.assertEqual(result['msg'], 'Server seems to be present') + 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_server + + 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_server', + server_exists=m, + nitro_exception=MockException + ): + self.module = netscaler_server + 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_server + + 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_server', + server_exists=m, + nitro_exception=MockException + ): + self.module = netscaler_server + result = self.failed() + self.assertTrue( + result['msg'].startswith('nitro exception'), + msg='Nitro exception not caught on operation absent' + )