From dcb38bece0757a7379fe27df3fd256e7b0be3c82 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 26 Nov 2023 17:35:57 +0100
Subject: [PATCH] [PR #7461/d05932fb backport][stable-8] Add option to proxmox
 dynamic inventory to exclude nodes (#7606)

Add option to proxmox dynamic inventory to exclude nodes (#7461)

* Create option to exclude proxmox nodes

* improve node exclusion by only remove the top level group

* add fragment

* Update changelogs/fragments/7437-proxmox-inventory-add-exclude-nodes.yaml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/inventory/proxmox.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Rework node exclusion

* Update fragement PR number

* include release version in option

Co-authored-by: Felix Fontein <felix@fontein.de>

* Clarify description

* Update unit test

* Fix typos in unit test

* Fix additonal typos in test

* Fix CI

* Fixing yet another whitespace pep error

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit d05932fb2cf0bcb81041e8df3b8819ee201424c0)

Co-authored-by: IamLunchbox <56757745+IamLunchbox@users.noreply.github.com>
---
 ...1-proxmox-inventory-add-exclude-nodes.yaml |  2 +
 plugins/inventory/proxmox.py                  | 24 ++++++----
 tests/unit/plugins/inventory/test_proxmox.py  | 45 ++++++++++++++++++-
 3 files changed, 60 insertions(+), 11 deletions(-)
 create mode 100644 changelogs/fragments/7461-proxmox-inventory-add-exclude-nodes.yaml

diff --git a/changelogs/fragments/7461-proxmox-inventory-add-exclude-nodes.yaml b/changelogs/fragments/7461-proxmox-inventory-add-exclude-nodes.yaml
new file mode 100644
index 00000000000..40391342f7e
--- /dev/null
+++ b/changelogs/fragments/7461-proxmox-inventory-add-exclude-nodes.yaml
@@ -0,0 +1,2 @@
+minor_changes:
+  - proxmox inventory plugin - adds an option to exclude nodes from the dynamic inventory generation. The new setting is optional, not using this option will behave as usual (https://github.com/ansible-collections/community.general/issues/6714, https://github.com/ansible-collections/community.general/pull/7461).
diff --git a/plugins/inventory/proxmox.py b/plugins/inventory/proxmox.py
index c6f70a45665..df593665cf2 100644
--- a/plugins/inventory/proxmox.py
+++ b/plugins/inventory/proxmox.py
@@ -116,6 +116,11 @@
           - The default of this option changed from V(true) to V(false) in community.general 6.0.0.
         type: bool
         default: false
+      exclude_nodes:
+        description: Exclude proxmox nodes and the nodes-group from the inventory output.
+        type: bool
+        default: false
+        version_added: 8.1.0
       filters:
         version_added: 4.6.0
         description: A list of Jinja templates that allow filtering hosts.
@@ -565,9 +570,9 @@ def _populate(self):
 
         for group in default_groups:
             self.inventory.add_group(self._group('all_%s' % (group)))
-
         nodes_group = self._group('nodes')
-        self.inventory.add_group(nodes_group)
+        if not self.exclude_nodes:
+            self.inventory.add_group(nodes_group)
 
         want_proxmox_nodes_ansible_host = self.get_option("want_proxmox_nodes_ansible_host")
 
@@ -577,22 +582,23 @@ def _populate(self):
         for node in self._get_nodes():
             if not node.get('node'):
                 continue
-
-            self.inventory.add_host(node['node'])
-            if node['type'] == 'node':
+            if not self.exclude_nodes:
+                self.inventory.add_host(node['node'])
+            if node['type'] == 'node' and not self.exclude_nodes:
                 self.inventory.add_child(nodes_group, node['node'])
 
             if node['status'] == 'offline':
                 continue
 
             # get node IP address
-            if want_proxmox_nodes_ansible_host:
+            if want_proxmox_nodes_ansible_host and not self.exclude_nodes:
                 ip = self._get_node_ip(node['node'])
                 self.inventory.set_variable(node['node'], 'ansible_host', ip)
 
             # Setting composite variables
-            variables = self.inventory.get_host(node['node']).get_vars()
-            self._set_composite_vars(self.get_option('compose'), variables, node['node'], strict=self.strict)
+            if not self.exclude_nodes:
+                variables = self.inventory.get_host(node['node']).get_vars()
+                self._set_composite_vars(self.get_option('compose'), variables, node['node'], strict=self.strict)
 
             # add LXC/Qemu groups for the node
             for ittype in ('lxc', 'qemu'):
@@ -635,8 +641,8 @@ def parse(self, inventory, loader, path, cache=True):
 
         if self.get_option('qemu_extended_statuses') and not self.get_option('want_facts'):
             raise AnsibleError('You must set want_facts to True if you want to use qemu_extended_statuses.')
-
         # read rest of options
+        self.exclude_nodes = self.get_option('exclude_nodes')
         self.cache_key = self.get_cache_key(path)
         self.use_cache = cache and self.get_option('cache')
         self.host_filters = self.get_option('filters')
diff --git a/tests/unit/plugins/inventory/test_proxmox.py b/tests/unit/plugins/inventory/test_proxmox.py
index 13832c9387b..ea6c84bcdaa 100644
--- a/tests/unit/plugins/inventory/test_proxmox.py
+++ b/tests/unit/plugins/inventory/test_proxmox.py
@@ -646,13 +646,15 @@ def test_populate(inventory, mocker):
     inventory.group_prefix = 'proxmox_'
     inventory.facts_prefix = 'proxmox_'
     inventory.strict = False
+    inventory.exclude_nodes = False
 
     opts = {
         'group_prefix': 'proxmox_',
         'facts_prefix': 'proxmox_',
         'want_facts': True,
         'want_proxmox_nodes_ansible_host': True,
-        'qemu_extended_statuses': True
+        'qemu_extended_statuses': True,
+        'exclude_nodes': False
     }
 
     # bypass authentication and API fetch calls
@@ -723,13 +725,15 @@ def test_populate_missing_qemu_extended_groups(inventory, mocker):
     inventory.group_prefix = 'proxmox_'
     inventory.facts_prefix = 'proxmox_'
     inventory.strict = False
+    inventory.exclude_nodes = False
 
     opts = {
         'group_prefix': 'proxmox_',
         'facts_prefix': 'proxmox_',
         'want_facts': True,
         'want_proxmox_nodes_ansible_host': True,
-        'qemu_extended_statuses': False
+        'qemu_extended_statuses': False,
+        'exclude_nodes': False
     }
 
     # bypass authentication and API fetch calls
@@ -743,3 +747,40 @@ def test_populate_missing_qemu_extended_groups(inventory, mocker):
     # make sure that ['prelaunch', 'paused'] are not in the group list
     for group in ['paused', 'prelaunch']:
         assert ('%sall_%s' % (inventory.group_prefix, group)) not in inventory.inventory.groups
+
+
+def test_populate_exclude_nodes(inventory, mocker):
+    # module settings
+    inventory.proxmox_user = 'root@pam'
+    inventory.proxmox_password = 'password'
+    inventory.proxmox_url = 'https://localhost:8006'
+    inventory.group_prefix = 'proxmox_'
+    inventory.facts_prefix = 'proxmox_'
+    inventory.strict = False
+    inventory.exclude_nodes = True
+
+    opts = {
+        'group_prefix': 'proxmox_',
+        'facts_prefix': 'proxmox_',
+        'want_facts': True,
+        'want_proxmox_nodes_ansible_host': True,
+        'qemu_extended_statuses': False,
+        'exclude_nodes': True
+    }
+
+    # bypass authentication and API fetch calls
+    inventory._get_auth = mocker.MagicMock(side_effect=get_auth)
+    inventory._get_json = mocker.MagicMock(side_effect=get_json)
+    inventory._get_vm_snapshots = mocker.MagicMock(side_effect=get_vm_snapshots)
+    inventory.get_option = mocker.MagicMock(side_effect=get_option(opts))
+    inventory._can_add_host = mocker.MagicMock(return_value=True)
+    inventory._populate()
+
+    # make sure that nodes are not in the inventory
+    for node in ['testnode', 'testnode2']:
+        assert node not in inventory.inventory.hosts
+    # make sure that nodes group is absent
+    assert ('%s_nodes' % (inventory.group_prefix)) not in inventory.inventory.groups
+    # make sure that nodes are not in the "ungrouped" group
+    for node in ['testnode', 'testnode2']:
+        assert node not in inventory.inventory.get_groups_dict()["ungrouped"]