From 34f965adddec6782f1d30415158e05bc94ca5554 Mon Sep 17 00:00:00 2001 From: George Nikolopoulos Date: Tue, 28 Nov 2017 16:16:01 +0200 Subject: [PATCH] New module: Issue NITRO requests to a Netscaler instance (network/netscaler/netscaler_nitro_request) (#33091) * Add netscaler_nitro_request module * Make changes as requested. * Fix whitepsace in EXAMPLES block. * Set module changed status according to operation performed. --- .../netscaler/netscaler_nitro_request.py | 909 ++++++++++++++++++ .../netscaler/test_netscaler_nitro_request.py | 347 +++++++ 2 files changed, 1256 insertions(+) create mode 100644 lib/ansible/modules/network/netscaler/netscaler_nitro_request.py create mode 100644 test/units/modules/network/netscaler/test_netscaler_nitro_request.py diff --git a/lib/ansible/modules/network/netscaler/netscaler_nitro_request.py b/lib/ansible/modules/network/netscaler/netscaler_nitro_request.py new file mode 100644 index 00000000000..eab95dcfa0c --- /dev/null +++ b/lib/ansible/modules/network/netscaler/netscaler_nitro_request.py @@ -0,0 +1,909 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2017 Citrix Systems +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: netscaler_nitro_request +short_description: Issue Nitro API requests to a Netscaler instance. +description: + - Issue Nitro API requests to a Netscaler instance. + - This is intended to be a short hand for using the uri Ansible module to issue the raw HTTP requests directly. + - It provides consistent return values and has no other dependencies apart from the base Ansible runtime environment. + - 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.5.0" + +author: George Nikolopoulos (@giorgos-nikolopoulos) + +options: + + nsip: + description: + - The IP address of the Netscaler or MAS instance where the Nitro API calls will be made. + - "The port can be specified with the colon C(:). E.g. C(192.168.1.1:555)." + + 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. + default: 'yes' + + nitro_auth_token: + description: + - The authentication token provided by the C(mas_login) operation. It is required when issuing Nitro API calls through a MAS proxy. + + resource: + description: + - The type of resource we are operating on. + - It is required for all I(operation) values except C(mas_login) and C(save_config). + + name: + description: + - The name of the resource we are operating on. + - "It is required for the following I(operation) values: C(update), C(get), C(delete)." + + attributes: + description: + - The attributes of the Nitro object we are operating on. + - "It is required for the following I(operation) values: C(add), C(update), C(action)." + + args: + description: + - A dictionary which defines the key arguments by which we will select the Nitro object to operate on. + - "It is required for the following I(operation) values: C(get_by_args), C('delete_by_args')." + + filter: + description: + - A dictionary which defines the filter with which to refine the Nitro objects returned by the C(get_filtered) I(operation). + + operation: + description: + - Define the Nitro operation that we want to perform. + choices: + - add + - update + - get + - get_by_args + - get_filtered + - get_all + - delete + - delete_by_args + - count + - mas_login + - save_config + - action + + expected_nitro_errorcode: + description: + - A list of numeric values that signify that the operation was successful. + default: [0] + required: true + + action: + description: + - The action to perform when the I(operation) value is set to C(action). + - Some common values for this parameter are C(enable), C(disable), C(rename). + + instance_ip: + description: + - The IP address of the target Netscaler instance when issuing a Nitro request through a MAS proxy. + + instance_name: + description: + - The name of the target Netscaler instance when issuing a Nitro request through a MAS proxy. + + instance_id: + description: + - The id of the target Netscaler instance when issuing a Nitro request through a MAS proxy. +''' + +EXAMPLES = ''' +- name: Add a server + delegate_to: localhost + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: add + resource: server + name: test-server-1 + attributes: + name: test-server-1 + ipaddress: 192.168.1.1 + +- name: Update server + delegate_to: localhost + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: update + resource: server + name: test-server-1 + attributes: + name: test-server-1 + ipaddress: 192.168.1.2 + +- name: Get server + delegate_to: localhost + register: result + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: get + resource: server + name: test-server-1 + +- name: Delete server + delegate_to: localhost + register: result + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: delete + resource: server + name: test-server-1 + +- name: Rename server + delegate_to: localhost + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: action + action: rename + resource: server + attributes: + name: test-server-1 + newname: test-server-2 + +- name: Get server by args + delegate_to: localhost + register: result + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: get_by_args + resource: server + args: + name: test-server-1 + +- name: Get server by filter + delegate_to: localhost + register: result + netscaler_nitro_request: + nsip: "{{ nsip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: get_filtered + resource: server + filter: + ipaddress: 192.168.1.2 + +# Doing a NITRO request through MAS. +# Requires to have an authentication token from the mas_login and used as the nitro_auth_token parameter +# Also nsip is the MAS address and the target Netscaler IP must be defined with instance_ip +# The rest of the task arguments remain the same as when issuing the NITRO request directly to a Netscaler instance. + +- name: Do mas login + delegate_to: localhost + register: login_result + netscaler_nitro_request: + nsip: "{{ mas_ip }}" + nitro_user: "{{ nitro_user }}" + nitro_pass: "{{ nitro_pass }}" + operation: mas_login + +- name: Add resource through MAS proxy + delegate_to: localhost + netscaler_nitro_request: + nsip: "{{ mas_ip }}" + nitro_auth_token: "{{ login_result.nitro_auth_token }}" + instance_ip: "{{ nsip }}" + operation: add + resource: server + name: test-server-1 + attributes: + name: test-server-1 + ipaddress: 192.168.1.7 +''' + +RETURN = ''' +nitro_errorcode: + description: A numeric value containing the return code of the NITRO operation. When 0 the operation is succesful. Any non zero value indicates an error. + returned: always + type: int + sample: 0 + +nitro_message: + description: A string containing a human readable explanation for the NITRO operation result. + returned: always + type: string + sample: Success + +nitro_severity: + description: A string describing the severity of the NITRO operation error or NONE. + returned: always + type: string + sample: NONE + +http_response_data: + description: A dictionary that contains all the HTTP response's data. + returned: always + type: dict + sample: "status: 200" + +http_response_body: + description: A string with the actual HTTP response body content if existent. If there is no HTTP response body it is an empty string. + returned: always + type: string + sample: "{ errorcode: 0, message: Done, severity: NONE }" + +nitro_object: + description: The object returned from the NITRO operation. This is applicable to the various get operations which return an object. + returned: when applicable + type: list + sample: + - + ipaddress: "192.168.1.8" + ipv6address: "NO" + maxbandwidth: "0" + name: "test-server-1" + port: 0 + sp: "OFF" + state: "ENABLED" + +nitro_auth_token: + description: The token returned by the C(mas_login) operation when succesful. + returned: when applicable + type: string + sample: "##E8D7D74DDBD907EE579E8BB8FF4529655F22227C1C82A34BFC93C9539D66" +''' + + +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.basic import env_fallback +from ansible.module_utils.basic import AnsibleModule +import codecs + + +class NitroAPICaller(object): + + _argument_spec = dict( + nsip=dict( + fallback=(env_fallback, ['NETSCALER_NSIP']), + ), + nitro_user=dict( + fallback=(env_fallback, ['NETSCALER_NITRO_USER']), + ), + nitro_pass=dict( + fallback=(env_fallback, ['NETSCALER_NITRO_PASS']), + no_log=True + ), + nitro_protocol=dict( + choices=['http', 'https'], + fallback=(env_fallback, ['NETSCALER_NITRO_PROTOCOL']), + default='http' + ), + validate_certs=dict( + default=True, + type='bool' + ), + nitro_auth_token=dict( + type='str', + no_log=True + ), + resource=dict(type='str'), + name=dict(type='str'), + attributes=dict(type='dict'), + + args=dict(type='dict'), + filter=dict(type='dict'), + + operation=dict( + type='str', + required=True, + choices=[ + 'add', + 'update', + 'get', + 'get_by_args', + 'get_filtered', + 'get_all', + 'delete', + 'delete_by_args', + 'count', + + 'mas_login', + + # Actions + 'save_config', + + # Generic action handler + 'action', + ] + ), + expected_nitro_errorcode=dict( + type='list', + default=[0], + ), + action=dict(type='str'), + instance_ip=dict(type='str'), + instance_name=dict(type='str'), + instance_id=dict(type='str'), + ) + + def __init__(self): + + self._module = AnsibleModule( + argument_spec=self._argument_spec, + supports_check_mode=False, + ) + + self._module_result = dict( + failed=False, + ) + + # Prepare the http headers according to module arguments + self._headers = {} + self._headers['Content-Type'] = 'application/json' + + # Check for conflicting authentication methods + have_token = self._module.params['nitro_auth_token'] is not None + have_userpass = None not in (self._module.params['nitro_user'], self._module.params['nitro_pass']) + login_operation = self._module.params['operation'] == 'mas_login' + + if have_token and have_userpass: + self.fail_module(msg='Cannot define both authentication token and username/password') + + if have_token: + self._headers['Cookie'] = "NITRO_AUTH_TOKEN=%s" % self._module.params['nitro_auth_token'] + + if have_userpass and not login_operation: + self._headers['X-NITRO-USER'] = self._module.params['nitro_user'] + self._headers['X-NITRO-PASS'] = self._module.params['nitro_pass'] + + # Do header manipulation when doing a MAS proxy call + if self._module.params['instance_ip'] is not None: + self._headers['_MPS_API_PROXY_MANAGED_INSTANCE_IP'] = self._module.params['instance_ip'] + elif self._module.params['instance_name'] is not None: + self._headers['_MPS_API_PROXY_MANAGED_INSTANCE_NAME'] = self._module.params['instance_name'] + elif self._module.params['instance_id'] is not None: + self._headers['_MPS_API_PROXY_MANAGED_INSTANCE_ID'] = self._module.params['instance_id'] + + def edit_response_data(self, r, info, result, success_status): + # Search for body in both http body and http data + if r is not None: + result['http_response_body'] = codecs.decode(r.read(), 'utf-8') + elif 'body' in info: + result['http_response_body'] = codecs.decode(info['body'], 'utf-8') + del info['body'] + else: + result['http_response_body'] = '' + + result['http_response_data'] = info + + # Update the nitro_* parameters according to expected success_status + # Use explicit return values from http response or deduce from http status code + + # Nitro return code in http data + result['nitro_errorcode'] = None + result['nitro_message'] = None + result['nitro_severity'] = None + + if result['http_response_body'] != '': + try: + data = self._module.from_json(result['http_response_body']) + except ValueError: + data = {} + result['nitro_errorcode'] = data.get('errorcode') + result['nitro_message'] = data.get('message') + result['nitro_severity'] = data.get('severity') + + # If we do not have the nitro errorcode from body deduce it from the http status + if result['nitro_errorcode'] is None: + # HTTP status failed + if result['http_response_data'].get('status') != success_status: + result['nitro_errorcode'] = -1 + result['nitro_message'] = result['http_response_data'].get('msg', 'HTTP status %s' % result['http_response_data']['status']) + result['nitro_severity'] = 'ERROR' + # HTTP status succeeded + else: + result['nitro_errorcode'] = 0 + result['nitro_message'] = 'Success' + result['nitro_severity'] = 'NONE' + + def handle_get_return_object(self, result): + result['nitro_object'] = [] + if result['nitro_errorcode'] == 0: + if result['http_response_body'] != '': + data = self._module.from_json(result['http_response_body']) + if self._module.params['resource'] in data: + result['nitro_object'] = data[self._module.params['resource']] + else: + del result['nitro_object'] + + def fail_module(self, msg, **kwargs): + self._module_result['failed'] = True + self._module_result['changed'] = False + self._module_result.update(kwargs) + self._module_result['msg'] = msg + self._module.fail_json(**self._module_result) + + def main(self): + if self._module.params['operation'] == 'add': + result = self.add() + + if self._module.params['operation'] == 'update': + result = self.update() + + if self._module.params['operation'] == 'delete': + result = self.delete() + + if self._module.params['operation'] == 'delete_by_args': + result = self.delete_by_args() + + if self._module.params['operation'] == 'get': + result = self.get() + + if self._module.params['operation'] == 'get_by_args': + result = self.get_by_args() + + if self._module.params['operation'] == 'get_filtered': + result = self.get_filtered() + + if self._module.params['operation'] == 'get_all': + result = self.get_all() + + if self._module.params['operation'] == 'count': + result = self.count() + + if self._module.params['operation'] == 'mas_login': + result = self.mas_login() + + if self._module.params['operation'] == 'action': + result = self.action() + + if self._module.params['operation'] == 'save_config': + result = self.save_config() + + if result['nitro_errorcode'] not in self._module.params['expected_nitro_errorcode']: + self.fail_module(msg='NITRO Failure', **result) + + self._module_result.update(result) + self._module.exit_json(**self._module_result) + + def exit_module(self): + self._module.exit_json() + + def add(self): + # Check if required attributes are present + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + if self._module.params['attributes'] is None: + self.fail_module(msg='NITRO resource attributes are undefined.') + + url = '%s://%s/nitro/v1/config/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + ) + + data = self._module.jsonify({self._module.params['resource']: self._module.params['attributes']}) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + data=data, + method='POST', + ) + + result = {} + + self.edit_response_data(r, info, result, success_status=201) + + if result['nitro_errorcode'] == 0: + self._module_result['changed'] = True + else: + self._module_result['changed'] = False + + return result + + def update(self): + # Check if required attributes are arguments present + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + if self._module.params['name'] is None: + self.fail_module(msg='NITRO resource name is undefined.') + + if self._module.params['attributes'] is None: + self.fail_module(msg='NITRO resource attributes are undefined.') + + url = '%s://%s/nitro/v1/config/%s/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + self._module.params['name'], + ) + + data = self._module.jsonify({self._module.params['resource']: self._module.params['attributes']}) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + data=data, + method='PUT', + ) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + + if result['nitro_errorcode'] == 0: + self._module_result['changed'] = True + else: + self._module_result['changed'] = False + + return result + + def get(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + if self._module.params['name'] is None: + self.fail_module(msg='NITRO resource name is undefined.') + + url = '%s://%s/nitro/v1/config/%s/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + self._module.params['name'], + ) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='GET', + ) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + + self.handle_get_return_object(result) + self._module_result['changed'] = False + + return result + + def get_by_args(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + if self._module.params['args'] is None: + self.fail_module(msg='NITRO args is undefined.') + + url = '%s://%s/nitro/v1/config/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + ) + + args_dict = self._module.params['args'] + args = ','.join(['%s:%s' % (k, args_dict[k]) for k in args_dict]) + + args = 'args=' + args + + url = '?'.join([url, args]) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='GET', + ) + result = {} + self.edit_response_data(r, info, result, success_status=200) + + self.handle_get_return_object(result) + self._module_result['changed'] = False + + return result + + def get_filtered(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + if self._module.params['filter'] is None: + self.fail_module(msg='NITRO filter is undefined.') + + keys = list(self._module.params['filter'].keys()) + filter_key = keys[0] + filter_value = self._module.params['filter'][filter_key] + filter_str = '%s:%s' % (filter_key, filter_value) + + url = '%s://%s/nitro/v1/config/%s?filter=%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + filter_str, + ) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='GET', + ) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + self.handle_get_return_object(result) + self._module_result['changed'] = False + + return result + + def get_all(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + url = '%s://%s/nitro/v1/config/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + ) + + print('headers %s' % self._headers) + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='GET', + ) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + self.handle_get_return_object(result) + self._module_result['changed'] = False + + return result + + def delete(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + if self._module.params['name'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + # Deletion by name takes precedence over deletion by attributes + + url = '%s://%s/nitro/v1/config/%s/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + self._module.params['name'], + ) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='DELETE', + ) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + + if result['nitro_errorcode'] == 0: + self._module_result['changed'] = True + else: + self._module_result['changed'] = False + + return result + + def delete_by_args(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + if self._module.params['args'] is None: + self.fail_module(msg='NITRO args is undefined.') + + url = '%s://%s/nitro/v1/config/%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + ) + + args_dict = self._module.params['args'] + args = ','.join(['%s:%s' % (k, args_dict[k]) for k in args_dict]) + + args = 'args=' + args + + url = '?'.join([url, args]) + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='DELETE', + ) + result = {} + self.edit_response_data(r, info, result, success_status=200) + + if result['nitro_errorcode'] == 0: + self._module_result['changed'] = True + else: + self._module_result['changed'] = False + + return result + + def count(self): + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + + url = '%s://%s/nitro/v1/config/%s?count=yes' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + ) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + method='GET', + ) + + result = {} + self.edit_response_data(r, info, result) + + if result['http_response_body'] != '': + data = self._module.from_json(result['http_response_body']) + + result['nitro_errorcode'] = data['errorcode'] + result['nitro_message'] = data['message'] + result['nitro_severity'] = data['severity'] + if self._module.params['resource'] in data: + result['nitro_count'] = data[self._module.params['resource']][0]['__count'] + + self._module_result['changed'] = False + + return result + + def action(self): + # Check if required attributes are present + if self._module.params['resource'] is None: + self.fail_module(msg='NITRO resource is undefined.') + if self._module.params['attributes'] is None: + self.fail_module(msg='NITRO resource attributes are undefined.') + if self._module.params['action'] is None: + self.fail_module(msg='NITRO action is undefined.') + + url = '%s://%s/nitro/v1/config/%s?action=%s' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + self._module.params['resource'], + self._module.params['action'], + ) + + data = self._module.jsonify({self._module.params['resource']: self._module.params['attributes']}) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + data=data, + method='POST', + ) + + result = {} + + self.edit_response_data(r, info, result, success_status=200) + + if result['nitro_errorcode'] == 0: + self._module_result['changed'] = True + else: + self._module_result['changed'] = False + + return result + + def mas_login(self): + url = '%s://%s/nitro/v1/config/login' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + ) + + login_credentials = { + 'login': { + + 'username': self._module.params['nitro_user'], + 'password': self._module.params['nitro_pass'], + } + } + + data = 'object=\n%s' % self._module.jsonify(login_credentials) + + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + data=data, + method='POST', + ) + print(r, info) + + result = {} + self.edit_response_data(r, info, result, success_status=200) + + if result['nitro_errorcode'] == 0: + body_data = self._module.from_json(result['http_response_body']) + result['nitro_auth_token'] = body_data['login'][0]['sessionid'] + + self._module_result['changed'] = False + + return result + + def save_config(self): + + url = '%s://%s/nitro/v1/config/nsconfig?action=save' % ( + self._module.params['nitro_protocol'], + self._module.params['nsip'], + ) + + data = self._module.jsonify( + { + 'nsconfig': {}, + } + ) + r, info = fetch_url( + self._module, + url=url, + headers=self._headers, + data=data, + method='POST', + ) + + result = {} + + self.edit_response_data(r, info, result, success_status=200) + self._module_result['changed'] = False + + return result + + +def main(): + + nitro_api_caller = NitroAPICaller() + nitro_api_caller.main() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/netscaler/test_netscaler_nitro_request.py b/test/units/modules/network/netscaler/test_netscaler_nitro_request.py new file mode 100644 index 00000000000..6506ccbcc65 --- /dev/null +++ b/test/units/modules/network/netscaler/test_netscaler_nitro_request.py @@ -0,0 +1,347 @@ + +# 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, call +from .netscaler_module import TestModule +import copy +import tempfile +import json +import sys +import codecs + +from ansible.modules.network.netscaler import netscaler_nitro_request + +module_arguments = dict( + nsip=None, + nitro_user=None, + nitro_pass=None, + nitro_protocol=None, + validate_certs=None, + nitro_auth_token=None, + resource=None, + name=None, + attributes=None, + args=None, + filter=None, + operation=None, + expected_nitro_errorcode=None, + action=None, + instance_ip=None, + instance_name=None, + instance_id=None, +) + + +class TestNetscalerNitroRequestModule(TestModule): + + @classmethod + def setUpClass(cls): + class MockException(Exception): + pass + + cls.MockException = MockException + + @classmethod + def tearDownClass(cls): + pass + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_fail_on_conflicting_authentication_methods(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + nitro_auth_token='##DDASKLFDJ', + )) + mock_module_instance = Mock(params=args) + expected_calls = [ + call.fail_json( + changed=False, + failed=True, + msg='Cannot define both authentication token and username/password' + ) + ] + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + netscaler_nitro_request.NitroAPICaller() + mock_module_instance.assert_has_calls(expected_calls) + + def test_nitro_user_pass_credentials(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + )) + mock_module_instance = Mock(params=args) + expected_headers = { + 'Content-Type': 'application/json', + 'X-NITRO-USER': 'nsroot', + 'X-NITRO-PASS': 'nsroot', + } + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + instance = netscaler_nitro_request.NitroAPICaller() + self.assertDictEqual(instance._headers, expected_headers) + + def test_mas_login_headers(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + operation='mas_login', + )) + mock_module_instance = Mock(params=args) + expected_headers = { + 'Content-Type': 'application/json', + } + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + instance = netscaler_nitro_request.NitroAPICaller() + self.assertDictEqual(instance._headers, expected_headers) + + def test_mas_proxy_call_headers_instance_ip(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_auth_token='##ABDB', + operation='add', + instance_ip='192.168.1.1', + )) + mock_module_instance = Mock(params=args) + expected_headers = { + 'Content-Type': 'application/json', + '_MPS_API_PROXY_MANAGED_INSTANCE_IP': args['instance_ip'], + 'Cookie': 'NITRO_AUTH_TOKEN=%s' % args['nitro_auth_token'], + } + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + instance = netscaler_nitro_request.NitroAPICaller() + self.assertDictEqual(instance._headers, expected_headers) + + def test_mas_proxy_call_headers_instance_id(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_auth_token='##ABDB', + operation='add', + instance_id='myid', + )) + mock_module_instance = Mock(params=args) + expected_headers = { + 'Content-Type': 'application/json', + '_MPS_API_PROXY_MANAGED_INSTANCE_ID': args['instance_id'], + 'Cookie': 'NITRO_AUTH_TOKEN=%s' % args['nitro_auth_token'], + } + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + instance = netscaler_nitro_request.NitroAPICaller() + self.assertDictEqual(instance._headers, expected_headers) + + def test_mas_proxy_call_headers_instance_name(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_auth_token='##ABDB', + operation='add', + instance_name='myname', + )) + mock_module_instance = Mock(params=args) + expected_headers = { + 'Content-Type': 'application/json', + '_MPS_API_PROXY_MANAGED_INSTANCE_NAME': args['instance_name'], + 'Cookie': 'NITRO_AUTH_TOKEN=%s' % args['nitro_auth_token'], + } + module_mock = Mock(return_value=mock_module_instance) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', module_mock): + instance = netscaler_nitro_request.NitroAPICaller() + self.assertDictEqual(instance._headers, expected_headers) + + def test_edit_response_data_no_body_success_status(self): + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule'): + instance = netscaler_nitro_request.NitroAPICaller() + r = None + info = { + 'status': 200, + } + result = {} + success_status = 200 + + expected_result = { + 'nitro_errorcode': 0, + 'nitro_message': 'Success', + 'nitro_severity': 'NONE', + 'http_response_body': '', + 'http_response_data': info, + } + instance.edit_response_data(r, info, result, success_status) + self.assertDictEqual(result, expected_result) + + def test_edit_response_data_no_body_fail_status(self): + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule'): + instance = netscaler_nitro_request.NitroAPICaller() + r = None + info = { + 'status': 201, + } + result = {} + success_status = 200 + + expected_result = { + 'nitro_errorcode': -1, + 'nitro_message': 'HTTP status %s' % info['status'], + 'nitro_severity': 'ERROR', + 'http_response_body': '', + 'http_response_data': info, + } + instance.edit_response_data(r, info, result, success_status) + self.assertDictEqual(result, expected_result) + + def test_edit_response_data_actual_body_data(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + nitro_auth_token='##DDASKLFDJ', + )) + module_mock = Mock(params=args, from_json=json.loads) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', Mock(return_value=module_mock)): + with tempfile.TemporaryFile() as r: + actual_body = { + 'errorcode': 258, + 'message': 'Some error', + 'severity': 'ERROR', + } + r.write(codecs.encode(json.dumps(actual_body), 'utf-8')) + r.seek(0) + + instance = netscaler_nitro_request.NitroAPICaller() + info = { + 'status': 200, + } + result = {} + success_status = 200 + + expected_result = { + 'http_response_body': json.dumps(actual_body), + 'http_response_data': info, + } + nitro_data = {} + for key, value in actual_body.items(): + nitro_data['nitro_%s' % key] = value + expected_result.update(nitro_data) + + instance.edit_response_data(r, info, result, success_status) + self.assertDictEqual(result, expected_result) + + def test_edit_response_data_actual_body_data_irrelevant(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + nitro_auth_token='##DDASKLFDJ', + )) + module_mock = Mock(params=args, from_json=json.loads) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', Mock(return_value=module_mock)): + with tempfile.TemporaryFile() as r: + actual_body = {} + r.write(codecs.encode(json.dumps(actual_body), 'utf-8')) + r.seek(0) + + instance = netscaler_nitro_request.NitroAPICaller() + info = { + 'status': 200, + } + result = {} + success_status = 200 + + expected_result = { + 'http_response_body': json.dumps(actual_body), + 'http_response_data': info, + 'nitro_errorcode': 0, + 'nitro_message': 'Success', + 'nitro_severity': 'NONE', + } + + instance.edit_response_data(r, info, result, success_status) + self.assertDictEqual(result, expected_result) + + def test_edit_response_data_body_in_info(self): + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + )) + module_mock = Mock(params=args, from_json=json.loads) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', Mock(return_value=module_mock)): + body = { + 'errorcode': 258, + 'message': 'Numerical error 258', + 'severity': 'ERROR' + } + instance = netscaler_nitro_request.NitroAPICaller() + r = None + info = { + 'status': 200, + 'body': codecs.encode(json.dumps(body), 'utf-8'), + } + result = {} + success_status = 200 + + expected_result = { + 'http_response_body': json.dumps(body), + 'http_response_data': info, + } + + nitro_data = {} + for key, value in body.items(): + nitro_data['nitro_%s' % key] = value + + expected_result.update(nitro_data) + instance.edit_response_data(r, info, result, success_status) + self.assertDictEqual(result, expected_result) + + def test_handle_get_return_object(self): + resource = 'lbvserver' + args = copy.deepcopy(module_arguments) + args.update(dict( + nitro_user='nsroot', + nitro_pass='nsroot', + resource=resource, + )) + resource_data = { + 'property1': 'value1', + 'property2': 'value2', + } + module_mock = Mock(params=args, from_json=json.loads) + with patch('ansible.modules.network.netscaler.netscaler_nitro_request.AnsibleModule', Mock(return_value=module_mock)): + instance = netscaler_nitro_request.NitroAPICaller() + + data = {resource: resource_data} + result = { + 'nitro_errorcode': 0, + 'http_response_body': json.dumps(data), + } + expected_result = { + 'nitro_object': resource_data + } + expected_result.update(result) + instance.handle_get_return_object(result) + self.assertDictEqual(result, expected_result)