From 4f92f39720c5883fa38e32d6016e2cae70e2b760 Mon Sep 17 00:00:00 2001 From: Julian <374571+l00ptr@users.noreply.github.com> Date: Sun, 31 Dec 2023 15:21:20 +0100 Subject: [PATCH] Proxmox add storage content listing (#7725) Add module to list content on proxmox storage We first add a method to list storage content for proxmox, then use that new methode to add an Ansible module to list content on storage attached to a proxmox node. User can also use content filtering to define what they want to list (backup, iso, images,...). This commit also include the integration and unit test for that new module. Co-authored-by: Julian Vanden Broeck --- .github/BOTMETA.yml | 2 + plugins/module_utils/proxmox.py | 14 ++ .../modules/proxmox_storage_contents_info.py | 144 ++++++++++++++++++ .../targets/proxmox/tasks/main.yml | 19 +++ .../test_proxmox_storage_contents_info.py | 90 +++++++++++ 5 files changed, 269 insertions(+) create mode 100644 plugins/modules/proxmox_storage_contents_info.py create mode 100644 tests/unit/plugins/modules/test_proxmox_storage_contents_info.py 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