mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
New module: proxmox_snap (#1135)
* Fresh branch to try this again * Fix typo * Fix sanity error * Redid some tests, not sure if this works... * Re-fixed documentation * Fixed linter * Update layout * Processed feedback * Processed feedback * Added check_mode * Run test with check mode * Fix layout * Fix more layout * Fixed last sanity errors (for now) * Check mode is not a module arg.. * Fix sanity-error * Reworked check_mode and adapted tests * Added extra test to also run through remove logic, added message for check_mode to show output is different, grabbed magicmock from different library to fix py2 errors * Remove unused function * Processed feedback * Processed feedback * Processed feedback
This commit is contained in:
parent
5ee5c004b4
commit
0d1417dcfa
3 changed files with 378 additions and 0 deletions
289
plugins/modules/cloud/misc/proxmox_snap.py
Normal file
289
plugins/modules/cloud/misc/proxmox_snap.py
Normal file
|
@ -0,0 +1,289 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2020, Jeffrey van Pelt (@Thulium-Drake) <jeff@vanpelt.one>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: proxmox_snap
|
||||
short_description: Snapshot management of instances in Proxmox VE cluster
|
||||
version_added: 2.0.0
|
||||
description:
|
||||
- Allows you to create/delete snapshots from instances in Proxmox VE cluster.
|
||||
- Supports both KVM and LXC, OpenVZ has not been tested, as it is no longer supported on Proxmox VE.
|
||||
options:
|
||||
api_host:
|
||||
description:
|
||||
- The host of the Proxmox VE cluster.
|
||||
required: true
|
||||
type: str
|
||||
api_user:
|
||||
description:
|
||||
- The user to authenticate with.
|
||||
required: true
|
||||
type: str
|
||||
api_password:
|
||||
description:
|
||||
- The password to authenticate with.
|
||||
- You can use PROXMOX_PASSWORD environment variable.
|
||||
type: str
|
||||
hostname:
|
||||
description:
|
||||
- The instance name.
|
||||
type: str
|
||||
vmid:
|
||||
description:
|
||||
- The instance id.
|
||||
- If not set, will be fetched from PromoxAPI based on the hostname.
|
||||
type: str
|
||||
validate_certs:
|
||||
description:
|
||||
- Enable / disable https certificate verification.
|
||||
type: bool
|
||||
default: no
|
||||
state:
|
||||
description:
|
||||
- Indicate desired state of the instance snapshot.
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- For removal from config file, even if removing disk snapshot fails.
|
||||
default: no
|
||||
type: bool
|
||||
vmstate:
|
||||
description:
|
||||
- Snapshot includes RAM.
|
||||
default: no
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
- Specify the description for the snapshot. Only used on the configuration web interface.
|
||||
- This is saved as a comment inside the configuration file.
|
||||
type: str
|
||||
timeout:
|
||||
description:
|
||||
- Timeout for operations.
|
||||
default: 30
|
||||
type: int
|
||||
snapname:
|
||||
description:
|
||||
- Name of the snapshot that has to be created.
|
||||
default: 'ansible_snap'
|
||||
type: str
|
||||
|
||||
notes:
|
||||
- Requires proxmoxer and requests modules on host. These modules can be installed with pip.
|
||||
- Supports C(check_mode).
|
||||
requirements: [ "proxmoxer", "python >= 2.7", "requests" ]
|
||||
author: Jeffrey van Pelt (@Thulium-Drake)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create new container snapshot
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: present
|
||||
snapname: pre-updates
|
||||
|
||||
- name: Remove container snapshot
|
||||
community.general.proxmox_snap:
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
vmid: 100
|
||||
state: absent
|
||||
snapname: pre-updates
|
||||
'''
|
||||
|
||||
RETURN = r'''#'''
|
||||
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
PROXMOXER_IMP_ERR = None
|
||||
try:
|
||||
from proxmoxer import ProxmoxAPI
|
||||
HAS_PROXMOXER = True
|
||||
except ImportError:
|
||||
PROXMOXER_IMP_ERR = traceback.format_exc()
|
||||
HAS_PROXMOXER = False
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
VZ_TYPE = None
|
||||
|
||||
|
||||
def get_vmid(proxmox, hostname):
|
||||
return [vm['vmid'] for vm in proxmox.cluster.resources.get(type='vm') if 'name' in vm and vm['name'] == hostname]
|
||||
|
||||
|
||||
def get_instance(proxmox, vmid):
|
||||
return [vm for vm in proxmox.cluster.resources.get(type='vm') if int(vm['vmid']) == int(vmid)]
|
||||
|
||||
|
||||
def snapshot_create(module, proxmox, vm, vmid, timeout, snapname, description, vmstate):
|
||||
if module.check_mode:
|
||||
return True
|
||||
|
||||
if VZ_TYPE == 'lxc':
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).snapshot.post(snapname=snapname, description=description)
|
||||
else:
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).snapshot.post(snapname=snapname, description=description, vmstate=int(vmstate))
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for creating VM snapshot. Last line in task before timeout: %s' %
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
def snapshot_remove(module, proxmox, vm, vmid, timeout, snapname, force):
|
||||
if module.check_mode:
|
||||
return True
|
||||
|
||||
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).snapshot.delete(snapname, force=int(force))
|
||||
while timeout:
|
||||
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
|
||||
return True
|
||||
timeout -= 1
|
||||
if timeout == 0:
|
||||
module.fail_json(msg='Reached timeout while waiting for removing VM snapshot. Last line in task before timeout: %s' %
|
||||
proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
|
||||
def setup_api(api_host, api_user, api_password, validate_certs):
|
||||
api = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
|
||||
return api
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
api_host=dict(required=True),
|
||||
api_user=dict(required=True),
|
||||
api_password=dict(no_log=True),
|
||||
vmid=dict(required=False),
|
||||
validate_certs=dict(type='bool', default='no'),
|
||||
hostname=dict(),
|
||||
timeout=dict(type='int', default=30),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
description=dict(type='str'),
|
||||
snapname=dict(type='str', default='ansible_snap'),
|
||||
force=dict(type='bool', default='no'),
|
||||
vmstate=dict(type='bool', default='no'),
|
||||
),
|
||||
supports_check_mode=True
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('python-proxmoxer'),
|
||||
exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
state = module.params['state']
|
||||
api_user = module.params['api_user']
|
||||
api_host = module.params['api_host']
|
||||
api_password = module.params['api_password']
|
||||
vmid = module.params['vmid']
|
||||
validate_certs = module.params['validate_certs']
|
||||
hostname = module.params['hostname']
|
||||
description = module.params['description']
|
||||
snapname = module.params['snapname']
|
||||
timeout = module.params['timeout']
|
||||
force = module.params['force']
|
||||
vmstate = module.params['vmstate']
|
||||
|
||||
# If password not set get it from PROXMOX_PASSWORD env
|
||||
if not api_password:
|
||||
try:
|
||||
api_password = os.environ['PROXMOX_PASSWORD']
|
||||
except KeyError as e:
|
||||
module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable' % to_native(e))
|
||||
|
||||
try:
|
||||
proxmox = setup_api(api_host, api_user, api_password, validate_certs)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % to_native(e))
|
||||
|
||||
# If hostname is set get the VM id from ProxmoxAPI
|
||||
if not vmid and hostname:
|
||||
hosts = get_vmid(proxmox, hostname)
|
||||
if len(hosts) == 0:
|
||||
module.fail_json(msg="Vmid could not be fetched => Hostname does not exist (action: %s)" % state)
|
||||
vmid = hosts[0]
|
||||
elif not vmid:
|
||||
module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state)
|
||||
|
||||
vm = get_instance(proxmox, vmid)
|
||||
|
||||
global VZ_TYPE
|
||||
VZ_TYPE = vm[0]['type']
|
||||
|
||||
if state == 'present':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
if not vm:
|
||||
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||
|
||||
for i in getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).snapshot.get():
|
||||
if i['name'] == snapname:
|
||||
module.exit_json(changed=False, msg="Snapshot %s is already present" % snapname)
|
||||
|
||||
if snapshot_create(module, proxmox, vm, vmid, timeout, snapname, description, vmstate):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False, msg="Snapshot %s would be created" % snapname)
|
||||
else:
|
||||
module.exit_json(changed=True, msg="Snapshot %s created" % snapname)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Creating snapshot %s of VM %s failed with exception: %s" % (snapname, vmid, to_native(e)))
|
||||
|
||||
elif state == 'absent':
|
||||
try:
|
||||
vm = get_instance(proxmox, vmid)
|
||||
if not vm:
|
||||
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||
|
||||
snap_exist = False
|
||||
|
||||
for i in getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).snapshot.get():
|
||||
if i['name'] == snapname:
|
||||
snap_exist = True
|
||||
continue
|
||||
|
||||
if not snap_exist:
|
||||
module.exit_json(changed=False, msg="Snapshot %s does not exist" % snapname)
|
||||
else:
|
||||
if snapshot_remove(module, proxmox, vm, vmid, timeout, snapname, force):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=False, msg="Snapshot %s would be removed" % snapname)
|
||||
else:
|
||||
module.exit_json(changed=True, msg="Snapshot %s removed" % snapname)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Removing snapshot %s of VM %s failed with exception: %s" % (snapname, vmid, to_native(e)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
1
plugins/modules/proxmox_snap.py
Symbolic link
1
plugins/modules/proxmox_snap.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
./cloud/misc/proxmox_snap.py
|
88
tests/unit/plugins/modules/cloud/misc/test_proxmox_snap.py
Normal file
88
tests/unit/plugins/modules/cloud/misc/test_proxmox_snap.py
Normal file
|
@ -0,0 +1,88 @@
|
|||
# Copyright: (c) 2019, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from ansible_collections.community.general.tests.unit.compat.mock import MagicMock
|
||||
from ansible_collections.community.general.plugins.modules.cloud.misc import proxmox_snap
|
||||
from ansible_collections.community.general.tests.unit.plugins.modules.utils import set_module_args
|
||||
|
||||
|
||||
def get_resources(type):
|
||||
return [{"diskwrite": 0,
|
||||
"vmid": 100,
|
||||
"node": "localhost",
|
||||
"id": "lxc/100",
|
||||
"maxdisk": 10000,
|
||||
"template": 0,
|
||||
"disk": 10000,
|
||||
"uptime": 10000,
|
||||
"maxmem": 10000,
|
||||
"maxcpu": 1,
|
||||
"netin": 10000,
|
||||
"type": "lxc",
|
||||
"netout": 10000,
|
||||
"mem": 10000,
|
||||
"diskread": 10000,
|
||||
"cpu": 0.01,
|
||||
"name": "test-lxc",
|
||||
"status": "running"}]
|
||||
|
||||
|
||||
def fake_api(api_host, api_user, api_password, validate_certs):
|
||||
r = MagicMock()
|
||||
r.cluster.resources.get = MagicMock(side_effect=get_resources)
|
||||
return r
|
||||
|
||||
|
||||
def test_proxmox_snap_without_argument(capfd):
|
||||
set_module_args({})
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert json.loads(out)['failed']
|
||||
|
||||
|
||||
def test_create_snapshot_check_mode(capfd, mocker):
|
||||
set_module_args({"hostname": "test-lxc",
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"state": "present",
|
||||
"snapname": "test",
|
||||
"timeout": "1",
|
||||
"force": True,
|
||||
"_ansible_check_mode": True})
|
||||
proxmox_snap.HAS_PROXMOXER = True
|
||||
proxmox_snap.setup_api = mocker.MagicMock(side_effect=fake_api)
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert not json.loads(out)['changed']
|
||||
|
||||
|
||||
def test_remove_snapshot_check_mode(capfd, mocker):
|
||||
set_module_args({"hostname": "test-lxc",
|
||||
"api_user": "root@pam",
|
||||
"api_password": "secret",
|
||||
"api_host": "127.0.0.1",
|
||||
"state": "absent",
|
||||
"snapname": "test",
|
||||
"timeout": "1",
|
||||
"force": True,
|
||||
"_ansible_check_mode": True})
|
||||
proxmox_snap.HAS_PROXMOXER = True
|
||||
proxmox_snap.setup_api = mocker.MagicMock(side_effect=fake_api)
|
||||
with pytest.raises(SystemExit) as results:
|
||||
proxmox_snap.main()
|
||||
|
||||
out, err = capfd.readouterr()
|
||||
assert not err
|
||||
assert not json.loads(out)['changed']
|
Loading…
Reference in a new issue