#!/usr/bin/python
'''
# (c) 2020-2022, NetApp, Inc
# 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

DOCUMENTATION = '''
author: NetApp Ansible Team (@carchi8py) <ng-ansibleteam@netapp.com>
description:
  - Call a ZAPI on ONTAP.
  - Cluster ZAPIs are run using a cluster admin account.
  - Vserver ZAPIs can be run using a vsadmin account or using vserver tunneling (cluster admin with I(vserver option)).
  - In case of success, a json dictionary is returned as C(response).
  - In case of a ZAPI error, C(status), C(errno), C(reason) are set to help with diagnosing the issue,
  - and the call is reported as an error ('failed').
  - Other errors (eg connection issues) are reported as Ansible error.
extends_documentation_fragment:
  - netapp.ontap.netapp.na_ontap_zapi
module: na_ontap_zapit
short_description: NetApp ONTAP Run any ZAPI on ONTAP
version_added: "20.4.0"
options:
    zapi:
        description:
        - A dictionary for the zapi and arguments.
        - An XML tag I(<tag>value</tag>) is a dictionary with tag as the key.
        - Value can be another dictionary, a list of dictionaries, a string, or nothing.
        - eg I(<tag/>) is represented as I(tag:)
        - A single zapi can be called at a time.  Ansible warns if duplicate keys are found and only uses the last entry.
        required: true
        type: dict
    vserver:
        description:
        - if provided, forces vserver tunneling.  username identifies a cluster admin account.
        type: str
'''

EXAMPLES = """
-
  name: Ontap ZAPI
  hosts: localhost
  gather_facts: False
  collections:
    - netapp.ontap
  vars:
    login: &login
      hostname: "{{ admin_ip }}"
      username: "{{ admin_username }}"
      password: "{{ admin_password }}"
      https: true
      validate_certs: false
    svm_login: &svm_login
      hostname: "{{ svm_admin_ip }}"
      username: "{{ svm_admin_username }}"
      password: "{{ svm_admin_password }}"
      https: true
      validate_certs: false

  tasks:
    - name: run ontap ZAPI command as cluster admin
      netapp.ontap.na_ontap_zapit:
        <<: *login
        zapi:
          system-get-version:
      register: output
    - debug: var=output

    - name: run ontap ZAPI command as cluster admin
      netapp.ontap.na_ontap_zapit:
        <<: *login
        zapi:
          vserver-get-iter:
      register: output
    - debug: var=output

    - name: run ontap ZAPI command as cluster admin
      netapp.ontap.na_ontap_zapit:
        <<: *login
        zapi:
          vserver-get-iter:
            desired-attributes:
              vserver-info:
                - aggr-list:
                    - aggr-name
                - allowed-protocols:
                    - protocols
                - vserver-aggr-info-list:
                    - vserser-aggr-info
                - uuid
            query:
              vserver-info:
                vserver-name: trident_svm
      register: output
    - debug: var=output

    - name: run ontap ZAPI command as vsadmin
      netapp.ontap.na_ontap_zapit:
        <<: *svm_login
        zapi:
          vserver-get-iter:
            desired-attributes:
              vserver-info:
                - uuid
      register: output
    - debug: var=output

    - name: run ontap ZAPI command as vserver tunneling
      netapp.ontap.na_ontap_zapit:
        <<: *login
        vserver: trident_svm
        zapi:
          vserver-get-iter:
            desired-attributes:
              vserver-info:
                - uuid
      register: output
    - debug: var=output

    - name: run ontap active-directory ZAPI command
      netapp.ontap.na_ontap_zapit:
        <<: *login
        vserver: trident_svm
        zapi:
          active-directory-account-create:
            account-name: testaccount
            admin-username: testuser
            admin-password: testpass
            domain: testdomain
            organizational-unit: testou
      register: output
      ignore_errors: True
    - debug: var=output

"""

RETURN = """
response:
  description:
    - If successful, a json dictionary representing the data returned by the ZAPI.
    - If the ZAPI was executed but failed, an empty dictionary.
    - Not present if the ZAPI call cannot be performed.
  returned: On success
  type: dict
status:
  description:
    - If the ZAPI was executed but failed, the status set by the ZAPI.
    - Not present if successful, or if the ZAPI call cannot be performed.
  returned: On error
  type: str
errno:
  description:
    - If the ZAPI was executed but failed, the error code set by the ZAPI.
    - Not present if successful, or if the ZAPI call cannot be performed.
  returned: On error
  type: str
reason:
  description:
    - If the ZAPI was executed but failed, the error reason set by the ZAPI.
    - Not present if successful, or if the ZAPI call cannot be performed.
  returned: On error
  type: str
"""

import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils

try:
    import xmltodict
    HAS_XMLTODICT = True
except ImportError:
    HAS_XMLTODICT = False

try:
    import json
    HAS_JSON = True
except ImportError:
    HAS_JSON = False


class NetAppONTAPZapi:
    ''' calls a ZAPI command '''

    def __init__(self):
        self.argument_spec = netapp_utils.na_ontap_zapi_only_spec()
        self.argument_spec.update(dict(
            zapi=dict(required=True, type='dict'),
            vserver=dict(required=False, type='str'),
        ))
        self.module = AnsibleModule(
            argument_spec=self.argument_spec,
            supports_check_mode=False
        )
        parameters = self.module.params
        # set up state variables
        self.zapi = parameters['zapi']
        self.vserver = parameters['vserver']

        if not HAS_JSON:
            self.module.fail_json(msg="the python json module is required")
        if not netapp_utils.has_netapp_lib():
            self.module.fail_json(msg=netapp_utils.netapp_lib_is_required())
        if not HAS_XMLTODICT:
            self.module.fail_json(msg="the python xmltodict module is required")

        if self.vserver is not None:
            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module, vserver=self.vserver)
        else:
            self.server = netapp_utils.setup_na_ontap_zapi(module=self.module)

    def jsonify_and_parse_output(self, xml_data):
        ''' convert from XML to JSON
            extract status and error fields is present
        '''
        try:
            as_str = xml_data.to_string()
        except Exception as exc:
            self.module.fail_json(msg='Error running zapi in to_string: %s' %
                                  str(exc))
        try:
            as_dict = xmltodict.parse(as_str, xml_attribs=True)
        except Exception as exc:
            self.module.fail_json(msg='Error running zapi in xmltodict: %s: %s' %
                                  (as_str, str(exc)))
        try:
            as_json = json.loads(json.dumps(as_dict))
        except Exception as exc:
            self.module.fail_json(msg='Error running zapi in json load/dump: %s: %s' %
                                  (as_dict, str(exc)))

        if 'results' not in as_json:
            self.module.fail_json(msg='Error running zapi, no results field: %s: %s' %
                                  (as_str, repr(as_json)))

        # set status, and if applicable errno/reason, and remove attribute fields
        errno = None
        reason = None
        response = as_json.pop('results')
        status = response.get('@status', 'no_status_attr')
        if status != 'passed':
            # collect errno and reason
            errno = response.get('@errno', None)
            if errno is None:
                errno = response.get('errorno', None)
            if errno is None:
                errno = 'ESTATUSFAILED'
            reason = response.get('@reason', None)
            if reason is None:
                reason = response.get('reason', None)
            if reason is None:
                reason = 'Execution failure with unknown reason.'

        for key in ('@status', '@errno', '@reason', '@xmlns'):
            try:
                # remove irrelevant info
                del response[key]
            except KeyError:
                pass
        return response, status, errno, reason

    def run_zapi(self):
        ''' calls the ZAPI '''
        zapi_struct = self.zapi
        error = None
        if not isinstance(zapi_struct, dict):
            error = 'A directory entry is expected, eg: system-get-version: '
            zapi = zapi_struct
        else:
            zapi = list(zapi_struct.keys())
            if len(zapi) != 1:
                error = 'A single ZAPI can be called at a time'
            else:
                zapi = zapi[0]

        # log first, then error out as needed
        if error:
            self.module.fail_json(msg='%s, received: %s' % (error, zapi))

        zapi_obj = netapp_utils.zapi.NaElement(zapi)
        attributes = zapi_struct[zapi]
        if attributes is not None and attributes != 'None':
            zapi_obj.translate_struct(attributes)

        try:
            output = self.server.invoke_elem(zapi_obj, True)
        except netapp_utils.zapi.NaApiError as error:
            self.module.fail_json(msg='Error running zapi %s: %s' %
                                  (zapi, to_native(error)),
                                  exception=traceback.format_exc())

        return self.jsonify_and_parse_output(output)

    def apply(self):
        ''' calls the zapi and returns json output '''
        response, status, errno, reason = self.run_zapi()
        if status == 'passed':
            self.module.exit_json(changed=True, response=response)
        msg = 'ZAPI failure: check errno and reason.'
        self.module.fail_json(changed=False, response=response, status=status, errno=errno, reason=reason, msg=msg)


def main():
    """
    Execute action from playbook
    """
    zapi = NetAppONTAPZapi()
    zapi.apply()


if __name__ == '__main__':
    main()
