#!/usr/bin/python # Copyright (c) 2017, Ansible Project # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later from __future__ import annotations DOCUMENTATION = r""" module: ipa_host author: Thomas Krahn (@Nosmoht) short_description: Manage FreeIPA host description: - Add, modify and delete an IPA host using IPA API. attributes: check_mode: support: full diff_mode: support: none options: fqdn: description: - Full qualified domain name. - Can not be changed as it is the unique identifier. required: true aliases: ["name"] type: str description: description: - A description of this host. type: str userclass: description: - Host category (semantics placed on this attribute are for local interpretation). type: str version_added: 12.0.0 force: description: - Force host name even if not in DNS. required: false type: bool ip_address: description: - Add the host to DNS with this IP address. type: str mac_address: description: - List of Hardware MAC address(es) off this host. - If option is omitted MAC addresses are not checked nor changed. - If an empty list is passed all assigned MAC addresses are removed. - MAC addresses that are already assigned but not passed are removed. aliases: ["macaddress"] type: list elements: str l: description: - Host locality (for example V(Baltimore, MD)). aliases: ["locality"] type: str version_added: 12.0.0 ns_host_location: description: - Host location (for example V(Lab 2)). aliases: ["nshostlocation"] type: str ns_hardware_platform: description: - Host hardware platform (for example V(Lenovo T61")). aliases: ["nshardwareplatform"] type: str ns_os_version: description: - Host operating system and version (for example V(Fedora 9)). aliases: ["nsosversion"] type: str user_certificate: description: - List of Base-64 encoded server certificates. - If option is omitted certificates are not checked nor changed. - If an empty list is passed all assigned certificates are removed. - Certificates already assigned but not passed are removed. aliases: ["usercertificate"] type: list elements: str state: description: - State to ensure. default: present choices: ["absent", "disabled", "enabled", "present"] type: str force_creation: description: - Create host if O(state=disabled) or O(state=enabled) but not present. default: true type: bool version_added: 9.5.0 update_dns: description: - If set V(true) with O(state=absent), then removes DNS records of the host managed by FreeIPA DNS. - This option has no effect for states other than V(absent). type: bool random_password: description: Generate a random password to be used in bulk enrollment. type: bool extends_documentation_fragment: - community.general.ipa.documentation - community.general.ipa.connection_notes - community.general.attributes """ EXAMPLES = r""" - name: Ensure host is present community.general.ipa_host: name: host01.example.com description: Example host userclass: Server ip_address: 192.168.0.123 locality: Baltimore, MD 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 ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Generate a random password for bulk enrolment community.general.ipa_host: name: host01.example.com description: Example host ip_address: 192.168.0.123 state: present ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret random_password: true - name: Ensure host is disabled community.general.ipa_host: name: host01.example.com state: disabled ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Ensure that all user certificates are removed community.general.ipa_host: name: host01.example.com user_certificate: [] ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Ensure host is absent community.general.ipa_host: name: host01.example.com state: absent ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret - name: Ensure host and its DNS record is absent community.general.ipa_host: name: host01.example.com state: absent ipa_host: ipa.example.com ipa_user: admin ipa_pass: topsecret update_dns: true """ RETURN = r""" host: description: Host as returned by IPA API. returned: always type: dict host_diff: description: List of options that differ and would be changed. returned: if check mode and a difference is found type: list """ import traceback from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec from ansible.module_utils.common.text.converters import to_native class HostIPAClient(IPAClient): def __init__(self, module, host, port, protocol): super().__init__(module, host, port, protocol) def host_show(self, name): return self._post_json(method="host_show", name=name) def host_find(self, name): return self._post_json(method="host_find", name=None, item={"all": True, "fqdn": name}) def host_add(self, name, host): return self._post_json(method="host_add", name=name, item=host) def host_mod(self, name, host): return self._post_json(method="host_mod", name=name, item=host) def host_del(self, name, update_dns): return self._post_json(method="host_del", name=name, item={"updatedns": update_dns}) def host_disable(self, name): return self._post_json(method="host_disable", name=name) def get_host_dict( description=None, userclass=None, force=None, ip_address=None, l=None, ns_host_location=None, ns_hardware_platform=None, ns_os_version=None, user_certificate=None, mac_address=None, random_password=None, ): data = {} if description is not None: data["description"] = description if userclass is not None: data["userclass"] = userclass if force is not None: data["force"] = force if ip_address is not None: data["ip_address"] = ip_address if l is not None: data["l"] = l if ns_host_location is not None: data["nshostlocation"] = ns_host_location if ns_hardware_platform is not None: data["nshardwareplatform"] = ns_hardware_platform if ns_os_version is not None: data["nsosversion"] = ns_os_version if user_certificate is not None: data["usercertificate"] = [{"__base64__": item} for item in user_certificate] if mac_address is not None: data["macaddress"] = mac_address if random_password is not None: data["random"] = random_password return data def get_host_diff(client, ipa_host, module_host): non_updateable_keys = ["force", "ip_address"] if not module_host.get("random"): non_updateable_keys.append("random") for key in non_updateable_keys: if key in module_host: del module_host[key] return client.get_diff(ipa_data=ipa_host, module_data=module_host) def ensure(module, client): name = module.params["fqdn"] state = module.params["state"] force_creation = module.params["force_creation"] ipa_host = client.host_find(name=name) module_host = get_host_dict( description=module.params["description"], userclass=module.params["userclass"], force=module.params["force"], ip_address=module.params["ip_address"], l=module.params["l"], ns_host_location=module.params["ns_host_location"], ns_hardware_platform=module.params["ns_hardware_platform"], ns_os_version=module.params["ns_os_version"], user_certificate=module.params["user_certificate"], mac_address=module.params["mac_address"], random_password=module.params["random_password"], ) changed = False if state in ["present", "enabled", "disabled"]: if not ipa_host and (force_creation or state == "present"): changed = True if not module.check_mode: # OTP password generated by FreeIPA is visible only for host_add command # so, return directly from here. return changed, client.host_add(name=name, host=module_host) else: if state in ["disabled", "enabled"]: module.fail_json(msg=f"No host with name {ipa_host} found") diff = get_host_diff(client, ipa_host, module_host) if len(diff) > 0: changed = True if not module.check_mode: data = {} for key in diff: data[key] = module_host.get(key) if "usercertificate" not in data: data["usercertificate"] = [cert["__base64__"] for cert in ipa_host.get("usercertificate", [])] ipa_host_show = client.host_show(name=name) if ipa_host_show.get("has_keytab", True) and ( state == "disabled" or module.params.get("random_password") ): client.host_disable(name=name) return changed, client.host_mod(name=name, host=data) elif state == "absent": if ipa_host: changed = True update_dns = module.params.get("update_dns", False) if not module.check_mode: client.host_del(name=name, update_dns=update_dns) return changed, client.host_find(name=name) def main(): argument_spec = ipa_argument_spec() argument_spec.update( description=dict(type="str"), fqdn=dict(type="str", required=True, aliases=["name"]), force=dict(type="bool"), ip_address=dict(type="str"), l=dict(type="str", aliases=["locality"]), ns_host_location=dict(type="str", aliases=["nshostlocation"]), ns_hardware_platform=dict(type="str", aliases=["nshardwareplatform"]), ns_os_version=dict(type="str", aliases=["nsosversion"]), userclass=dict(type="str"), user_certificate=dict(type="list", aliases=["usercertificate"], elements="str"), mac_address=dict(type="list", aliases=["macaddress"], elements="str"), update_dns=dict(type="bool"), state=dict(type="str", default="present", choices=["present", "absent", "enabled", "disabled"]), random_password=dict(type="bool", no_log=False), force_creation=dict(type="bool", default=True), ) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) client = HostIPAClient( module=module, host=module.params["ipa_host"], port=module.params["ipa_port"], protocol=module.params["ipa_prot"], ) try: client.login(username=module.params["ipa_user"], password=module.params["ipa_pass"]) changed, host = ensure(module, client) module.exit_json(changed=changed, host=host) except Exception as e: module.fail_json(msg=to_native(e), exception=traceback.format_exc()) if __name__ == "__main__": main()