#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2016, Renato Orgito <orgito@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import absolute_import, division, print_function
__metaclass__ = type


DOCUMENTATION = r"""
module: spectrum_device
short_description: Creates/deletes devices in CA Spectrum
description:
  - This module allows you to create and delete devices in CA Spectrum U(https://www.ca.com/us/products/ca-spectrum.html).
  - Tested on CA Spectrum 9.4.2, 10.1.1 and 10.2.1.
author: "Renato Orgito (@orgito)"
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: full
  diff_mode:
    support: none
options:
  device:
    type: str
    aliases: [host, name]
    required: true
    description:
      - IP address of the device.
      - If a hostname is given, it will be resolved to the IP address.
  community:
    type: str
    description:
      - SNMP community used for device discovery.
      - Required when O(state=present).
    required: true
  landscape:
    type: str
    required: true
    description:
      - Landscape handle of the SpectroServer to which add or remove the device.
  state:
    type: str
    description:
      - On V(present) creates the device when it does not exist.
      - On V(absent) removes the device when it exists.
    choices: ['present', 'absent']
    default: 'present'
  url:
    type: str
    aliases: [oneclick_url]
    required: true
    description:
      - HTTP, HTTPS URL of the Oneclick server in the form V((http|https\)://host.domain[:port]).
  url_username:
    type: str
    aliases: [oneclick_user]
    required: true
    description:
      - Oneclick user name.
  url_password:
    type: str
    aliases: [oneclick_password]
    required: true
    description:
      - Oneclick user password.
  use_proxy:
    description:
      - If V(false), it will not use a proxy, even if one is defined in an environment variable on the target hosts.
    default: true
    type: bool
  validate_certs:
    description:
      - If V(false), SSL certificates will not be validated. This should only be used on personally controlled sites using
        self-signed certificates.
    default: true
    type: bool
  agentport:
    type: int
    required: false
    description:
      - UDP port used for SNMP discovery.
    default: 161
notes:
  - The devices will be created inside the I(Universe) container of the specified landscape.
  - All the operations will be performed only on the specified landscape.
"""

EXAMPLES = r"""
- name: Add device to CA Spectrum
  local_action:
    module: spectrum_device
    device: '{{ ansible_host }}'
    community: secret
    landscape: '0x100000'
    oneclick_url: http://oneclick.example.com:8080
    oneclick_user: username
    oneclick_password: password
    state: present


- name: Remove device from CA Spectrum
  local_action:
    module: spectrum_device
    device: '{{ ansible_host }}'
    landscape: '{{ landscape_handle }}'
    oneclick_url: http://oneclick.example.com:8080
    oneclick_user: username
    oneclick_password: password
    use_proxy: false
    state: absent
"""

RETURN = r"""
device:
  description: Device data when O(state=present).
  returned: success
  type: dict
  sample: {'model_handle': '0x1007ab', 'landscape': '0x100000', 'address': '10.10.5.1'}
"""

from socket import gethostbyname, gaierror
import xml.etree.ElementTree as ET

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url


def request(resource, xml=None, method=None):
    headers = {
        "Content-Type": "application/xml",
        "Accept": "application/xml"
    }

    url = module.params['oneclick_url'] + '/spectrum/restful/' + resource

    response, info = fetch_url(module, url, data=xml, method=method, headers=headers, timeout=45)

    if info['status'] == 401:
        module.fail_json(msg="failed to authenticate to Oneclick server")

    if info['status'] not in (200, 201, 204):
        module.fail_json(msg=info['msg'])

    return response.read()


def post(resource, xml=None):
    return request(resource, xml=xml, method='POST')


def delete(resource):
    return request(resource, xml=None, method='DELETE')


def get_ip():
    try:
        device_ip = gethostbyname(module.params.get('device'))
    except gaierror:
        module.fail_json(msg="failed to resolve device ip address for '%s'" % module.params.get('device'))

    return device_ip


def get_device(device_ip):
    """Query OneClick for the device using the IP Address"""
    resource = '/models'
    landscape_min = "0x%x" % int(module.params.get('landscape'), 16)
    landscape_max = "0x%x" % (int(module.params.get('landscape'), 16) + 0x100000)

    xml = """<?xml version="1.0" encoding="UTF-8"?>
        <rs:model-request throttlesize="5"
        xmlns:rs="http://www.ca.com/spectrum/restful/schema/request"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.ca.com/spectrum/restful/schema/request ../../../xsd/Request.xsd">
            <rs:target-models>
            <rs:models-search>
                <rs:search-criteria xmlns="http://www.ca.com/spectrum/restful/schema/filter">
                    <action-models>
                        <filtered-models>
                            <and>
                                <equals>
                                    <model-type>SearchManager</model-type>
                                </equals>
                                <greater-than>
                                    <attribute id="0x129fa">
                                        <value>{mh_min}</value>
                                    </attribute>
                                </greater-than>
                                <less-than>
                                    <attribute id="0x129fa">
                                        <value>{mh_max}</value>
                                    </attribute>
                                </less-than>
                            </and>
                        </filtered-models>
                        <action>FIND_DEV_MODELS_BY_IP</action>
                        <attribute id="AttributeID.NETWORK_ADDRESS">
                            <value>{search_ip}</value>
                        </attribute>
                    </action-models>
                </rs:search-criteria>
            </rs:models-search>
            </rs:target-models>
            <rs:requested-attribute id="0x12d7f" /> <!--Network Address-->
        </rs:model-request>
        """.format(search_ip=device_ip, mh_min=landscape_min, mh_max=landscape_max)

    result = post(resource, xml=xml)

    root = ET.fromstring(result)

    if root.get('total-models') == '0':
        return None

    namespace = dict(ca='http://www.ca.com/spectrum/restful/schema/response')

    # get the first device
    model = root.find('ca:model-responses', namespace).find('ca:model', namespace)

    if model.get('error'):
        module.fail_json(msg="error checking device: %s" % model.get('error'))

    # get the attributes
    model_handle = model.get('mh')

    model_address = model.find('./*[@id="0x12d7f"]').text

    # derive the landscape handler from the model handler of the device
    model_landscape = "0x%x" % int(int(model_handle, 16) // 0x100000 * 0x100000)

    device = dict(
        model_handle=model_handle,
        address=model_address,
        landscape=model_landscape)

    return device


def add_device():
    device_ip = get_ip()
    device = get_device(device_ip)

    if device:
        module.exit_json(changed=False, device=device)

    if module.check_mode:
        device = dict(
            model_handle=None,
            address=device_ip,
            landscape="0x%x" % int(module.params.get('landscape'), 16))
        module.exit_json(changed=True, device=device)

    resource = 'model?ipaddress=' + device_ip + '&commstring=' + module.params.get('community')
    resource += '&landscapeid=' + module.params.get('landscape')

    if module.params.get('agentport', None):
        resource += '&agentport=' + str(module.params.get('agentport', 161))

    result = post(resource)
    root = ET.fromstring(result)

    if root.get('error') != 'Success':
        module.fail_json(msg=root.get('error-message'))

    namespace = dict(ca='http://www.ca.com/spectrum/restful/schema/response')
    model = root.find('ca:model', namespace)

    model_handle = model.get('mh')
    model_landscape = "0x%x" % int(int(model_handle, 16) // 0x100000 * 0x100000)

    device = dict(
        model_handle=model_handle,
        address=device_ip,
        landscape=model_landscape,
    )

    module.exit_json(changed=True, device=device)


def remove_device():
    device_ip = get_ip()
    device = get_device(device_ip)

    if device is None:
        module.exit_json(changed=False)

    if module.check_mode:
        module.exit_json(changed=True)

    resource = '/model/' + device['model_handle']
    result = delete(resource)

    root = ET.fromstring(result)

    namespace = dict(ca='http://www.ca.com/spectrum/restful/schema/response')
    error = root.find('ca:error', namespace).text

    if error != 'Success':
        error_message = root.find('ca:error-message', namespace).text
        module.fail_json(msg="%s %s" % (error, error_message))

    module.exit_json(changed=True)


def main():
    global module
    module = AnsibleModule(
        argument_spec=dict(
            device=dict(required=True, aliases=['host', 'name']),
            landscape=dict(required=True),
            state=dict(choices=['present', 'absent'], default='present'),
            community=dict(required=True, no_log=True),   # @TODO remove the 'required', given the required_if ?
            agentport=dict(type='int', default=161),
            url=dict(required=True, aliases=['oneclick_url']),
            url_username=dict(required=True, aliases=['oneclick_user']),
            url_password=dict(required=True, no_log=True, aliases=['oneclick_password']),
            use_proxy=dict(type='bool', default=True),
            validate_certs=dict(type='bool', default=True),
        ),
        required_if=[('state', 'present', ['community'])],
        supports_check_mode=True
    )

    if module.params.get('state') == 'present':
        add_device()
    else:
        remove_device()


if __name__ == '__main__':
    main()