From a3517a3a2301ba92333b8435246c801fb9d25797 Mon Sep 17 00:00:00 2001 From: Thomas Woerner <twoerner@redhat.com> Date: Fri, 3 May 2024 17:29:54 +0200 Subject: [PATCH] New inventory plugin The inventory plugin compiles a dynamic inventory from IPA domain, filters servers by role(s). Usage: Create yml file, for example `freeipa.yml`: --- plugin: freeipa server: server.ipa.local ipaadmin_password: SomeADMINpassword verify: ca.crt Get compiled inventory: ansible-inventory -i freeipa.yml --graph --- README-inventory-plugin-freeipa.md | 106 +++++++++++++++++ README.md | 13 ++- plugins/inventory/freeipa.py | 180 +++++++++++++++++++++++++++++ utils/build-galaxy-release.sh | 8 ++ 4 files changed, 304 insertions(+), 3 deletions(-) create mode 100644 README-inventory-plugin-freeipa.md create mode 100644 plugins/inventory/freeipa.py diff --git a/README-inventory-plugin-freeipa.md b/README-inventory-plugin-freeipa.md new file mode 100644 index 00000000..ec8c2e63 --- /dev/null +++ b/README-inventory-plugin-freeipa.md @@ -0,0 +1,106 @@ +Inventory plugin +================ + +Description +----------- + + +The inventory plugin compiles a dynamic inventory from IPA domain. The servers can be filtered by their role(s). + +This plugin is using the Python requests binding, that is only available for Python 3.7 and up. + + +Features +-------- +* Dynamic inventory + + +Supported FreeIPA Versions +-------------------------- + +FreeIPA versions 4.6.0 and up are supported by the inventory plugin. + + +Requirements +------------ + +**Controller** +* Ansible version: 2.13+ + +**Node** +* Supported FreeIPA version (see above) + + +Configuration +============= + +The inventory plugin is automatically enabled from the Ansible collection or from the top directory of the git repo if the `plugins` folder is linked to `~/.ansible`. + +If `ansible.cfg` was modified to point to the roles and modules with `roles_path`, `library` and `module_utils` tag, then it is needed to set `inventory_plugins` also: + +``` +inventory_plugins = /my/dir/ansible-freeipa/plugins/inventory +``` + +Usage +===== + +Example inventory file "freeipa.yml": + +```yml +--- +plugin: freeipa +server: server.ipa.local +ipaadmin_password: SomeADMINpassword +``` + +Example inventory file "freeipa.yml" with server TLS certificate verification using local copy of `/etc/ipa/ca.crt` from the server: + +```yml +--- +plugin: freeipa +server: server.ipa.local +ipaadmin_password: SomeADMINpassword +verify: ca.crt +``` + + +How to use the plugin +--------------------- + +With the `ansible-inventory` command it is possible to show the generated inventorey: + +```bash +ansible-inventory -v -i freeipa.yml --graph +``` + +Example inventory file "freeipa.yml" for use with `playbooks/config/retrieve-config.yml`: + +```yml +--- +plugin: freeipa +server: server.ipa.local +ipaadmin_password: SomeADMINpassword +inventory_group: ipaserver +``` + +```bash +ansible-playbook -u root -i ipa.yml playbooks/config/retrieve-config.yml +``` + +Variables +========= + +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 +`server` | The FQDN of server to start the scan. (string) | yes +`verify` | The server TLS certificate file for verification (/etc/ipa/ca.crt). Turned off if not set. (string) | yes +`role` | The role(s) of the server. If several roles are given, only servers that have all the roles are returned. (list of strings) (choices: "IPA master", "CA server", "KRA server", "DNS server", "AD trust controller", "AD trust agent") | no +`inventory_group` | The inventory group to create. The default group name is "ipaservers". | no + +Authors +======= + +- Thomas Woerner diff --git a/README.md b/README.md index 7f0c9fc4..ab7d4e31 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Features * Repair mode for clients * Backup and restore, also to and from controller * Smartcard setup for servers and clients +* Inventory plugin freeipa * Modules for automembership rule management * Modules for automount key management * Modules for automount location management @@ -108,9 +109,10 @@ You can use the roles directly within the top directory of the git repo, but to You can either adapt ansible.cfg: ``` -roles_path = /my/dir/ansible-freeipa/roles -library = /my/dir/ansible-freeipa/plugins/modules -module_utils = /my/dir/ansible-freeipa/plugins/module_utils +roles_path = /my/dir/ansible-freeipa/roles +library = /my/dir/ansible-freeipa/plugins/modules +module_utils = /my/dir/ansible-freeipa/plugins/module_utils +inventory_plugins = /my/dir/ansible-freeipa/plugins/inventory ``` Or you can link the directories: @@ -470,3 +472,8 @@ Modules in plugin/modules * [ipavault](README-vault.md) If you want to write a new module please read [writing a new module](plugins/modules/README.md). + +Inventory plugins in plugin/inventory +===================================== + +* [freeipa](README-inventory-plugin-freeipa.md) diff --git a/plugins/inventory/freeipa.py b/plugins/inventory/freeipa.py new file mode 100644 index 00000000..887670d9 --- /dev/null +++ b/plugins/inventory/freeipa.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +# Authors: +# Thomas Woerner <twoerner@redhat.com> +# +# Copyright (C) 2024 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/>. + +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +ANSIBLE_METADATA = { + "metadata_version": "1.0", + "supported_by": "community", + "status": ["preview"], +} + +DOCUMENTATION = """ +--- +name: freeipa +plugin_type: inventory +version_added: "1.13" +short_description: Compiles a dynamic inventory from IPA domain +description: | + Compiles a dynamic inventory from IPA domain, filters servers by role(s). +options: + plugin: + description: Marks this as an instance of the "freeipa" plugin. + required: True + choices: ["freeipa"] + ipaadmin_principal: + description: The admin principal. + default: admin + type: str + ipaadmin_password: + description: The admin password. + required: true + type: str + server: + description: FQDN of server to start the scan. + type: str + required: true + verify: + description: | + The server TLS certificate file for verification (/etc/ipa/ca.crt). + Turned off if not set. + type: str + required: false + role: + description: | + The role(s) of the server. If several roles are given, only servers + that have all the roles are returned. + type: list + elements: str + choices: ["IPA master", "CA server", "KRA server", "DNS server", + "AD trust controller", "AD trust agent"] + required: false + inventory_group: + description: | + The inventory group to create. The default group name is "ipaservers". + type: str + default: ipaservers +author: + - Thomas Woerner (@t-woerner) +""" + +EXAMPLES = """ +# inventory.config file in YAML format +plugin: freeipa +server: ipaserver-01.ipa.local +ipaadmin_password: SomeADMINpassword + +# inventory.config file in YAML format with server TLS certificate verification +plugin: freeipa +server: ipaserver-01.ipa.local +ipaadmin_password: SomeADMINpassword +verify: ca.crt +""" + +import os +import requests +try: + from requests.packages import urllib3 +except ImportError: + import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +from ansible import constants +from ansible.errors import AnsibleParserError +from ansible.module_utils.common.text.converters import to_native +from ansible.plugins.inventory import BaseInventoryPlugin +from ansible.module_utils.six.moves.urllib.parse import quote + + +class InventoryModule(BaseInventoryPlugin): + + NAME = 'freeipa' + + def verify_file(self, path): + # pylint: disable=super-with-arguments + if super(InventoryModule, self).verify_file(path): + _name, ext = os.path.splitext(path) + if ext in constants.YAML_FILENAME_EXTENSIONS: + return True + return False + + def parse(self, inventory, loader, path, cache=False): + # pylint: disable=super-with-arguments + super(InventoryModule, self).parse(inventory, loader, path, + cache=cache) + self._read_config_data(path) # This also loads the cache + + self.get_option("plugin") + ipaadmin_principal = self.get_option("ipaadmin_principal") + ipaadmin_password = self.get_option("ipaadmin_password") + server = self.get_option("server") + verify = self.get_option("verify") + role = self.get_option("role") + inventory_group = self.get_option("inventory_group") + + if verify is not None: + if not os.path.exists(verify): + raise AnsibleParserError("ERROR: Could not load %s" % verify) + else: + verify = False + + self.inventory.add_group(inventory_group) + + ipa_url = "https://%s/ipa" % server + + s = requests.Session() + s.headers.update({"referer": ipa_url}) + s.headers.update({"Content-Type": + "application/x-www-form-urlencoded"}) + s.headers.update({"Accept": "text/plain"}) + + data = 'user=%s&password=%s' % (quote(ipaadmin_principal, safe=''), + quote(ipaadmin_password, safe='')) + response = s.post("%s/session/login_password" % ipa_url, + data=data, verify=verify) + + # Now use json API + s.headers.update({"Content-Type": "application/json"}) + + kw_args = {} + if role is not None: + kw_args["servrole"] = role + json_data = { + "method" : "server_find", + "params": [[], kw_args], + "id": 0 + } + response = s.post("%s/session/json" % ipa_url, json=json_data, + verify=verify) + json_res = response.json() + + error = json_res.get("error") + if error is not None: + raise AnsibleParserError("ERROR: %s" % to_native(error)) + + if "result" in json_res and "result" in json_res["result"]: + res = json_res["result"].get("result") + if isinstance(res, list): + for server in res: + self.inventory.add_host(server["cn"][0], + group=inventory_group) diff --git a/utils/build-galaxy-release.sh b/utils/build-galaxy-release.sh index a1681e05..bd6248cd 100755 --- a/utils/build-galaxy-release.sh +++ b/utils/build-galaxy-release.sh @@ -125,6 +125,7 @@ sed -i -e "s/namespace: .*/namespace: \"$namespace\"/" galaxy.yml sed -i -e "s/name: .*/name: \"$name\"/" galaxy.yml find . -name "*~" -exec rm {} \; +find . -name "__py*__" -exec rm -rf {} \; if [ $offline != 1 ]; then @@ -155,6 +156,13 @@ python utils/create_action_group.py "meta/runtime.yml" "$collection_prefix" # ln -sf ../../roles/*/action_plugins/*.py . #}) +# Adapt inventory plugin and inventory plugin README +echo "Fixing inventory plugin and doc..." +sed -i -e "s/plugin: freeipa/plugin: ${collection_prefix}.freeipa/g" plugins/inventory/freeipa.py +sed -i -e "s/choices: \[\"freeipa\"\]/choices: \[\"${collection_prefix}.freeipa\"\]/g" plugins/inventory/freeipa.py +sed -i -e "s/plugin: freeipa/plugin: ${collection_prefix}.freeipa/g" README-inventory-plugin-freeipa.md +echo -e "\033[AFixing inventory plugin and doc... \033[32;1mDONE\033[0m" + for doc_fragment in plugins/doc_fragments/*.py; do fragment=$(basename -s .py "$doc_fragment") -- GitLab