# -*- coding: utf-8 -*-

# Authors:
#   Thomas Woerner <twoerner@redhat.com>
#
# Copyright (C) 2019 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.

ANSIBLE_METADATA = {
    "metadata_version": "1.0",
    "supported_by": "community",
    "status": ["preview"],
}

DOCUMENTATION = """
---
module: ipahost
short description: Manage FreeIPA hosts
description: Manage FreeIPA hosts
options:
  ipaadmin_principal:
    description: The admin principal
    default: admin
  ipaadmin_password:
    description: The admin password
    required: false
  name:
    description: The full qualified domain name.
    aliases: ["fqdn"]
    required: true

  hosts:
    description: The list of user host dicts
    required: false
    options:
      name:
        description: The host (internally uid).
        aliases: ["fqdn"]
        required: true
      description:
        description: The host description
        required: false
      locality:
        description: Host locality (e.g. "Baltimore, MD")
        required: false
      location:
        description: Host location (e.g. "Lab 2")
        aliases: ["ns_host_location"]
        required: false
      platform:
        description: Host hardware platform (e.g. "Lenovo T61")
        aliases: ["ns_hardware_platform"]
        required: false
      os:
        description: Host operating system and version (e.g. "Fedora 9")
        aliases: ["ns_os_version"]
        required: false
      password:
        description: Password used in bulk enrollment
        aliases: ["user_password", "userpassword"]
        required: false
      random:
        description:
          Initiate the generation of a random password to be used in bulk
          enrollment
        aliases: ["random_password"]
        required: false
      certificate:
        description: List of base-64 encoded host certificates
        type: list
        aliases: ["usercertificate"]
        required: false
      managedby_host:
        description: List of hosts that can manage this host
        type: list
        aliases: ["principalname", "krbprincipalname"]
        required: false
      principal:
        description: List of principal aliases for this host
        type: list
        aliases: ["principalname", "krbprincipalname"]
        required: false
      allow_create_keytab_user:
        description: Users allowed to create a keytab of this host
        aliases: ["ipaallowedtoperform_write_keys_user"]
        required: false
      allow_create_keytab_group:
        description: Groups allowed to create a keytab of this host
        aliases: ["ipaallowedtoperform_write_keys_group"]
        required: false
      allow_create_keytab_host:
        description: Hosts allowed to create a keytab of this host
        aliases: ["ipaallowedtoperform_write_keys_host"]
        required: false
      allow_create_keytab_hostgroup:
        description: Hostgroups allowed to create a keytab of this host
        aliases: ["ipaallowedtoperform_write_keys_hostgroup"]
        required: false
      allow_retrieve_keytab_user:
        description: Users allowed to retrieve a keytab of this host
        aliases: ["ipaallowedtoperform_read_keys_user"]
        required: false
      allow_retrieve_keytab_group:
        description: Groups allowed to retrieve a keytab of this host
        aliases: ["ipaallowedtoperform_read_keys_group"]
        required: false
      allow_retrieve_keytab_host:
        description: Hosts allowed to retrieve a keytab of this host
        aliases: ["ipaallowedtoperform_read_keys_host"]
        required: false
      allow_retrieve_keytab_hostgroup:
        description: Hostgroups allowed to retrieve a keytab of this host
        aliases: ["ipaallowedtoperform_read_keys_hostgroup"]
        required: false
      mac_address:
        description: List of hardware MAC addresses.
        type: list
        aliases: ["macaddress"]
        required: false
      sshpubkey:
        description: List of SSH public keys
        type: list
        aliases: ["ipasshpubkey"]
        required: false
      userclass:
        description:
          Host category (semantics placed on this attribute are for local
          interpretation)
        aliases: ["class"]
        required: false
      auth_ind:
        description:
          Defines a whitelist for Authentication Indicators. Use 'otp' to allow
          OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA
          authentications. Other values may be used for custom configurations.
          Use empty string to reset auth_ind to the initial value.
        type: list
        aliases: ["krbprincipalauthind"]
        choices: ["radius", "otp", "pkinit", "hardened", ""]
        required: false
      requires_pre_auth:
        description: Pre-authentication is required for the service
        type: bool
        aliases: ["ipakrbrequirespreauth"]
        required: false
      ok_as_delegate:
        description: Client credentials may be delegated to the service
        type: bool
        aliases: ["ipakrbokasdelegate"]
        required: false
      ok_to_auth_as_delegate:
        description:
          The service is allowed to authenticate on behalf of a client
        type: bool
        aliases: ["ipakrboktoauthasdelegate"]
        required: false
      force:
        description: Force host name even if not in DNS
        required: false
      reverse:
        description: Reverse DNS detection
        default: true
        required: false
      ip_address:
        description:
          The host IP address list (IPv4 and IPv6). No IP address conflict
          check will be done.
        aliases: ["ipaddress"]
        required: false
      update_dns:
        description:
          Controls the update of the DNS SSHFP records for existing hosts and
          the removal of all DNS entries if a host gets removed with state
          absent.
        required: false
  description:
    description: The host description
    required: false
  locality:
    description: Host locality (e.g. "Baltimore, MD")
    required: false
  location:
    description: Host location (e.g. "Lab 2")
    aliases: ["ns_host_location"]
    required: false
  platform:
    description: Host hardware platform (e.g. "Lenovo T61")
    aliases: ["ns_hardware_platform"]
    required: false
  os:
    description: Host operating system and version (e.g. "Fedora 9")
    aliases: ["ns_os_version"]
    required: false
  password:
    description: Password used in bulk enrollment
    aliases: ["user_password", "userpassword"]
    required: false
  random:
    description:
      Initiate the generation of a random password to be used in bulk
      enrollment
    aliases: ["random_password"]
    required: false
  certificate:
    description: List of base-64 encoded host certificates
    type: list
    aliases: ["usercertificate"]
    required: false
  managedby_host:
    description: List of hosts that can manage this host
    type: list
    aliases: ["principalname", "krbprincipalname"]
    required: false
  principal:
    description: List of principal aliases for this host
    type: list
    aliases: ["principalname", "krbprincipalname"]
    required: false
  allow_create_keytab_user:
    description: Users allowed to create a keytab of this host
    aliases: ["ipaallowedtoperform_write_keys_user"]
    required: false
  allow_create_keytab_group:
    description: Groups allowed to create a keytab of this host
    aliases: ["ipaallowedtoperform_write_keys_group"]
    required: false
  allow_create_keytab_host:
    description: Hosts allowed to create a keytab of this host
    aliases: ["ipaallowedtoperform_write_keys_host"]
    required: false
  allow_create_keytab_hostgroup:
    description: Hostgroups allowed to create a keytab of this host
    aliases: ["ipaallowedtoperform_write_keys_hostgroup"]
    required: false
  allow_retrieve_keytab_user:
    description: Users allowed to retrieve a keytab of this host
    aliases: ["ipaallowedtoperform_read_keys_user"]
    required: false
  allow_retrieve_keytab_group:
    description: Groups allowed to retrieve a keytab of this host
    aliases: ["ipaallowedtoperform_read_keys_group"]
    required: false
  allow_retrieve_keytab_host:
    description: Hosts allowed to retrieve a keytab of this host
    aliases: ["ipaallowedtoperform_read_keys_host"]
    required: false
  allow_retrieve_keytab_hostgroup:
    description: Hostgroups allowed to retrieve a keytab of this host
    aliases: ["ipaallowedtoperform_read_keys_hostgroup"]
    required: false
  mac_address:
    description: List of hardware MAC addresses.
    type: list
    aliases: ["macaddress"]
    required: false
  sshpubkey:
    description: List of SSH public keys
    type: list
    aliases: ["ipasshpubkey"]
    required: false
  userclass:
    description:
      Host category (semantics placed on this attribute are for local
      interpretation)
    aliases: ["class"]
    required: false
  auth_ind:
    description:
      Defines a whitelist for Authentication Indicators. Use 'otp' to allow
      OTP-based 2FA authentications. Use 'radius' to allow RADIUS-based 2FA
      authentications. Other values may be used for custom configurations.
      Use empty string to reset auth_ind to the initial value.
    type: list
    aliases: ["krbprincipalauthind"]
    choices: ["radius", "otp", "pkinit", "hardened", ""]
    required: false
  requires_pre_auth:
    description: Pre-authentication is required for the service
    type: bool
    aliases: ["ipakrbrequirespreauth"]
    required: false
  ok_as_delegate:
    description: Client credentials may be delegated to the service
    type: bool
    aliases: ["ipakrbokasdelegate"]
    required: false
  ok_to_auth_as_delegate:
    description: The service is allowed to authenticate on behalf of a client
    type: bool
    aliases: ["ipakrboktoauthasdelegate"]
    required: false
  force:
    description: Force host name even if not in DNS
    required: false
  reverse:
    description: Reverse DNS detection
    default: true
    required: false
  ip_address:
    description:
      The host IP address list (IPv4 and IPv6). No IP address conflict
      check will be done.
    aliases: ["ipaddress"]
    required: false
  update_dns:
    description:
      Controls the update of the DNS SSHFP records for existing hosts and
      the removal of all DNS entries if a host gets removed with state
      absent.
    required: false
  update_password:
    description:
      Set password for a host in present state only on creation or always
    default: 'always'
    choices: ["always", "on_create"]
  action:
    description: Work on host or member level
    default: "host"
    choices: ["member", "host"]
  state:
    description: State to ensure
    default: present
    choices: ["present", "absent",
              "disabled"]
author:
    - Thomas Woerner
"""

EXAMPLES = """
# Ensure host is present
- ipahost:
    ipaadmin_password: SomeADMINpassword
    name: host01.example.com
    description: Example host
    ip_address: 192.168.0.123
    locality: Lab
    ns_host_location: Lab
    ns_os_version: CentOS 7
    ns_hardware_platform: Lenovo T61
    mac_address:
    - "08:00:27:E3:B1:2D"
    - "52:54:00:BD:97:1E"
    state: present

# Ensure host is present without DNS
- ipahost:
    ipaadmin_password: SomeADMINpassword
    name: host02.example.com
    description: Example host
    force: yes

# Initiate generation of a random password for the host
- ipahost:
    ipaadmin_password: SomeADMINpassword
    name: host01.example.com
    description: Example host
    ip_address: 192.168.0.123
    random: yes

# Ensure host is disabled
- ipahost:
    ipaadmin_password: SomeADMINpassword
    name: host01.example.com
    update_dns: yes
    state: disabled

# Ensure host is absent
- ipahost:
    ipaadmin_password: password1
    name: host01.example.com
    state: absent
"""

RETURN = """
host:
  description: Host dict with random password
  returned: If random is yes and user did not exist or update_password is yes
  type: dict
  options:
    randompassword:
      description: The generated random password
      returned: If only one user is handled by the module
    name:
      description: The user name of the user that got a new random password
      returned: If several users are handled by the module
      type: dict
      options:
        randompassword:
          description: The generated random password
          returned: always
"""

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
from ansible.module_utils.ansible_freeipa_module import temp_kinit, \
    temp_kdestroy, valid_creds, api_connect, api_command, compare_args_ipa, \
    module_params_get, gen_add_del_lists, encode_certificate, api_get_realm, \
    is_ipv4_addr, is_ipv6_addr, ipalib_errors
import six


if six.PY3:
    unicode = str


def find_host(module, name):
    _args = {
        "all": True,
    }

    try:
        _result = api_command(module, "host_show", to_text(name), _args)
    except ipalib_errors.NotFound as e:
        msg = str(e)
        if "host not found" in msg:
            return None
        module.fail_json(msg="host_show failed: %s" % msg)

    _res = _result["result"]
    certs = _res.get("usercertificate")
    if certs is not None:
        _res["usercertificate"] = [encode_certificate(cert) for
                                   cert in certs]
    return _res


def find_dnsrecord(module, name):
    """
    Search for a DNS record.

    This function may raise ipalib_errors.NotFound in some cases,
    and it should be handled by the caller.
    """
    domain_name = name[name.find(".")+1:]
    host_name = name[:name.find(".")]

    _args = {
        "all": True,
        "idnsname": to_text(host_name)
    }

    _result = api_command(module, "dnsrecord_show", to_text(domain_name),
                          _args)

    return _result["result"]


def show_host(module, name):
    _result = api_command(module, "host_show", to_text(name), {})
    return _result["result"]


def gen_args(description, locality, location, platform, os, password, random,
             mac_address, sshpubkey, userclass, auth_ind, requires_pre_auth,
             ok_as_delegate, ok_to_auth_as_delegate, force, _reverse,
             ip_address, update_dns):
    # certificate, managedby_host, principal, create_keytab_* and
    # allow_retrieve_keytab_* are not handled here
    _args = {}
    if description is not None:
        _args["description"] = description
    if locality is not None:
        _args["l"] = locality
    if location is not None:
        _args["nshostlocation"] = location
    if platform is not None:
        _args["nshardwareplatform"] = platform
    if os is not None:
        _args["nsosversion"] = os
    if password is not None:
        _args["userpassword"] = password
    if random is not None:
        _args["random"] = random
    if mac_address is not None:
        _args["macaddress"] = mac_address
    if sshpubkey is not None:
        _args["ipasshpubkey"] = sshpubkey
    if userclass is not None:
        _args["userclass"] = userclass
    if auth_ind is not None:
        _args["krbprincipalauthind"] = auth_ind
    if requires_pre_auth is not None:
        _args["ipakrbrequirespreauth"] = requires_pre_auth
    if ok_as_delegate is not None:
        _args["ipakrbokasdelegate"] = ok_as_delegate
    if ok_to_auth_as_delegate is not None:
        _args["ipakrboktoauthasdelegate"] = ok_to_auth_as_delegate
    if force is not None:
        _args["force"] = force
    if ip_address is not None:
        # IP addresses are handed extra, therefore it is needed to set
        # the force option here to make sure that host-add is able to
        # add a host without IP address.
        _args["force"] = True
    if update_dns is not None:
        _args["updatedns"] = update_dns

    return _args


def gen_dnsrecord_args(module, ip_address, reverse):
    _args = {}
    if reverse is not None:
        _args["a_extra_create_reverse"] = reverse
        _args["aaaa_extra_create_reverse"] = reverse
    if ip_address is not None:
        for ip in ip_address:
            if is_ipv4_addr(ip):
                _args.setdefault("arecord", []).append(ip)
            elif is_ipv6_addr(ip):
                _args.setdefault("aaaarecord", []).append(ip)
            else:
                module.fail_json(msg="'%s' is not a valid IP address." % ip)

    return _args


def check_parameters(   # pylint: disable=unused-argument
        module, state, action,
        description, locality, location, platform, os, password, random,
        certificate, managedby_host, principal, allow_create_keytab_user,
        allow_create_keytab_group, allow_create_keytab_host,
        allow_create_keytab_hostgroup, allow_retrieve_keytab_user,
        allow_retrieve_keytab_group, allow_retrieve_keytab_host,
        allow_retrieve_keytab_hostgroup, mac_address, sshpubkey,
        userclass, auth_ind, requires_pre_auth, ok_as_delegate,
        ok_to_auth_as_delegate, force, reverse, ip_address, update_dns,
        update_password):
    if state == "present":
        if action == "member":
            # certificate, managedby_host, principal,
            # allow_create_keytab_*, allow_retrieve_keytab_*,
            invalid = ["description", "locality", "location", "platform",
                       "os", "password", "random", "mac_address", "sshpubkey",
                       "userclass", "auth_ind", "requires_pre_auth",
                       "ok_as_delegate", "ok_to_auth_as_delegate", "force",
                       "reverse", "update_dns", "update_password"]
            for x in invalid:
                if vars()[x] is not None:
                    module.fail_json(
                        msg="Argument '%s' can not be used with action "
                        "'%s'" % (x, action))

    if state == "absent":
        invalid = ["description", "locality", "location", "platform", "os",
                   "password", "random", "mac_address", "sshpubkey",
                   "userclass", "auth_ind", "requires_pre_auth",
                   "ok_as_delegate", "ok_to_auth_as_delegate", "force",
                   "reverse", "update_password"]
        for x in invalid:
            if vars()[x] is not None:
                module.fail_json(
                    msg="Argument '%s' can not be used with state '%s'" %
                    (x, state))
        if action == "host":
            invalid = [
                "certificate", "managedby_host", "principal",
                "allow_create_keytab_user", "allow_create_keytab_group",
                "allow_create_keytab_host", "allow_create_keytab_hostgroup",
                "allow_retrieve_keytab_user", "allow_retrieve_keytab_group",
                "allow_retrieve_keytab_host",
                "allow_retrieve_keytab_hostgroup"
            ]
            for x in invalid:
                if vars()[x] is not None:
                    module.fail_json(
                        msg="Argument '%s' can only be used with action "
                        "'member' for state '%s'" % (x, state))


def main():
    host_spec = dict(
        # present
        description=dict(type="str", default=None),
        locality=dict(type="str", default=None),
        location=dict(type="str", aliases=["ns_host_location"],
                      default=None),
        platform=dict(type="str", aliases=["ns_hardware_platform"],
                      default=None),
        os=dict(type="str", aliases=["ns_os_version"], default=None),
        password=dict(type="str",
                      aliases=["user_password", "userpassword"],
                      default=None, no_log=True),
        random=dict(type="bool", aliases=["random_password"],
                    default=None),
        certificate=dict(type="list", aliases=["usercertificate"],
                         default=None),
        managedby_host=dict(type="list",
                            default=None),
        principal=dict(type="list", aliases=["krbprincipalname"],
                       default=None),
        allow_create_keytab_user=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_user"],
            default=None),
        allow_create_keytab_group=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_group"],
            default=None),
        allow_create_keytab_host=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_host"],
            default=None),
        allow_create_keytab_hostgroup=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_hostgroup"],
            default=None),
        allow_retrieve_keytab_user=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_user"],
            default=None),
        allow_retrieve_keytab_group=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_group"],
            default=None),
        allow_retrieve_keytab_host=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_host"],
            default=None),
        allow_retrieve_keytab_hostgroup=dict(
            type="list",
            aliases=["ipaallowedtoperform_write_keys_hostgroup"],
            default=None),
        mac_address=dict(type="list", aliases=["macaddress"],
                         default=None),
        sshpubkey=dict(type="str", aliases=["ipasshpubkey"],
                       default=None),
        userclass=dict(type="list", aliases=["class"],
                       default=None),
        auth_ind=dict(type='list', aliases=["krbprincipalauthind"],
                      default=None,
                      choices=['radius', 'otp', 'pkinit', 'hardened', '']),
        requires_pre_auth=dict(type="bool", aliases=["ipakrbrequirespreauth"],
                               default=None),
        ok_as_delegate=dict(type="bool", aliases=["ipakrbokasdelegate"],
                            default=None),
        ok_to_auth_as_delegate=dict(type="bool",
                                    aliases=["ipakrboktoauthasdelegate"],
                                    default=None),
        force=dict(type='bool', default=None),
        reverse=dict(type='bool', default=None),
        ip_address=dict(type="list", aliases=["ipaddress"],
                        default=None),
        update_dns=dict(type="bool", aliases=["updatedns"],
                        default=None),
        # no_members

        # for update:
        # krbprincipalname
    )

    ansible_module = AnsibleModule(
        argument_spec=dict(
            # general
            ipaadmin_principal=dict(type="str", default="admin"),
            ipaadmin_password=dict(type="str", no_log=True),

            name=dict(type="list", aliases=["fqdn"], default=None,
                      required=False),

            hosts=dict(type="list", default=None,
                       options=dict(
                           # Here name is a simple string
                           name=dict(type="str", aliases=["fqdn"],
                                     required=True),
                           # Add host specific parameters
                           **host_spec
                       ),
                       elements='dict', required=False),

            # mod
            update_password=dict(type='str', default=None,
                                 choices=['always', 'on_create']),

            # general
            action=dict(type="str", default="host",
                        choices=["member", "host"]),
            state=dict(type="str", default="present",
                       choices=["present", "absent", "disabled"]),

            # Add host specific parameters for simple use case
            **host_spec
        ),
        mutually_exclusive=[["name", "hosts"]],
        required_one_of=[["name", "hosts"]],
        supports_check_mode=True,
    )

    ansible_module._ansible_debug = True

    # Get parameters

    # general
    ipaadmin_principal = module_params_get(ansible_module,
                                           "ipaadmin_principal")
    ipaadmin_password = module_params_get(ansible_module,
                                          "ipaadmin_password")
    names = module_params_get(ansible_module, "name")
    hosts = module_params_get(ansible_module, "hosts")

    # present
    description = module_params_get(ansible_module, "description")
    locality = module_params_get(ansible_module, "locality")
    location = module_params_get(ansible_module, "location")
    platform = module_params_get(ansible_module, "platform")
    os = module_params_get(ansible_module, "os")
    password = module_params_get(ansible_module, "password")
    random = module_params_get(ansible_module, "random")
    certificate = module_params_get(ansible_module, "certificate")
    managedby_host = module_params_get(ansible_module, "managedby_host")
    principal = module_params_get(ansible_module, "principal")
    allow_create_keytab_user = module_params_get(
        ansible_module, "allow_create_keytab_user")
    allow_create_keytab_group = module_params_get(
        ansible_module, "allow_create_keytab_group")
    allow_create_keytab_host = module_params_get(
        ansible_module, "allow_create_keytab_host")
    allow_create_keytab_hostgroup = module_params_get(
        ansible_module, "allow_create_keytab_hostgroup")
    allow_retrieve_keytab_user = module_params_get(
        ansible_module, "allow_retrieve_keytab_user")
    allow_retrieve_keytab_group = module_params_get(
        ansible_module, "allow_retrieve_keytab_group")
    allow_retrieve_keytab_host = module_params_get(
        ansible_module, "allow_retrieve_keytab_host")
    allow_retrieve_keytab_hostgroup = module_params_get(
        ansible_module, "allow_retrieve_keytab_hostgroup")
    mac_address = module_params_get(ansible_module, "mac_address")
    sshpubkey = module_params_get(ansible_module, "sshpubkey")
    userclass = module_params_get(ansible_module, "userclass")
    auth_ind = module_params_get(ansible_module, "auth_ind")
    requires_pre_auth = module_params_get(ansible_module, "requires_pre_auth")
    ok_as_delegate = module_params_get(ansible_module, "ok_as_delegate")
    ok_to_auth_as_delegate = module_params_get(ansible_module,
                                               "ok_to_auth_as_delegate")
    force = module_params_get(ansible_module, "force")
    reverse = module_params_get(ansible_module, "reverse")
    ip_address = module_params_get(ansible_module, "ip_address")
    update_dns = module_params_get(ansible_module, "update_dns")
    update_password = module_params_get(ansible_module, "update_password")
    # general
    action = module_params_get(ansible_module, "action")
    state = module_params_get(ansible_module, "state")

    # Check parameters

    if (names is None or len(names) < 1) and \
       (hosts is None or len(hosts) < 1):
        ansible_module.fail_json(msg="One of name and hosts is required")

    if state == "present":
        if names is not None and len(names) != 1:
            ansible_module.fail_json(
                msg="Only one host can be added at a time.")

    check_parameters(
        ansible_module, state, action,
        description, locality, location, platform, os, password, random,
        certificate, managedby_host, principal, allow_create_keytab_user,
        allow_create_keytab_group, allow_create_keytab_host,
        allow_create_keytab_hostgroup, allow_retrieve_keytab_user,
        allow_retrieve_keytab_group, allow_retrieve_keytab_host,
        allow_retrieve_keytab_hostgroup, mac_address, sshpubkey, userclass,
        auth_ind, requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate,
        force, reverse, ip_address, update_dns, update_password)

    # Use hosts if names is None
    if hosts is not None:
        names = hosts

    # Init

    changed = False
    exit_args = {}
    ccache_dir = None
    ccache_name = None
    try:
        if not valid_creds(ansible_module, ipaadmin_principal):
            ccache_dir, ccache_name = temp_kinit(ipaadmin_principal,
                                                 ipaadmin_password)
        api_connect()

        # Check version specific settings

        server_realm = api_get_realm()

        commands = []
        host_set = set()

        for host in names:
            if isinstance(host, dict):
                name = host.get("name")
                if name in host_set:
                    ansible_module.fail_json(
                        msg="host '%s' is used more than once" % name)
                host_set.add(name)
                description = host.get("description")
                locality = host.get("locality")
                location = host.get("location")
                platform = host.get("platform")
                os = host.get("os")
                password = host.get("password")
                random = host.get("random")
                certificate = host.get("certificate")
                managedby_host = host.get("managedby_host")
                principal = host.get("principal")
                allow_create_keytab_user = host.get(
                    "allow_create_keytab_user")
                allow_create_keytab_group = host.get(
                    "allow_create_keytab_group")
                allow_create_keytab_host = host.get(
                    "allow_create_keytab_host")
                allow_create_keytab_hostgroup = host.get(
                    "allow_create_keytab_hostgroup")
                allow_retrieve_keytab_user = host.get(
                    "allow_retrieve_keytab_user")
                allow_retrieve_keytab_group = host.get(
                    "allow_retrieve_keytab_group")
                allow_retrieve_keytab_host = host.get(
                    "allow_retrieve_keytab_host")
                allow_retrieve_keytab_hostgroup = host.get(
                    "allow_retrieve_keytab_hostgroup")
                mac_address = host.get("mac_address")
                sshpubkey = host.get("sshpubkey")
                userclass = host.get("userclass")
                auth_ind = host.get("auth_ind")
                requires_pre_auth = host.get("requires_pre_auth")
                ok_as_delegate = host.get("ok_as_delegate")
                ok_to_auth_as_delegate = host.get("ok_to_auth_as_delegate")
                force = host.get("force")
                reverse = host.get("reverse")
                ip_address = host.get("ip_address")
                update_dns = host.get("update_dns")
                # update_password is not part of hosts structure
                # action is not part of hosts structure
                # state is not part of hosts structure

                check_parameters(
                    ansible_module, state, action,
                    description, locality, location, platform, os, password,
                    random, certificate, managedby_host, principal,
                    allow_create_keytab_user, allow_create_keytab_group,
                    allow_create_keytab_host, allow_create_keytab_hostgroup,
                    allow_retrieve_keytab_user, allow_retrieve_keytab_group,
                    allow_retrieve_keytab_host,
                    allow_retrieve_keytab_hostgroup, mac_address, sshpubkey,
                    userclass, auth_ind, requires_pre_auth, ok_as_delegate,
                    ok_to_auth_as_delegate, force, reverse, ip_address,
                    update_dns, update_password)

            elif isinstance(host, (str, unicode)):
                name = host
            else:
                ansible_module.fail_json(msg="Host '%s' is not valid" %
                                         repr(host))

            # Make sure host exists
            res_find = find_host(ansible_module, name)
            try:
                res_find_dnsrecord = find_dnsrecord(ansible_module, name)
            except ipalib_errors.NotFound as e:
                msg = str(e)
                dns_not_configured = "DNS is not configured" in msg
                dns_zone_not_found = "DNS zone not found" in msg
                dns_res_not_found = "DNS resource record not found" in msg
                if (
                    dns_res_not_found
                    or ip_address is None
                    and (dns_not_configured or dns_zone_not_found)
                ):
                    # IP address(es) not given and no DNS support in IPA
                    # -> Ignore failure
                    # IP address(es) not given and DNS zone is not found
                    # -> Ignore failure
                    res_find_dnsrecord = None
                else:
                    ansible_module.fail_json(msg="%s: %s" % (host, msg))

            # Create command
            if state == "present":
                # Generate args
                args = gen_args(
                    description, locality, location, platform, os, password,
                    random, mac_address, sshpubkey, userclass, auth_ind,
                    requires_pre_auth, ok_as_delegate, ok_to_auth_as_delegate,
                    force, reverse, ip_address, update_dns)
                dnsrecord_args = gen_dnsrecord_args(
                    ansible_module, ip_address, reverse)

                if action == "host":
                    # Found the host
                    if res_find is not None:
                        # Ignore password with update_password == on_create
                        if update_password == "on_create":
                            # Ignore userpassword and random for existing
                            # host if update_password is "on_create"
                            if "userpassword" in args:
                                del args["userpassword"]
                            if "random" in args:
                                del args["random"]
                        elif "userpassword" in args or "random" in args:
                            # Allow an existing OTP to be reset but don't
                            # allow a OTP or to be added to an enrolled host.
                            # Also do not allow to change the password for an
                            # enrolled host.

                            if not res_find["has_password"] and \
                               res_find["has_keytab"]:
                                ansible_module.fail_json(
                                    msg="%s: Password cannot be set on "
                                    "enrolled host." % host
                                )

                        # Ignore force, ip_address and no_reverse for mod
                        for x in ["force", "ip_address", "no_reverse"]:
                            if x in args:
                                del args[x]

                        # Ignore auth_ind if it is empty (for resetting)
                        # and not set in for the host
                        if "krbprincipalauthind" not in res_find and \
                           "krbprincipalauthind" in args and \
                           args["krbprincipalauthind"] == ['']:
                            del args["krbprincipalauthind"]

                        # For all settings is args, check if there are
                        # different settings in the find result.
                        # If yes: modify
                        if not compare_args_ipa(ansible_module, args,
                                                res_find):
                            commands.append([name, "host_mod", args])
                        elif random and "userpassword" in res_find:
                            # Host exists and random is set, return
                            # userpassword
                            if len(names) == 1:
                                exit_args["userpassword"] = \
                                    res_find["userpassword"]
                            else:
                                exit_args.setdefault("hosts", {})[name] = {
                                    "userpassword": res_find["userpassword"]
                                }

                    else:
                        # Remove update_dns as it is not supported by host_add
                        if "updatedns" in args:
                            del args["updatedns"]
                        commands.append([name, "host_add", args])

                    # Handle members: certificate, managedby_host, principal,
                    # allow_create_keytab and allow_retrieve_keytab
                    if res_find is not None:
                        certificate_add, certificate_del = gen_add_del_lists(
                            certificate, res_find.get("usercertificate"))
                        managedby_host_add, managedby_host_del = \
                            gen_add_del_lists(managedby_host,
                                              res_find.get("managedby_host"))
                        principal_add, principal_del = gen_add_del_lists(
                            principal, res_find.get("principal"))
                        # Principals are not returned as utf8 for IPA using
                        # python2 using host_show, therefore we need to
                        # convert the principals that we should remove.
                        principal_del = [to_text(x) for x in principal_del]

                        (allow_create_keytab_user_add,
                         allow_create_keytab_user_del) = \
                            gen_add_del_lists(
                                allow_create_keytab_user,
                                res_find.get(
                                    "ipaallowedtoperform_write_keys_user"))
                        (allow_create_keytab_group_add,
                         allow_create_keytab_group_del) = \
                            gen_add_del_lists(
                                allow_create_keytab_group,
                                res_find.get(
                                    "ipaallowedtoperform_write_keys_group"))
                        (allow_create_keytab_host_add,
                         allow_create_keytab_host_del) = \
                            gen_add_del_lists(
                                allow_create_keytab_host,
                                res_find.get(
                                    "ipaallowedtoperform_write_keys_host"))
                        (allow_create_keytab_hostgroup_add,
                         allow_create_keytab_hostgroup_del) = \
                            gen_add_del_lists(
                                allow_create_keytab_hostgroup,
                                res_find.get(
                                    "ipaallowedtoperform_write_keys_"
                                    "hostgroup"))
                        (allow_retrieve_keytab_user_add,
                         allow_retrieve_keytab_user_del) = \
                            gen_add_del_lists(
                                allow_retrieve_keytab_user,
                                res_find.get(
                                    "ipaallowedtoperform_read_keys_user"))
                        (allow_retrieve_keytab_group_add,
                         allow_retrieve_keytab_group_del) = \
                            gen_add_del_lists(
                                allow_retrieve_keytab_group,
                                res_find.get(
                                    "ipaallowedtoperform_read_keys_group"))
                        (allow_retrieve_keytab_host_add,
                         allow_retrieve_keytab_host_del) = \
                            gen_add_del_lists(
                                allow_retrieve_keytab_host,
                                res_find.get(
                                    "ipaallowedtoperform_read_keys_host"))
                        (allow_retrieve_keytab_hostgroup_add,
                         allow_retrieve_keytab_hostgroup_del) = \
                            gen_add_del_lists(
                                allow_retrieve_keytab_hostgroup,
                                res_find.get(
                                    "ipaallowedtoperform_read_keys_hostgroup"))

                        # IP addresses are not really a member of hosts, but
                        # we will simply treat it as this to enable the
                        # addition and removal of IPv4 and IPv6 addresses in
                        # a simple way.
                        _dnsrec = res_find_dnsrecord or {}
                        dnsrecord_a_add, dnsrecord_a_del = gen_add_del_lists(
                            dnsrecord_args.get("arecord"),
                            _dnsrec.get("arecord"))
                        dnsrecord_aaaa_add, dnsrecord_aaaa_del = \
                            gen_add_del_lists(
                                dnsrecord_args.get("aaaarecord"),
                                _dnsrec.get("aaaarecord"))

                else:
                    if res_find is None:
                        ansible_module.fail_json(
                            msg="No host '%s'" % name)

                if action != "host" or (action == "host" and res_find is None):
                    certificate_add = certificate or []
                    certificate_del = []
                    managedby_host_add = managedby_host or []
                    managedby_host_del = []
                    principal_add = principal or []
                    principal_del = []
                    allow_create_keytab_user_add = \
                        allow_create_keytab_user or []
                    allow_create_keytab_user_del = []
                    allow_create_keytab_group_add = \
                        allow_create_keytab_group or []
                    allow_create_keytab_group_del = []
                    allow_create_keytab_host_add = \
                        allow_create_keytab_host or []
                    allow_create_keytab_host_del = []
                    allow_create_keytab_hostgroup_add = \
                        allow_create_keytab_hostgroup or []
                    allow_create_keytab_hostgroup_del = []
                    allow_retrieve_keytab_user_add = \
                        allow_retrieve_keytab_user or []
                    allow_retrieve_keytab_user_del = []
                    allow_retrieve_keytab_group_add = \
                        allow_retrieve_keytab_group or []
                    allow_retrieve_keytab_group_del = []
                    allow_retrieve_keytab_host_add = \
                        allow_retrieve_keytab_host or []
                    allow_retrieve_keytab_host_del = []
                    allow_retrieve_keytab_hostgroup_add = \
                        allow_retrieve_keytab_hostgroup or []
                    allow_retrieve_keytab_hostgroup_del = []
                    dnsrecord_a_add = dnsrecord_args.get("arecord") or []
                    dnsrecord_a_del = []
                    dnsrecord_aaaa_add = dnsrecord_args.get("aaaarecord") or []
                    dnsrecord_aaaa_del = []

                # Remove canonical principal from principal_del
                canonical_principal = "host/" + name + "@" + server_realm
                if canonical_principal in principal_del and \
                   action == "host" and (principal is not None or
                                         canonical_principal not in principal):
                    principal_del.remove(canonical_principal)

                # Remove canonical managedby managedby_host_del for
                # action host if managedby_host is set and the canonical
                # managedby host is not in the managedby_host list.
                canonical_managedby_host = name
                if canonical_managedby_host in managedby_host_del and \
                   action == "host" and (managedby_host is None or
                                         canonical_managedby_host not in
                                         managedby_host):
                    managedby_host_del.remove(canonical_managedby_host)

                # Certificates need to be added and removed one by one,
                # because if entry already exists, the processing of
                # the remaining enries is stopped. The same applies to
                # the removal of non-existing entries.

                # Add certificates
                for _certificate in certificate_add:
                    commands.append([name, "host_add_cert",
                                     {
                                         "usercertificate":
                                         _certificate,
                                     }])
                # Remove certificates
                for _certificate in certificate_del:
                    commands.append([name, "host_remove_cert",
                                     {
                                         "usercertificate":
                                         _certificate,
                                     }])

                # Managedby_Hosts need to be added and removed one by one,
                # because if entry already exists, the processing of
                # the remaining enries is stopped. The same applies to
                # the removal of non-existing entries.

                # Add managedby_hosts
                for _managedby_host in managedby_host_add:
                    commands.append([name, "host_add_managedby",
                                     {
                                         "host":
                                         _managedby_host,
                                     }])
                # Remove managedby_hosts
                for _managedby_host in managedby_host_del:
                    commands.append([name, "host_remove_managedby",
                                     {
                                         "host":
                                         _managedby_host,
                                     }])

                # Principals need to be added and removed one by one,
                # because if entry already exists, the processing of
                # the remaining enries is stopped. The same applies to
                # the removal of non-existing entries.

                # Add principals
                for _principal in principal_add:
                    commands.append([name, "host_add_principal",
                                     {
                                         "krbprincipalname":
                                         _principal,
                                     }])
                # Remove principals
                for _principal in principal_del:
                    commands.append([name, "host_remove_principal",
                                     {
                                         "krbprincipalname":
                                         _principal,
                                     }])

                # Allow create keytab
                if len(allow_create_keytab_user_add) > 0 or \
                   len(allow_create_keytab_group_add) > 0 or \
                   len(allow_create_keytab_host_add) > 0 or \
                   len(allow_create_keytab_hostgroup_add) > 0:
                    commands.append(
                        [name, "host_allow_create_keytab",
                         {
                             "user": allow_create_keytab_user_add,
                             "group": allow_create_keytab_group_add,
                             "host": allow_create_keytab_host_add,
                             "hostgroup": allow_create_keytab_hostgroup_add,
                         }])

                # Disallow create keytab
                if len(allow_create_keytab_user_del) > 0 or \
                   len(allow_create_keytab_group_del) > 0 or \
                   len(allow_create_keytab_host_del) > 0 or \
                   len(allow_create_keytab_hostgroup_del) > 0:
                    commands.append(
                        [name, "host_disallow_create_keytab",
                         {
                             "user": allow_create_keytab_user_del,
                             "group": allow_create_keytab_group_del,
                             "host": allow_create_keytab_host_del,
                             "hostgroup": allow_create_keytab_hostgroup_del,
                         }])

                # Allow retrieve keytab
                if len(allow_retrieve_keytab_user_add) > 0 or \
                   len(allow_retrieve_keytab_group_add) > 0 or \
                   len(allow_retrieve_keytab_host_add) > 0 or \
                   len(allow_retrieve_keytab_hostgroup_add) > 0:
                    commands.append(
                        [name, "host_allow_retrieve_keytab",
                         {
                             "user": allow_retrieve_keytab_user_add,
                             "group": allow_retrieve_keytab_group_add,
                             "host": allow_retrieve_keytab_host_add,
                             "hostgroup": allow_retrieve_keytab_hostgroup_add,
                         }])

                # Disallow retrieve keytab
                if len(allow_retrieve_keytab_user_del) > 0 or \
                   len(allow_retrieve_keytab_group_del) > 0 or \
                   len(allow_retrieve_keytab_host_del) > 0 or \
                   len(allow_retrieve_keytab_hostgroup_del) > 0:
                    commands.append(
                        [name, "host_disallow_retrieve_keytab",
                         {
                             "user": allow_retrieve_keytab_user_del,
                             "group": allow_retrieve_keytab_group_del,
                             "host": allow_retrieve_keytab_host_del,
                             "hostgroup": allow_retrieve_keytab_hostgroup_del,
                         }])

                if len(dnsrecord_a_add) > 0 or len(dnsrecord_aaaa_add) > 0:
                    domain_name = name[name.find(".")+1:]
                    host_name = name[:name.find(".")]

                    _args = {"idnsname": host_name}
                    if len(dnsrecord_a_add) > 0:
                        _args["arecord"] = dnsrecord_a_add
                        if reverse is not None:
                            _args["a_extra_create_reverse"] = reverse
                    if len(dnsrecord_aaaa_add) > 0:
                        _args["aaaarecord"] = dnsrecord_aaaa_add
                        if reverse is not None:
                            _args["aaaa_extra_create_reverse"] = reverse

                    commands.append([domain_name,
                                     "dnsrecord_add", _args])

                if len(dnsrecord_a_del) > 0 or len(dnsrecord_aaaa_del) > 0:
                    domain_name = name[name.find(".")+1:]
                    host_name = name[:name.find(".")]

                    # There seems to be an issue with dnsrecord_del (not
                    # for dnsrecord_add) if aaaarecord is an empty list.
                    # Therefore this is done differently here:
                    _args = {"idnsname": host_name}
                    if len(dnsrecord_a_del) > 0:
                        _args["arecord"] = dnsrecord_a_del
                    if len(dnsrecord_aaaa_del) > 0:
                        _args["aaaarecord"] = dnsrecord_aaaa_del

                    commands.append([domain_name,
                                     "dnsrecord_del", _args])

            elif state == "absent":
                if action == "host":

                    if res_find is not None:
                        args = {}
                        if update_dns is not None:
                            args["updatedns"] = update_dns
                        commands.append([name, "host_del", args])
                else:

                    # Certificates need to be added and removed one by one,
                    # because if entry already exists, the processing of
                    # the remaining enries is stopped. The same applies to
                    # the removal of non-existing entries.

                    # Remove certificates
                    if certificate is not None:
                        for _certificate in certificate:
                            commands.append([name, "host_remove_cert",
                                             {
                                                 "usercertificate":
                                                 _certificate,
                                             }])

                    # Managedby_Hosts need to be added and removed one by one,
                    # because if entry already exists, the processing of
                    # the remaining enries is stopped. The same applies to
                    # the removal of non-existing entries.

                    # Remove managedby_hosts
                    if managedby_host is not None:
                        for _managedby_host in managedby_host:
                            commands.append([name, "host_remove_managedby",
                                             {
                                                 "host":
                                                 _managedby_host,
                                             }])

                    # Principals need to be added and removed one by one,
                    # because if entry already exists, the processing of
                    # the remaining enries is stopped. The same applies to
                    # the removal of non-existing entries.

                    # Remove principals
                    if principal is not None:
                        for _principal in principal:
                            commands.append([name, "host_remove_principal",
                                             {
                                                 "krbprincipalname":
                                                 _principal,
                                             }])

                    # Disallow create keytab
                    if allow_create_keytab_user is not None or \
                       allow_create_keytab_group is not None or \
                       allow_create_keytab_host is not None or \
                       allow_create_keytab_hostgroup is not None:
                        commands.append(
                            [name, "host_disallow_create_keytab",
                             {
                                 "user": allow_create_keytab_user,
                                 "group": allow_create_keytab_group,
                                 "host": allow_create_keytab_host,
                                 "hostgroup": allow_create_keytab_hostgroup,
                             }])

                    # Disallow retrieve keytab
                    if allow_retrieve_keytab_user is not None or \
                       allow_retrieve_keytab_group is not None or \
                       allow_retrieve_keytab_host is not None or \
                       allow_retrieve_keytab_hostgroup is not None:
                        commands.append(
                            [name, "host_disallow_retrieve_keytab",
                             {
                                 "user": allow_retrieve_keytab_user,
                                 "group": allow_retrieve_keytab_group,
                                 "host": allow_retrieve_keytab_host,
                                 "hostgroup": allow_retrieve_keytab_hostgroup,
                             }])

                    dnsrecord_args = gen_dnsrecord_args(ansible_module,
                                                        ip_address, reverse)

                    # Remove arecord and aaaarecord from dnsrecord_args
                    # if the record does not exits in res_find_dnsrecord
                    # to prevent "DNS resource record not found" error
                    if "arecord" in dnsrecord_args \
                       and dnsrecord_args["arecord"] is not None \
                       and len(dnsrecord_args["arecord"]) > 0 \
                       and (res_find_dnsrecord is None
                            or "arecord" not in res_find_dnsrecord):
                        del dnsrecord_args["arecord"]
                    if "aaaarecord" in dnsrecord_args \
                       and dnsrecord_args["aaaarecord"] is not None \
                       and len(dnsrecord_args["aaaarecord"]) > 0 \
                       and (res_find_dnsrecord is None
                            or "aaaarecord" not in res_find_dnsrecord):
                        del dnsrecord_args["aaaarecord"]

                    if "arecord" in dnsrecord_args or \
                       "aaaarecord" in dnsrecord_args:
                        domain_name = name[name.find(".")+1:]
                        host_name = name[:name.find(".")]
                        dnsrecord_args["idnsname"] = host_name

                        commands.append([domain_name, "dnsrecord_del",
                                         dnsrecord_args])

            elif state == "disabled":
                if res_find is not None:
                    commands.append([name, "host_disable", {}])
                else:
                    raise ValueError("No host '%s'" % name)

            else:
                ansible_module.fail_json(msg="Unkown state '%s'" % state)

        del host_set

        # Check mode exit
        if ansible_module.check_mode:
            ansible_module.exit_json(changed=len(commands) > 0, **exit_args)

        # Execute commands

        errors = []
        for name, command, args in commands:
            try:
                result = api_command(ansible_module, command, to_text(name),
                                     args)
                if "completed" in result:
                    if result["completed"] > 0:
                        changed = True
                else:
                    changed = True

                if "random" in args and command in ["host_add", "host_mod"] \
                   and "randompassword" in result["result"]:
                    if len(names) == 1:
                        exit_args["randompassword"] = \
                            result["result"]["randompassword"]
                    else:
                        exit_args.setdefault(name, {})["randompassword"] = \
                            result["result"]["randompassword"]

            except Exception as e:
                msg = str(e)
                if "already contains" in msg \
                   or "does not contain" in msg:
                    continue

                #  The canonical principal name may not be removed
                if "equal to the canonical principal name must" in msg:
                    continue

                # Host is already disabled, ignore error
                if "This entry is already disabled" in msg:
                    continue

                # Ignore no modification error.
                if "no modifications to be performed" in msg:
                    continue

                ansible_module.fail_json(msg="%s: %s: %s" % (command, name,
                                                             msg))

            # Get all errors
            # All "already a member" and "not a member" failures in the
            # result are ignored. All others are reported.
            if "failed" in result and len(result["failed"]) > 0:
                for item in result["failed"]:
                    failed_item = result["failed"][item]
                    for member_type in failed_item:
                        for member, failure in failed_item[member_type]:
                            if "already a member" in failure \
                               or "not a member" in failure:
                                continue
                            errors.append("%s: %s %s: %s" % (
                                command, member_type, member, failure))

        if len(errors) > 0:
            ansible_module.fail_json(msg=", ".join(errors))

    except Exception as e:
        ansible_module.fail_json(msg=str(e))

    finally:
        temp_kdestroy(ccache_dir, ccache_name)

    # Done

    ansible_module.exit_json(changed=changed, host=exit_args)


if __name__ == "__main__":
    main()
