From 96f465ddf82bd0cc5825511f2d7b00b3d6bc1cf9 Mon Sep 17 00:00:00 2001 From: raoufnezhad <72685312+raoufnezhad@users.noreply.github.com> Date: Sun, 26 Jan 2025 23:25:56 +0330 Subject: [PATCH] Add module proxmox_backup_info (#9437) * Create proxmox_backup_info.py The `proxmox_backup_info` module displays information such as backup times, VM name, VM ID, mode, backup type, and backup schedule using the Proxmox Server API. * Create test_proxmox_backup_info.py create test for proxmox_backup_info.py module * Update plugins/modules/proxmox_backup_info.py Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> * check tests proxmox_backup_info.py * check tests test_proxmox_backup_info.py * Update check tests test_proxmox_backup_info.py * Update check tests proxmox_backup_info.py * Update authors proxmox_backup_info.py * Update active maintainers for proxmox_backup_info module * Update add proxmox_backup_info module in proxmox group * edit timestamp to UTC test_proxmox_backup_info.py * Update vm name or vmid to VM name or VM id proxmox_backup_info.py * update documentation in proxmox_backup_info.py * Update backup_section decription in proxmox_backup_info.py * Update plugins/modules/proxmox_backup_info.py Co-authored-by: Felix Fontein * change backup_section to backup_jobs * change backup_section to backup_jobs * remove whitespace in line 35 and 36 * improve descriptions * check again proxmox_backup_info.py module * change vmid type and some descriptions proxmox_backup_info.py * delete comment #if ... * solve trailing whitespace error * Update the name of the functions * Update proxmox_backup_info.py * Update proxmox_backup_info.py * Update tests/unit/plugins/modules/test_proxmox_backup_info.py Co-authored-by: Felix Fontein * Update test_proxmox_backup_info.py * Update runtime.yml * add proxmox_backup_schedule module in runtime.yml * add proxmox_backup_schedule.py module in BOTMETA.yml * remove proxmox_backup_schedule module runtime.yml * remove proxmox_backup_schedule.py module in BOTMETA.yml * change some id to ID proxmox_backup_info.py * Update proxmox_backup_info.py * remove required: falsefrom documentations * change vimd values to str format * add samples to output documentations * the exact output of vimd in endpoint of proxmox resoures is like int --------- Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com> Co-authored-by: Felix Fontein Co-authored-by: mmayabi <137920552+mmayabi@users.noreply.github.com> --- .github/BOTMETA.yml | 2 + meta/runtime.yml | 1 + plugins/modules/proxmox_backup_info.py | 244 ++++++++++++++++ .../modules/test_proxmox_backup_info.py | 275 ++++++++++++++++++ 4 files changed, 522 insertions(+) create mode 100644 plugins/modules/proxmox_backup_info.py create mode 100644 tests/unit/plugins/modules/test_proxmox_backup_info.py diff --git a/.github/BOTMETA.yml b/.github/BOTMETA.yml index 4e9af36ff2..1beb35c57b 100644 --- a/.github/BOTMETA.yml +++ b/.github/BOTMETA.yml @@ -1146,6 +1146,8 @@ files: maintainers: helldorado krauthosting $modules/proxmox_backup.py: maintainers: IamLunchbox + $modules/proxmox_backup_info.py: + maintainers: raoufnezhad mmayabi $modules/proxmox_nic.py: maintainers: Kogelvis krauthosting $modules/proxmox_node_info.py: diff --git a/meta/runtime.yml b/meta/runtime.yml index 1106260176..d6ffdbec57 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -17,6 +17,7 @@ action_groups: proxmox: - proxmox - proxmox_backup + - proxmox_backup_info - proxmox_disk - proxmox_domain_info - proxmox_group_info diff --git a/plugins/modules/proxmox_backup_info.py b/plugins/modules/proxmox_backup_info.py new file mode 100644 index 0000000000..0889239b37 --- /dev/null +++ b/plugins/modules/proxmox_backup_info.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024 Marzieh Raoufnezhad +# Copyright (c) 2024 Maryam Mayabi +# 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_backup_info + +short_description: Retrieve information on Proxmox scheduled backups + +version_added: 10.3.0 + +description: + - Retrieve information such as backup times, VM name, VM ID, mode, backup type, and backup schedule using the Proxmox Server API. + +author: + - "Marzieh Raoufnezhad (@raoufnezhad) " + - "Maryam Mayabi (@mmayabi) " + +options: + vm_name: + description: + - The name of the Proxmox VM. + - If defined, the returned list will contain backup jobs that have been parsed and filtered based on O(vm_name) value. + - Mutually exclusive with O(vm_id) and O(backup_jobs). + type: str + vm_id: + description: + - The ID of the Proxmox VM. + - If defined, the returned list will contain backup jobs that have been parsed and filtered based on O(vm_id) value. + - Mutually exclusive with O(vm_name) and O(backup_jobs). + type: str + backup_jobs: + description: + - If V(true), the module will return all backup jobs information. + - If V(false), the module will parse all backup jobs based on VM IDs and return a list of VMs' backup information. + - Mutually exclusive with O(vm_id) and O(vm_name). + default: false + type: bool + +extends_documentation_fragment: + - community.general.proxmox.documentation + - community.general.attributes + - community.general.attributes.info_module + - community.general.proxmox.actiongroup_proxmox +""" + +EXAMPLES = """ +- name: Print all backup information by VM ID and VM name + community.general.proxmox_backup_info: + api_user: 'myUser@pam' + api_password: '*******' + api_host: '192.168.20.20' + +- name: Print Proxmox backup information for a specific VM based on its name + community.general.proxmox_backup_info: + api_user: 'myUser@pam' + api_password: '*******' + api_host: '192.168.20.20' + vm_name: 'mailsrv' + +- name: Print Proxmox backup information for a specific VM based on its VM ID + community.general.proxmox_backup_info: + api_user: 'myUser@pam' + api_password: '*******' + api_host: '192.168.20.20' + vm_id: '150' + +- name: Print Proxmox all backup job information + community.general.proxmox_backup_info: + api_user: 'myUser@pam' + api_password: '*******' + api_host: '192.168.20.20' + backup_jobs: true +""" + +RETURN = """ +--- +backup_info: + description: The return value provides backup job information based on VM ID or VM name, or total backup job information. + returned: on success, but can be empty + type: list + elements: dict + contains: + bktype: + description: The type of the backup. + returned: on success + type: str + sample: vzdump + enabled: + description: V(1) if backup is enabled else V(0). + returned: on success + type: int + sample: 1 + id: + description: The backup job ID. + returned: on success + type: str + sample: backup-83831498-c631 + mode: + description: The backup job mode such as snapshot. + returned: on success + type: str + sample: snapshot + next-run: + description: The next backup time. + returned: on success + type: str + sample: "2024-12-28 11:30:00" + schedule: + description: The backup job schedule. + returned: on success + type: str + sample: "sat 15:00" + storage: + description: The backup storage location. + returned: on success + type: str + sample: local + vm_name: + description: The VM name. + returned: on success + type: str + sample: test01 + vmid: + description: The VM ID. + returned: on success + type: str + sample: "100" +""" + +from datetime import datetime +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.community.general.plugins.module_utils.proxmox import ( + proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR) + + +class ProxmoxBackupInfoAnsible(ProxmoxAnsible): + + # Get all backup information + def get_jobs_list(self): + try: + backupJobs = self.proxmox_api.cluster.backup.get() + except Exception as e: + self.module.fail_json(msg="Getting backup jobs failed: %s" % e) + return backupJobs + + # Get VM information + def get_vms_list(self): + try: + vms = self.proxmox_api.cluster.resources.get(type='vm') + except Exception as e: + self.module.fail_json(msg="Getting VMs info from cluster failed: %s" % e) + return vms + + # Get all backup information by VM ID and VM name + def vms_backup_info(self): + backupList = self.get_jobs_list() + vmInfo = self.get_vms_list() + bkInfo = [] + for backupItem in backupList: + nextrun = datetime.fromtimestamp(backupItem['next-run']) + vmids = backupItem['vmid'].split(',') + for vmid in vmids: + for vm in vmInfo: + if vm['vmid'] == int(vmid): + vmName = vm['name'] + break + bkInfoData = {'id': backupItem['id'], + 'schedule': backupItem['schedule'], + 'storage': backupItem['storage'], + 'mode': backupItem['mode'], + 'next-run': nextrun.strftime("%Y-%m-%d %H:%M:%S"), + 'enabled': backupItem['enabled'], + 'bktype': backupItem['type'], + 'vmid': vmid, + 'vm_name': vmName} + bkInfo.append(bkInfoData) + return bkInfo + + # Get proxmox backup information for a specific VM based on its VM ID or VM name + def specific_vmbackup_info(self, vm_name_id): + fullBackupInfo = self.vms_backup_info() + vmBackupJobs = [] + for vm in fullBackupInfo: + if (vm["vm_name"] == vm_name_id or vm["vmid"] == vm_name_id): + vmBackupJobs.append(vm) + return vmBackupJobs + + +def main(): + # Define module args + args = proxmox_auth_argument_spec() + backup_info_args = dict( + vm_id=dict(type='str'), + vm_name=dict(type='str'), + backup_jobs=dict(type='bool', default=False) + ) + args.update(backup_info_args) + + module = AnsibleModule( + argument_spec=args, + mutually_exclusive=[('backup_jobs', 'vm_id', 'vm_name')], + supports_check_mode=True + ) + + # Define (init) result value + result = dict( + changed=False + ) + + # Check if proxmoxer exist + if not HAS_PROXMOXER: + module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR) + + # Start to connect to proxmox to get backup data + proxmox = ProxmoxBackupInfoAnsible(module) + vm_id = module.params['vm_id'] + vm_name = module.params['vm_name'] + backup_jobs = module.params['backup_jobs'] + + # Update result value based on what requested (module args) + if backup_jobs: + result['backup_info'] = proxmox.get_jobs_list() + elif vm_id: + result['backup_info'] = proxmox.specific_vmbackup_info(vm_id) + elif vm_name: + result['backup_info'] = proxmox.specific_vmbackup_info(vm_name) + else: + result['backup_info'] = proxmox.vms_backup_info() + + # Return result value + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/unit/plugins/modules/test_proxmox_backup_info.py b/tests/unit/plugins/modules/test_proxmox_backup_info.py new file mode 100644 index 0000000000..73a15b8ab8 --- /dev/null +++ b/tests/unit/plugins/modules/test_proxmox_backup_info.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2024 Marzieh Raoufnezhad +# Copyright (c) 2024 Maryam Mayabi +# 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_backup_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 + +RESOURCE_LIST = [ + { + "uptime": 0, + "diskwrite": 0, + "name": "test01", + "maxcpu": 0, + "node": "NODE1", + "mem": 0, + "netout": 0, + "netin": 0, + "maxmem": 0, + "diskread": 0, + "disk": 0, + "maxdisk": 0, + "status": "running", + "cpu": 0, + "id": "qemu/100", + "template": 0, + "vmid": 100, + "type": "qemu" + }, + { + "uptime": 0, + "diskwrite": 0, + "name": "test02", + "maxcpu": 0, + "node": "NODE1", + "mem": 0, + "netout": 0, + "netin": 0, + "maxmem": 0, + "diskread": 0, + "disk": 0, + "maxdisk": 0, + "status": "running", + "cpu": 0, + "id": "qemu/101", + "template": 0, + "vmid": 101, + "type": "qemu" + }, + { + "uptime": 0, + "diskwrite": 0, + "name": "test03", + "maxcpu": 0, + "node": "NODE2", + "mem": 0, + "netout": 0, + "netin": 0, + "maxmem": 0, + "diskread": 0, + "disk": 0, + "maxdisk": 0, + "status": "running", + "cpu": 0, + "id": "qemu/102", + "template": 0, + "vmid": 102, + "type": "qemu" + } +] +BACKUP_JOBS = [ + { + "type": "vzdump", + "id": "backup-83831498-c631", + "storage": "local", + "vmid": "100", + "enabled": 1, + "next-run": 1735138800, + "mailnotification": "always", + "schedule": "06,18:30", + "mode": "snapshot", + "notes-template": "guestname" + }, + { + "schedule": "sat 15:00", + "notes-template": "guestname", + "mode": "snapshot", + "mailnotification": "always", + "next-run": 1735385400, + "type": "vzdump", + "enabled": 1, + "vmid": "100,101,102", + "storage": "local", + "id": "backup-70025700-2302", + } +] + +EXPECTED_BACKUP_OUTPUT = [ + { + "bktype": "vzdump", + "enabled": 1, + "id": "backup-83831498-c631", + "mode": "snapshot", + "next-run": "2024-12-25 15:00:00", + "schedule": "06,18:30", + "storage": "local", + "vm_name": "test01", + "vmid": "100" + }, + { + "bktype": "vzdump", + "enabled": 1, + "id": "backup-70025700-2302", + "mode": "snapshot", + "next-run": "2024-12-28 11:30:00", + "schedule": "sat 15:00", + "storage": "local", + "vm_name": "test01", + "vmid": "100" + }, + { + "bktype": "vzdump", + "enabled": 1, + "id": "backup-70025700-2302", + "mode": "snapshot", + "next-run": "2024-12-28 11:30:00", + "schedule": "sat 15:00", + "storage": "local", + "vm_name": "test02", + "vmid": "101" + }, + { + "bktype": "vzdump", + "enabled": 1, + "id": "backup-70025700-2302", + "mode": "snapshot", + "next-run": "2024-12-28 11:30:00", + "schedule": "sat 15:00", + "storage": "local", + "vm_name": "test03", + "vmid": "102" + } +] +EXPECTED_BACKUP_JOBS_OUTPUT = [ + { + "enabled": 1, + "id": "backup-83831498-c631", + "mailnotification": "always", + "mode": "snapshot", + "next-run": 1735138800, + "notes-template": "guestname", + "schedule": "06,18:30", + "storage": "local", + "type": "vzdump", + "vmid": "100" + }, + { + "enabled": 1, + "id": "backup-70025700-2302", + "mailnotification": "always", + "mode": "snapshot", + "next-run": 1735385400, + "notes-template": "guestname", + "schedule": "sat 15:00", + "storage": "local", + "type": "vzdump", + "vmid": "100,101,102" + } +] + + +class TestProxmoxBackupInfoModule(ModuleTestCase): + def setUp(self): + super(TestProxmoxBackupInfoModule, self).setUp() + proxmox_utils.HAS_PROXMOXER = True + self.module = proxmox_backup_info + self.connect_mock = patch( + "ansible_collections.community.general.plugins.module_utils.proxmox.ProxmoxAnsible._connect", + ).start() + self.connect_mock.return_value.cluster.resources.get.return_value = ( + RESOURCE_LIST + ) + self.connect_mock.return_value.cluster.backup.get.return_value = ( + BACKUP_JOBS + ) + + def tearDown(self): + self.connect_mock.stop() + super(TestProxmoxBackupInfoModule, self).tearDown() + + def test_module_fail_when_required_args_missing(self): + with pytest.raises(AnsibleFailJson) as exc_info: + set_module_args({}) + self.module.main() + + result = exc_info.value.args[0] + assert result["msg"] == "missing required arguments: api_host, api_user" + + def test_get_all_backups_information(self): + with pytest.raises(AnsibleExitJson) as exc_info: + set_module_args({ + 'api_host': 'proxmoxhost', + 'api_user': 'root@pam', + 'api_password': 'supersecret' + }) + self.module.main() + + result = exc_info.value.args[0] + assert result["backup_info"] == EXPECTED_BACKUP_OUTPUT + + def test_get_specific_backup_information_by_vmname(self): + with pytest.raises(AnsibleExitJson) as exc_info: + vmname = 'test01' + expected_output = [ + backup for backup in EXPECTED_BACKUP_OUTPUT if backup["vm_name"] == vmname + ] + set_module_args({ + 'api_host': 'proxmoxhost', + 'api_user': 'root@pam', + 'api_password': 'supersecret', + 'vm_name': vmname + }) + self.module.main() + + result = exc_info.value.args[0] + assert result["backup_info"] == expected_output + assert len(result["backup_info"]) == 2 + + def test_get_specific_backup_information_by_vmid(self): + with pytest.raises(AnsibleExitJson) as exc_info: + vmid = "101" + expected_output = [ + backup for backup in EXPECTED_BACKUP_OUTPUT if backup["vmid"] == vmid + ] + set_module_args({ + 'api_host': 'proxmoxhost', + 'api_user': 'root@pam', + 'api_password': 'supersecret', + 'vm_id': vmid + }) + self.module.main() + result = exc_info.value.args[0] + assert result["backup_info"] == expected_output + assert len(result["backup_info"]) == 1 + + def test_get_specific_backup_information_by_backupjobs(self): + with pytest.raises(AnsibleExitJson) as exc_info: + backupjobs = True + set_module_args({ + 'api_host': 'proxmoxhost', + 'api_user': 'root@pam', + 'api_password': 'supersecret', + 'backup_jobs': backupjobs + }) + self.module.main() + + result = exc_info.value.args[0] + assert result["backup_info"] == EXPECTED_BACKUP_JOBS_OUTPUT