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