File implement-multiple-inventory-for-ansible.targets.patch of Package salt

From f60cc567c1c3d849d14fa547e87ca369bbbe1d2b Mon Sep 17 00:00:00 2001
From: Victor Zhestkov <vzhestkov@suse.com>
Date: Mon, 10 Mar 2025 13:25:56 +0100
Subject: [PATCH] Implement multiple inventory for `ansible.targets`

* Implement multiple inventory for ansible.targets

* Add tests for multiple inventories with ansible.targets
---
 salt/modules/ansiblegate.py                   |  10 +-
 salt/utils/ansible.py                         |  61 ++++++---
 .../pytests/unit/modules/test_ansiblegate.py  | 119 ++++++++++++++++++
 3 files changed, 169 insertions(+), 21 deletions(-)

diff --git a/salt/modules/ansiblegate.py b/salt/modules/ansiblegate.py
index 920c374e5a..9dd878665f 100644
--- a/salt/modules/ansiblegate.py
+++ b/salt/modules/ansiblegate.py
@@ -423,7 +423,7 @@ def playbooks(
     return retdata
 
 
-def targets(inventory="/etc/ansible/hosts", yaml=False, export=False):
+def targets(inventory=None, inventories=None, yaml=False, export=False):
     """
     .. versionadded:: 3005
 
@@ -432,6 +432,10 @@ def targets(inventory="/etc/ansible/hosts", yaml=False, export=False):
     :param inventory:
         The inventory file to read the inventory from. Default: "/etc/ansible/hosts"
 
+    :param inventories:
+        The list of inventory files to read the inventory from.
+        Uses `inventory` in case if `inventories` is not specified.
+
     :param yaml:
         Return the inventory as yaml output. Default: False
 
@@ -446,7 +450,9 @@ def targets(inventory="/etc/ansible/hosts", yaml=False, export=False):
         salt 'ansiblehost' ansible.targets inventory=my_custom_inventory
 
     """
-    return salt.utils.ansible.targets(inventory=inventory, yaml=yaml, export=export)
+    return salt.utils.ansible.targets(
+        inventory=inventory, inventories=inventories, yaml=yaml, export=export
+    )
 
 
 def discover_playbooks(
diff --git a/salt/utils/ansible.py b/salt/utils/ansible.py
index b91c931dff..2c85da4753 100644
--- a/salt/utils/ansible.py
+++ b/salt/utils/ansible.py
@@ -18,32 +18,55 @@ def __virtual__():  # pylint: disable=expected-2-blank-lines-found-0
     return (False, "Install `ansible` to use inventory")
 
 
-def targets(inventory="/etc/ansible/hosts", yaml=False, export=False):
+def targets(inventory=None, inventories=None, yaml=False, export=False):
     """
     Return the targets from the ansible inventory_file
     Default: /etc/salt/roster
     """
-    if not os.path.isfile(inventory):
-        raise CommandExecutionError("Inventory file not found: {}".format(inventory))
-    if not os.path.isabs(inventory):
-        raise CommandExecutionError("Path to inventory file must be an absolute path")
+
+    if inventory is None and inventories is None:
+        inventory = "/etc/ansible/hosts"
+    multi_inventory = True
+    if not isinstance(inventories, list):
+        multi_inventory = False
+        inventories = []
+    if inventory is not None and inventory not in inventories:
+        inventories.append(inventory)
 
     extra_cmd = []
     if export:
         extra_cmd.append("--export")
     if yaml:
         extra_cmd.append("--yaml")
-    inv = salt.modules.cmdmod.run(
-        "ansible-inventory -i {} --list {}".format(inventory, " ".join(extra_cmd)),
-        env={"ANSIBLE_DEPRECATION_WARNINGS": "0"},
-        reset_system_locale=False,
-    )
-    if yaml:
-        return salt.utils.stringutils.to_str(inv)
-    else:
-        try:
-            return salt.utils.json.loads(salt.utils.stringutils.to_str(inv))
-        except ValueError:
-            raise CommandExecutionError(
-                "Error processing the inventory: {}".format(inv)
-            )
+
+    ret = {}
+
+    for inventory in inventories:
+        if not os.path.isfile(inventory):
+            raise CommandExecutionError("Inventory file not found: {}".format(inventory))
+        if not os.path.isabs(inventory):
+            raise CommandExecutionError("Path to inventory file must be an absolute path")
+
+        inv = salt.modules.cmdmod.run(
+            "ansible-inventory -i {} --list {}".format(inventory, " ".join(extra_cmd)),
+            env={"ANSIBLE_DEPRECATION_WARNINGS": "0"},
+            reset_system_locale=False,
+        )
+
+        if yaml:
+            inv = salt.utils.stringutils.to_str(inv)
+        else:
+            try:
+                inv = salt.utils.json.loads(salt.utils.stringutils.to_str(inv))
+            except ValueError:
+                raise CommandExecutionError(
+                    "Error processing the inventory {}: {}".format(inventory, inv)
+                )
+
+        if not multi_inventory:
+            ret = inv
+            break
+
+        ret[inventory] = inv
+
+    return ret
diff --git a/tests/pytests/unit/modules/test_ansiblegate.py b/tests/pytests/unit/modules/test_ansiblegate.py
index 272da721bf..d8bdd1140e 100644
--- a/tests/pytests/unit/modules/test_ansiblegate.py
+++ b/tests/pytests/unit/modules/test_ansiblegate.py
@@ -189,6 +189,125 @@ def test_ansible_targets(minion_opts):
             assert len(ret["ungrouped"]["hosts"]) == 2
 
 
+def test_ansible_targets_multiple_inventories(minion_opts):
+    """
+    Test ansible.targets execution module function with multiple inventories.
+    :return:
+    """
+    ansible_inventory1_ret = """
+{
+    "_meta": {
+        "hostvars": {
+            "uyuni-stable-ansible-centos7-1.tf.local": {
+                "ansible_ssh_private_key_file": "/etc/ansible/my_ansible_private_key"
+            },
+            "uyuni-stable-ansible-centos7-2.tf.local": {
+                "ansible_ssh_private_key_file": "/etc/ansible/my_ansible_private_key"
+            }
+        }
+    },
+    "all": {
+        "children": [
+            "ungrouped"
+        ]
+    },
+    "ungrouped": {
+        "hosts": [
+            "uyuni-stable-ansible-centos7-1.tf.local",
+            "uyuni-stable-ansible-centos7-2.tf.local"
+        ]
+    }
+}
+    """
+    ansible_inventory2_ret = """
+{
+    "_meta": {
+        "hostvars": {
+            "uyuni-stable-ansible-alma9-1.tf.local": {
+                "ansible_ssh_private_key_file": "/etc/ansible/my_ansible_private_key"
+            },
+            "uyuni-stable-ansible-alma9-2.tf.local": {
+                "ansible_ssh_private_key_file": "/etc/ansible/my_ansible_private_key"
+            }
+        }
+    },
+    "all": {
+        "children": [
+            "ungrouped"
+        ]
+    },
+    "ungrouped": {
+        "hosts": [
+            "uyuni-stable-ansible-alma9-1.tf.local",
+            "uyuni-stable-ansible-alma9-2.tf.local"
+        ]
+    }
+}
+    """
+    ansible_inventory_mock = MagicMock(
+        side_effect=[ansible_inventory1_ret, ansible_inventory2_ret]
+    )
+    with patch("salt.utils.path.which", MagicMock(return_value=True)):
+        utils = salt.loader.utils(minion_opts, whitelist=["ansible"])
+        with patch("salt.modules.cmdmod.run", ansible_inventory_mock), patch.dict(
+            ansiblegate.__utils__, utils
+        ), patch("os.path.isfile", MagicMock(return_value=True)):
+            ret = ansiblegate.targets(
+                inventories=["/etc/ansible/hosts1", "/etc/ansible/hosts2"]
+            )
+            assert ansible_inventory_mock.call_args
+            assert ansible_inventory_mock.call_args
+            assert len(ret.keys()) == 2
+            assert "/etc/ansible/hosts1" in ret.keys()
+            assert "/etc/ansible/hosts2" in ret.keys()
+            assert "_meta" in ret["/etc/ansible/hosts1"]
+            assert "_meta" in ret["/etc/ansible/hosts2"]
+            assert (
+                "uyuni-stable-ansible-centos7-1.tf.local"
+                in ret["/etc/ansible/hosts1"]["_meta"]["hostvars"]
+            )
+            assert (
+                "uyuni-stable-ansible-centos7-2.tf.local"
+                in ret["/etc/ansible/hosts1"]["_meta"]["hostvars"]
+            )
+            assert (
+                "uyuni-stable-ansible-alma9-1.tf.local"
+                in ret["/etc/ansible/hosts2"]["_meta"]["hostvars"]
+            )
+            assert (
+                "uyuni-stable-ansible-alma9-2.tf.local"
+                in ret["/etc/ansible/hosts2"]["_meta"]["hostvars"]
+            )
+            assert (
+                "ansible_ssh_private_key_file"
+                in ret["/etc/ansible/hosts1"]["_meta"]["hostvars"][
+                    "uyuni-stable-ansible-centos7-1.tf.local"
+                ]
+            )
+            assert (
+                "ansible_ssh_private_key_file"
+                in ret["/etc/ansible/hosts1"]["_meta"]["hostvars"][
+                    "uyuni-stable-ansible-centos7-2.tf.local"
+                ]
+            )
+            assert (
+                "ansible_ssh_private_key_file"
+                in ret["/etc/ansible/hosts2"]["_meta"]["hostvars"][
+                    "uyuni-stable-ansible-alma9-1.tf.local"
+                ]
+            )
+            assert (
+                "ansible_ssh_private_key_file"
+                in ret["/etc/ansible/hosts2"]["_meta"]["hostvars"][
+                    "uyuni-stable-ansible-alma9-2.tf.local"
+                ]
+            )
+            assert "all" in ret["/etc/ansible/hosts1"]
+            assert "all" in ret["/etc/ansible/hosts2"]
+            assert len(ret["/etc/ansible/hosts1"]["ungrouped"]["hosts"]) == 2
+            assert len(ret["/etc/ansible/hosts2"]["ungrouped"]["hosts"]) == 2
+
+
 def test_ansible_discover_playbooks_single_path():
     playbooks_dir = os.path.join(
         RUNTIME_VARS.TESTS_DIR, "unit/files/playbooks/example_playbooks/"
-- 
2.48.1

openSUSE Build Service is sponsored by