From 4fc722f73bd724227d788c45dc1b634ee4f23bf4 Mon Sep 17 00:00:00 2001 From: Thomas Woerner <twoerner@redhat.com> Date: Mon, 9 Sep 2019 23:24:34 +0200 Subject: [PATCH] New host management module There is a new user management module placed in the plugins folder: plugins/modules/ipauser.py The host module allows to add, remove and disable hosts. The host module is as compatible as possible to the Ansible upstream ipa_host` module, but addtionally offers to disable hosts. Here is the documentation for the module: README-host.md New example playbooks have been added: playbooks/host/add-host.yml playbooks/host/delete-host.yml playbooks/host/disable-host.yml --- README-host.md | 173 ++++++++++++++ README.md | 4 +- playbooks/host/add-host.yml | 20 ++ playbooks/host/delete-host.yml | 11 + playbooks/host/disable-host.yml | 11 + plugins/modules/ipahost.py | 400 ++++++++++++++++++++++++++++++++ 6 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 README-host.md create mode 100644 playbooks/host/add-host.yml create mode 100644 playbooks/host/delete-host.yml create mode 100644 playbooks/host/disable-host.yml create mode 100644 plugins/modules/ipahost.py diff --git a/README-host.md b/README-host.md new file mode 100644 index 00000000..ef4733ca --- /dev/null +++ b/README-host.md @@ -0,0 +1,173 @@ +Host module +=========== + +Description +----------- + +The host module allows to add, remove, and disable hosts. + +The host module is as compatible as possible to the Ansible upstream `ipa_host` module, but addtionally offers to disable hosts. + + +Features +-------- +* Host management + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.4.0 and up are supported by the ipahost module. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.8+ + +**Node** +* Supported FreeIPA version (see above) + + +Usage +===== + +Example inventory file + +```ini +[ipaserver] +ipaserver.test.local +``` + + +Example playbook to add hosts: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is present + - ipahost: + ipaadmin_password: MyPassword123 + 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 +``` + + +Example playbook to create host without DNS: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is present without DNS + - ipahost: + ipaadmin_password: MyPassword123 + name: host02.example.com + description: Example host + force: yes +``` + + +Example playbook to initiate the generation of a random password to be used in bulk enrollment: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Generate a random password for bulk enrolment + - ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + random: yes +``` + + +Example playbook to disable a host: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is disabled + - ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + update_dns: yes + state: disabled +``` +`update_dns` controls if the DNS entries will be updated. + + +Example playbook to ensure a host is absent: + +```yaml +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + # Ensure host is absent + - ipahost: + ipaadmin_password: password1 + name: host01.example.com + state: absent +``` + + +Variables +========= + +ipahost +------- + +Variable | Description | Required +-------- | ----------- | -------- +`ipaadmin_principal` | The admin principal is a string and defaults to `admin` | no +`ipaadmin_password` | The admin password is a string and is required if there is no admin ticket available on the node | no +`name` \| `fqdn` | The list of host name strings. | yes +`description` | The host description. | no +`locality` | Host locality (e.g. "Baltimore, MD"). | no +`location` \| `ns_host_location` | Host location (e.g. "Lab 2"). | no +`platform` \| `ns_hardware_platform` | Host hardware platform (e.g. "Lenovo T61"). | no +`os` \| `ns_os_version` | Host operating system and version (e.g. "Fedora 9"). | no +`password` \| `user_password` \| `userpassword` | Password used in bulk enrollment. | no +`random` \| `random_password` | Initiate the generation of a random password to be used in bulk enrollment. | no +`mac_address` \| `macaddress` | List of hardware MAC addresses. | no +`force` | Force host name even if not in DNS. | no +`reverse` | Reverse DNS detection. | no +`ip_address` \| `ipaddress` | The host IP address. | no +`update_dns` | Update DNS entries. | no +`update_password` | Set password for a host in present state only on creation or always. It can be one of `always` or `on_create` and defaults to `always`. | no +`state` | The state to ensure. It can be one of `present`, `absent` or `disabled`, default: `present`. | yes + + +Authors +======= + +Thomas Woerner diff --git a/README.md b/README.md index d377c5e4..35dfdde8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ FreeIPA Ansible collection ========================== -This repository contains [Ansible](https://www.ansible.com/) roles and playbooks to install and uninstall [FreeIPA](https://www.freeipa.org/) `servers`, `replicas` and `clients`. Also modules for group, topology and user management. +This repository contains [Ansible](https://www.ansible.com/) roles and playbooks to install and uninstall [FreeIPA](https://www.freeipa.org/) `servers`, `replicas` and `clients`. Also modules for group, host, topology and user management. **Note**: The ansible playbooks and roles require a configured ansible environment where the ansible nodes are reachable and are properly set up to have an IP address and a working package manager. @@ -12,6 +12,7 @@ Features * One-time-password (OTP) support for client installation * Repair mode for clients * Modules for group management +* Modules for host management * Modules for topology management * Modules for user management @@ -387,6 +388,7 @@ Modules in plugin/modules ========================= * [ipagroup](README-group.md) +* [ipahost](README-host.md) * [ipatopologysegment](README-topology.md) * [ipatopologysuffix](README-topology.md) * [ipauser](README-user.md) diff --git a/playbooks/host/add-host.yml b/playbooks/host/add-host.yml new file mode 100644 index 00000000..61b8a958 --- /dev/null +++ b/playbooks/host/add-host.yml @@ -0,0 +1,20 @@ +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + - name: Ensure host is present + ipahost: + ipaadmin_password: MyPassword123 + 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 diff --git a/playbooks/host/delete-host.yml b/playbooks/host/delete-host.yml new file mode 100644 index 00000000..30eaf3ef --- /dev/null +++ b/playbooks/host/delete-host.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + - name: Ensure host host01.example.com is absent + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + state: absent diff --git a/playbooks/host/disable-host.yml b/playbooks/host/disable-host.yml new file mode 100644 index 00000000..3e265fe2 --- /dev/null +++ b/playbooks/host/disable-host.yml @@ -0,0 +1,11 @@ +--- +- name: Playbook to handle hosts + hosts: ipaserver + become: true + + tasks: + - name: Disable host host01.example.com + ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + state: disabled diff --git a/plugins/modules/ipahost.py b/plugins/modules/ipahost.py new file mode 100644 index 00000000..952e5442 --- /dev/null +++ b/plugins/modules/ipahost.py @@ -0,0 +1,400 @@ +#!/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 + 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 + mac_address: + description: List of hardware MAC addresses. + type: list + aliases: ["macaddress"] + 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 + aliases: ["ipaddress"] + required: false + update_dns: + description: Update DNS entries + required: false + update_password: + description: + Set password for a host in present state only on creation or always + default: 'always' + choices: ["always", "on_create"] + state: + description: State to ensure + default: present + choices: ["present", "absent", + "disabled"] +author: + - Thomas Woerner +""" + +EXAMPLES = """ +# Ensure host is present +- ipahost: + ipaadmin_password: MyPassword123 + 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: MyPassword123 + name: host02.example.com + description: Example host + force: yes + +# Initiate generation of a random password for the host +- ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + description: Example host + ip_address: 192.168.0.123 + random: yes + +# Ensure host is disabled +- ipahost: + ipaadmin_password: MyPassword123 + name: host01.example.com + update_dns: yes + state: disabled + +# Ensure host is absent +- ipahost: + ipaadmin_password: password1 + name: host01.example.com + state: absent +""" + +RETURN = """ +""" + +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 + + +def find_host(module, name): + _args = { + "all": True, + "fqdn": to_text(name), + } + + _result = api_command(module, "host_find", to_text(name), _args) + + if len(_result["result"]) > 1: + module.fail_json( + msg="There is more than one host '%s'" % (name)) + elif len(_result["result"]) == 1: + return _result["result"][0] + else: + return None + + +def show_host(module, name): + _result = api_command(module, "host_show", to_text(name), {}) + return _result["result"] + + +def gen_args(description, force, locality, location, platform, os, password, + random, mac_address, ip_address, update_dns, reverse): + _args = {} + if description is not None: + _args["description"] = description + if force is not None: + _args["force"] = force + 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 ip_address is not None: + _args["ip_address"] = ip_address + if update_dns is not None: + _args["updatedns"] = update_dns + if reverse is not None: + _args["no_reverse"] = not reverse + + return _args + + +def main(): + 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=True), + # 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 (usercertificate) + mac_address=dict(type="list", aliases=["macaddress"], + default=None), + # sshpubkey=dict(type="str", aliases=["ipasshpubkey"], + # default=None), + # class + # auth_ind + # requires_pre_auth + # ok_as_delegate + # ok_to_auth_as_delegate + force=dict(type='bool', default=None), + reverse=dict(type='bool', default=True), + ip_address=dict(type="str", aliases=["ipaddress"], + default=None), + # no_members + + # for update: + # krbprincipalname + update_dns=dict(type="bool", aliases=["updatedns"], + default=None), + update_password=dict(type='str', default=None, + choices=['always', 'on_create']), + # absent + # continue + + # disabled + + # state + state=dict(type="str", default="present", + choices=["present", "absent", "disabled"]), + ), + supports_check_mode=True, + ) + + ansible_module._ansible_debug = True + + # Get parameters + + # general + ipaadmin_principal = ansible_module.params.get("ipaadmin_principal") + ipaadmin_password = ansible_module.params.get("ipaadmin_password") + names = ansible_module.params.get("name") + + # present + description = ansible_module.params.get("description") + locality = ansible_module.params.get("locality") + location = ansible_module.params.get("location") + platform = ansible_module.params.get("platform") + os = ansible_module.params.get("os") + password = ansible_module.params.get("password") + random = ansible_module.params.get("random") + mac_address = ansible_module.params.get("mac_address") + force = ansible_module.params.get("force") + reverse = ansible_module.params.get("reverse") + ip_address = ansible_module.params.get("ip_address") + update_dns = ansible_module.params.get("update_dns") + update_password = ansible_module.params.get("update_password") + # absent + # disabled + # state + state = ansible_module.params.get("state") + + # Check parameters + + if state == "present": + if len(names) != 1: + ansible_module.fail_json( + msg="Only one host can be added at a time.") + + if state == "absent": + if len(names) < 1: + ansible_module.fail_json( + msg="No name given.") + for x in ["description", "password", "random", "mac_address", + "force", "ip_address", "update_password"]: + if vars()[x] is not None: + ansible_module.fail_json( + msg="Argument '%s' can not be used with state '%s'" % + (x, state)) + + if update_password is None: + update_password = "always" + + # 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() + + commands = [] + + for name in names: + # Make sure host exists + res_find = find_host(ansible_module, name) + + # Create command + if state == "present": + # Generate args + args = gen_args( + description, force, locality, location, platform, os, + password, random, mac_address, ip_address, update_dns, + reverse) + + # Found the host + if res_find is not None: + # Ignore password with update_password == on_create + if update_password == "on_create" and \ + "userpassword" in args: + del args["userpassword"] + + # Ignore force, ip_address and no_reverse for mod + for x in ["force", "ip_address", "no_reverse"]: + if x in args: + del args[x] + + # 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]) + else: + commands.append([name, "host_add", args]) + + elif state == "absent": + if res_find is not None: + commands.append([name, "host_del", {}]) + + elif state == "disabled": + if res_find is not None: + res_show = show_host(ansible_module, name) + if res_show["has_keytab"]: + commands.append([name, "host_disable", {}]) + else: + raise ValueError("No host '%s'" % name) + + else: + ansible_module.fail_json(msg="Unkown state '%s'" % state) + + # Execute commands + for name, command, args in commands: + try: + api_command(ansible_module, command, to_text(name), args) + changed = True + except Exception as e: + ansible_module.fail_json(msg="%s: %s: %s" % (command, name, + str(e))) + + 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, **exit_args) + + +if __name__ == "__main__": + main() -- GitLab