Skip to content
Snippets Groups Projects
Select Git revision
  • e7b9e97a84dd446380059b145138967c2ffcaf6e
  • master default protected
  • v1.14.7
  • v1.14.6
  • v1.14.5
  • v1.14.4
  • v1.14.3
  • v1.14.2
  • v1.14.1
  • v1.14.0
  • v1.13.2
  • v1.13.1
  • v1.13.0
  • v1.12.1
  • v1.12.0
  • v1.11.1
  • v1.11.0
  • v1.10.0
  • v1.9.2
  • v1.9.1
  • v1.9.0
  • v1.8.4
22 results

ipapwpolicy.py

Blame
  • ipahost.py 59.87 KiB
    #!/usr/bin/python
    # -*- 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(
            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) or isinstance(host, 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()