diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 8c212c65d4..42b3c210b1 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1053,6 +1053,8 @@ files: maintainers: Kogelvis $modules/proxmox_node_info.py: maintainers: jwbernin + $modules/proxmox_storage_contents_info.py: + maintainers: l00ptr $modules/proxmox_tasks_info: maintainers: paginabianca $modules/proxmox_template.py: diff --git a/plugins/module_utils/proxmox.py b/plugins/module_utils/proxmox.py index 27c797e7cc..5fd783d654 100644 --- a/plugins/module_utils/proxmox.py +++ b/plugins/module_utils/proxmox.py @@ -180,3 +180,17 @@ class ProxmoxAnsible(object): 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 index 0000000000..498490fe41 --- /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) +# 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() diff --git a/tests/integration/targets/proxmox/tasks/main.yml b/tests/integration/targets/proxmox/tasks/main.yml index 4bebd71954..1b529d1112 100644 --- 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 index 0000000000..df2625dba6 --- /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 +# 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