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

# Copyright (c) 2018, Dag Wieers (dagwieers) <dag@wieers.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: cobbler_system
short_description: Manage system objects in Cobbler
description:
- Add, modify or remove systems in Cobbler
options:
  host:
    description:
    - The name or IP address of the Cobbler system.
    default: 127.0.0.1
    type: str
  port:
    description:
    - Port number to be used for REST connection.
    - The default value depends on parameter C(use_ssl).
    type: int
  username:
    description:
    - The username to log in to Cobbler.
    default: cobbler
    type: str
  password:
    description:
    - The password to log in to Cobbler.
    type: str
  use_ssl:
    description:
    - If C(no), an HTTP connection will be used instead of the default HTTPS connection.
    type: bool
    default: 'yes'
  validate_certs:
    description:
    - If C(no), SSL certificates will not be validated.
    - This should only set to C(no) when used on personally controlled sites using self-signed certificates.
    type: bool
    default: 'yes'
  name:
    description:
    - The system name to manage.
    type: str
  properties:
    description:
    - A dictionary with system properties.
    type: dict
  interfaces:
    description:
    - A list of dictionaries containing interface options.
    type: dict
  sync:
    description:
    - Sync on changes.
    - Concurrently syncing Cobbler is bound to fail.
    type: bool
    default: no
  state:
    description:
    - Whether the system should be present, absent or a query is made.
    choices: [ absent, present, query ]
    default: present
    type: str
author:
- Dag Wieers (@dagwieers)
notes:
- Concurrently syncing Cobbler is bound to fail with weird errors.
- On python 2.7.8 and older (i.e. on RHEL7) you may need to tweak the python behaviour to disable certificate validation.
  More information at L(Certificate verification in Python standard library HTTP clients,https://access.redhat.com/articles/2039753).
'''

EXAMPLES = r'''
- name: Ensure the system exists in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: myhost
    properties:
      profile: CentOS6-x86_64
      name_servers: [ 2.3.4.5, 3.4.5.6 ]
      name_servers_search: foo.com, bar.com
    interfaces:
      eth0:
        macaddress: 00:01:02:03:04:05
        ipaddress: 1.2.3.4
  delegate_to: localhost

- name: Enable network boot in Cobbler
  community.general.cobbler_system:
    host: bdsol-aci-cobbler-01
    username: cobbler
    password: ins3965!
    name: bdsol-aci51-apic1.cisco.com
    properties:
      netboot_enabled: yes
    state: present
  delegate_to: localhost

- name: Query all systems in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    state: query
  register: cobbler_systems
  delegate_to: localhost

- name: Query a specific system in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: '{{ inventory_hostname }}'
    state: query
  register: cobbler_properties
  delegate_to: localhost

- name: Ensure the system does not exist in Cobbler
  community.general.cobbler_system:
    host: cobbler01
    username: cobbler
    password: MySuperSecureP4sswOrd
    name: myhost
    state: absent
  delegate_to: localhost
'''

RETURN = r'''
systems:
  description: List of systems
  returned: C(state=query) and C(name) is not provided
  type: list
system:
  description: (Resulting) information about the system we are working with
  returned: when C(name) is provided
  type: dict
'''

import copy
import datetime
import ssl

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six import iteritems
from ansible.module_utils.six.moves import xmlrpc_client
from ansible.module_utils.common.text.converters import to_text

IFPROPS_MAPPING = dict(
    bondingopts='bonding_opts',
    bridgeopts='bridge_opts',
    connected_mode='connected_mode',
    cnames='cnames',
    dhcptag='dhcp_tag',
    dnsname='dns_name',
    ifgateway='if_gateway',
    interfacetype='interface_type',
    interfacemaster='interface_master',
    ipaddress='ip_address',
    ipv6address='ipv6_address',
    ipv6defaultgateway='ipv6_default_gateway',
    ipv6mtu='ipv6_mtu',
    ipv6prefix='ipv6_prefix',
    ipv6secondaries='ipv6_secondariesu',
    ipv6staticroutes='ipv6_static_routes',
    macaddress='mac_address',
    management='management',
    mtu='mtu',
    netmask='netmask',
    static='static',
    staticroutes='static_routes',
    virtbridge='virt_bridge',
)


def getsystem(conn, name, token):
    system = dict()
    if name:
        # system = conn.get_system(name, token)
        systems = conn.find_system(dict(name=name), token)
        if systems:
            system = systems[0]
    return system


def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type='str', default='127.0.0.1'),
            port=dict(type='int'),
            username=dict(type='str', default='cobbler'),
            password=dict(type='str', no_log=True),
            use_ssl=dict(type='bool', default=True),
            validate_certs=dict(type='bool', default=True),
            name=dict(type='str'),
            interfaces=dict(type='dict'),
            properties=dict(type='dict'),
            sync=dict(type='bool', default=False),
            state=dict(type='str', default='present', choices=['absent', 'present', 'query']),
        ),
        supports_check_mode=True,
    )

    username = module.params['username']
    password = module.params['password']
    port = module.params['port']
    use_ssl = module.params['use_ssl']
    validate_certs = module.params['validate_certs']

    name = module.params['name']
    state = module.params['state']

    module.params['proto'] = 'https' if use_ssl else 'http'
    if not port:
        module.params['port'] = '443' if use_ssl else '80'

    result = dict(
        changed=False,
    )

    start = datetime.datetime.utcnow()

    ssl_context = None
    if not validate_certs:
        try:
            ssl_context = ssl._create_unverified_context()
        except AttributeError:
            # Legacy Python that doesn't verify HTTPS certificates by default
            pass
        else:
            # Handle target environment that doesn't support HTTPS verification
            ssl._create_default_https_context = ssl._create_unverified_context

    url = '{proto}://{host}:{port}/cobbler_api'.format(**module.params)
    if ssl_context:
        conn = xmlrpc_client.ServerProxy(url, context=ssl_context)
    else:
        conn = xmlrpc_client.Server(url)

    try:
        token = conn.login(username, password)
    except xmlrpc_client.Fault as e:
        module.fail_json(msg="Failed to log in to Cobbler '{url}' as '{username}'. {error}".format(url=url, error=to_text(e), **module.params))
    except Exception as e:
        module.fail_json(msg="Connection to '{url}' failed. {error}".format(url=url, error=to_text(e), **module.params))

    system = getsystem(conn, name, token)
    # result['system'] = system

    if state == 'query':
        if name:
            result['system'] = system
        else:
            # Turn it into a dictionary of dictionaries
            # all_systems = conn.get_systems()
            # result['systems'] = { system['name']: system for system in all_systems }

            # Return a list of dictionaries
            result['systems'] = conn.get_systems()

    elif state == 'present':

        if system:
            # Update existing entry
            system_id = conn.get_system_handle(name, token)

            for key, value in iteritems(module.params['properties']):
                if key not in system:
                    module.warn("Property '{0}' is not a valid system property.".format(key))
                if system[key] != value:
                    try:
                        conn.modify_system(system_id, key, value, token)
                        result['changed'] = True
                    except Exception as e:
                        module.fail_json(msg="Unable to change '{0}' to '{1}'. {2}".format(key, value, e))

        else:
            # Create a new entry
            system_id = conn.new_system(token)
            conn.modify_system(system_id, 'name', name, token)
            result['changed'] = True

            if module.params['properties']:
                for key, value in iteritems(module.params['properties']):
                    try:
                        conn.modify_system(system_id, key, value, token)
                    except Exception as e:
                        module.fail_json(msg="Unable to change '{0}' to '{1}'. {2}".format(key, value, e))

        # Add interface properties
        interface_properties = dict()
        if module.params['interfaces']:
            for device, values in iteritems(module.params['interfaces']):
                for key, value in iteritems(values):
                    if key == 'name':
                        continue
                    if key not in IFPROPS_MAPPING:
                        module.warn("Property '{0}' is not a valid system property.".format(key))
                    if not system or system['interfaces'][device][IFPROPS_MAPPING[key]] != value:
                        result['changed'] = True
                    interface_properties['{0}-{1}'.format(key, device)] = value

            if result['changed'] is True:
                conn.modify_system(system_id, "modify_interface", interface_properties, token)

        # Only save when the entry was changed
        if not module.check_mode and result['changed']:
            conn.save_system(system_id, token)

    elif state == 'absent':

        if system:
            if not module.check_mode:
                conn.remove_system(name, token)
            result['changed'] = True

    if not module.check_mode and module.params['sync'] and result['changed']:
        try:
            conn.sync(token)
        except Exception as e:
            module.fail_json(msg="Failed to sync Cobbler. {0}".format(to_text(e)))

    if state in ('absent', 'present'):
        result['system'] = getsystem(conn, name, token)

        if module._diff:
            result['diff'] = dict(before=system, after=result['system'])

    elapsed = datetime.datetime.utcnow() - start
    module.exit_json(elapsed=elapsed.seconds, **result)


if __name__ == '__main__':
    main()
