Repository: ansible-collections/community.general

Issue:
Add module to list content of proxmox storage
### Summary

It could be nice to have a module to list content (by type) from proxmox storage. I would like to use that kind of feature to automate backup restoration. I can imagine that:

- launch a task to retrieve backups dedicated to a vmid on a specific storage
- calculate the newest backup (some json parsing on my role)
- launch a task to restore that backup

### Issue Type

Feature Idea

### Component Name

proxmox

### Additional Information

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: List existing backup stored on specific storage
  register: backups_for_vmid_123
  community.general.proxmox_storage_list:
    api_host: helldorado
    api_user: root@pam
    api_password: "{{ password | default(omit) }}"
    api_token_id: "{{ token_id | default(omit) }}"
    api_token_secret: "{{ token_secret | default(omit) }}"
    storage:  backup_store
    vmid: 123
    content: backup
```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:

Resolving patch:
diff --git a/plugins/module_utils/proxmox.py b/plugins/module_utils/proxmox.py
--- a/plugins/module_utils/proxmox.py
+++ b/plugins/module_utils/proxmox.py
@@ -180,3 +180,17 @@ def get_storages(self, type):
             return self.proxmox_api.storage.get(type=type)
         except Exception as e:
             self.module.fail_json(msg="Unable to retrieve storages information with type %s: %s" % (type, e))
+
+    def get_storage_content(self, node, storage, content=None, vmid=None):
+        try:
+            return (
+                self.proxmox_api.nodes(node)
+                .storage(storage)
+                .content()
+                .get(content=content, vmid=vmid)
+            )
+        except Exception as e:
+            self.module.fail_json(
+                msg="Unable to list content on %s, %s for %s and %s: %s"
+                % (node, storage, content, vmid, e)
+            )
diff --git a/plugins/modules/proxmox_storage_contents_info.py b/plugins/modules/proxmox_storage_contents_info.py
new file mode 100644
--- /dev/null
+++ b/plugins/modules/proxmox_storage_contents_info.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright Julian Vanden Broeck (@l00ptr) <julian.vandenbroeck at dalibo.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+DOCUMENTATION = """
+---
+module: proxmox_storage_contents_info
+short_description: List content from a Proxmox VE storage
+version_added: 8.2.0
+description:
+  - Retrieves information about stored objects on a specific storage attached to a node.
+options:
+  storage:
+    description:
+      - Only return content stored on that specific storage.
+    aliases: ['name']
+    type: str
+    required: true
+  node:
+    description:
+      - Proxmox node to which the storage is attached.
+    type: str
+    required: true
+  content:
+    description:
+      - Filter on a specific content type.
+    type: str
+    choices: ["all", "backup", "rootdir", "images", "iso"]
+    default: "all"
+  vmid:
+    description:
+      - Filter on a specific VMID.
+    type: int
+author: Julian Vanden Broeck (@l00ptr)
+extends_documentation_fragment:
+  - community.general.proxmox.documentation
+  - community.general.attributes
+  - community.general.attributes.info_module
+"""
+
+
+EXAMPLES = """
+- name: List existing storages
+  community.general.proxmox_storage_contents_info:
+    api_host: helldorado
+    api_user: root@pam
+    api_password: "{{ password | default(omit) }}"
+    api_token_id: "{{ token_id | default(omit) }}"
+    api_token_secret: "{{ token_secret | default(omit) }}"
+    storage: lvm2
+    content: backup
+    vmid: 130
+"""
+
+
+RETURN = """
+proxmox_storage_content:
+  description: Content of of storage attached to a node.
+  type: list
+  returned: success
+  elements: dict
+  contains:
+    content:
+      description: Proxmox content of listed objects on this storage.
+      type: str
+      returned: success
+    ctime:
+      description: Creation time of the listed objects.
+      type: str
+      returned: success
+    format:
+      description: Format of the listed objects (can be V(raw), V(pbs-vm), V(iso),...).
+      type: str
+      returned: success
+    size:
+      description: Size of the listed objects.
+      type: int
+      returned: success
+    subtype:
+      description: Subtype of the listed objects (can be V(qemu) or V(lxc)).
+      type: str
+      returned: When storage is dedicated to backup, typically on PBS storage.
+    verification:
+      description: Backup verification status of the listed objects.
+      type: dict
+      returned: When storage is dedicated to backup, typically on PBS storage.
+      sample: {
+        "state": "ok",
+        "upid": "UPID:backup-srv:00130F49:1A12D8375:00001CD7:657A2258:verificationjob:daily\\x3av\\x2dd0cc18c5\\x2d8707:root@pam:"
+        }
+    volid:
+      description: Volume identifier of the listed objects.
+      type: str
+      returned: success
+"""
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.general.plugins.module_utils.proxmox import (
+    ProxmoxAnsible, proxmox_auth_argument_spec)
+
+
+def proxmox_storage_info_argument_spec():
+    return dict(
+        storage=dict(type="str", required=True, aliases=["name"]),
+        content=dict(type="str", required=False, default="all", choices=["all", "backup", "rootdir", "images", "iso"]),
+        vmid=dict(type="int"),
+        node=dict(required=True, type="str"),
+    )
+
+
+def main():
+    module_args = proxmox_auth_argument_spec()
+    storage_info_args = proxmox_storage_info_argument_spec()
+    module_args.update(storage_info_args)
+
+    module = AnsibleModule(
+        argument_spec=module_args,
+        required_one_of=[("api_password", "api_token_id")],
+        required_together=[("api_token_id", "api_token_secret")],
+        supports_check_mode=True,
+    )
+    result = dict(changed=False)
+    proxmox = ProxmoxAnsible(module)
+    res = proxmox.get_storage_content(
+        node=module.params["node"],
+        storage=module.params["storage"],
+        content=None if module.params["content"] == "all" else module.params["content"],
+        vmid=module.params["vmid"],
+    )
+    result["proxmox_storage_content"] = res
+    module.exit_json(**result)
+
+
+if __name__ == "__main__":
+    main()


Test patch:
diff --git a/tests/integration/targets/proxmox/tasks/main.yml b/tests/integration/targets/proxmox/tasks/main.yml
--- a/tests/integration/targets/proxmox/tasks/main.yml
+++ b/tests/integration/targets/proxmox/tasks/main.yml
@@ -129,6 +129,25 @@
     - results_storage.proxmox_storages|length == 1
     - results_storage.proxmox_storages[0].storage == "{{ storage }}"
 
+- name: List content on storage
+  proxmox_storage_contents_info:
+    api_host: "{{ api_host }}"
+    api_user: "{{ user }}@{{ domain }}"
+    api_password: "{{ api_password | default(omit) }}"
+    api_token_id: "{{ api_token_id | default(omit) }}"
+    api_token_secret: "{{ api_token_secret | default(omit) }}"
+    validate_certs: "{{ validate_certs }}"
+    storage: "{{ storage }}"
+    node: "{{ node }}"
+    content: images
+  register: results_list_storage
+
+- assert:
+    that:
+    - results_storage is not changed
+    - results_storage.proxmox_storage_content is defined
+    - results_storage.proxmox_storage_content |length == 1
+
 - name: VM creation
   tags: [ 'create' ]
   block:
diff --git a/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py b/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/plugins/modules/test_proxmox_storage_contents_info.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2023, Julian Vanden Broeck <julian.vandenbroeck at dalibo.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import pytest
+
+proxmoxer = pytest.importorskip("proxmoxer")
+
+from ansible_collections.community.general.plugins.modules import proxmox_storage_contents_info
+from ansible_collections.community.general.tests.unit.compat.mock import patch
+from ansible_collections.community.general.tests.unit.plugins.modules.utils import (
+    AnsibleExitJson,
+    AnsibleFailJson,
+    ModuleTestCase,
+    set_module_args,
+)
+import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
+
+NODE1 = "pve"
+RAW_LIST_OUTPUT = [
+    {
+        "content": "backup",
+        "ctime": 1702528474,
+        "format": "pbs-vm",
+        "size": 273804166061,
+        "subtype": "qemu",
+        "vmid": 931,
+        "volid": "datastore:backup/vm/931/2023-12-14T04:34:34Z",
+    },
+    {
+        "content": "backup",
+        "ctime": 1702582560,
+        "format": "pbs-vm",
+        "size": 273804166059,
+        "subtype": "qemu",
+        "vmid": 931,
+        "volid": "datastore:backup/vm/931/2023-12-14T19:36:00Z",
+    },
+]
+
+
+def get_module_args(node, storage, content="all", vmid=None):
+    return {
+        "api_host": "host",
+        "api_user": "user",
+        "api_password": "password",
+        "node": node,
+        "storage": storage,
+        "content": content,
+        "vmid": vmid,
+    }
+
+
+class TestProxmoxStorageContentsInfo(ModuleTestCase):
+    def setUp(self):
+        super(TestProxmoxStorageContentsInfo, self).setUp()
+        proxmox_utils.HAS_PROXMOXER = True
+        self.module = proxmox_storage_contents_info
+        self.connect_mock = patch(
+            "ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect",
+        ).start()
+        self.connect_mock.return_value.nodes.return_value.storage.return_value.content.return_value.get.return_value = (
+            RAW_LIST_OUTPUT
+        )
+        self.connect_mock.return_value.nodes.get.return_value = [{"node": NODE1}]
+
+    def tearDown(self):
+        self.connect_mock.stop()
+        super(TestProxmoxStorageContentsInfo, self).tearDown()
+
+    def test_module_fail_when_required_args_missing(self):
+        with pytest.raises(AnsibleFailJson) as exc_info:
+            set_module_args({})
+            self.module.main()
+
+    def test_storage_contents_info(self):
+        with pytest.raises(AnsibleExitJson) as exc_info:
+            set_module_args(get_module_args(node=NODE1, storage="datastore"))
+            expected_output = {}
+            self.module.main()
+
+        result = exc_info.value.args[0]
+        assert not result["changed"]
+        assert result["proxmox_storage_content"] == RAW_LIST_OUTPUT