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