Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] proxmox_snap: snapshot containers with configured mountpoints #5274

Merged
merged 7 commits into from
Sep 28, 2022
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
minor_changes:
- proxmox_snap - add `unbind` param to support snapshotting containers with configured mountpoints (https://github.com/ansible-collections/community.general/pull/5274).
nxet marked this conversation as resolved.
Show resolved Hide resolved
- module_utils.proxmox - add `api_task_ok` helper to standardize API task status checks across all proxmox modules (https://github.com/ansible-collections/community.general/pull/5274).
nxet marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions plugins/module_utils/proxmox.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,7 @@ def get_vm(self, vmid, ignore_missing=False):
return None

self.module.fail_json(msg='VM with vmid %s does not exist in cluster' % vmid)

def api_task_ok(self, node, taskid):
status = self.proxmox_api.nodes(node).tasks(taskid).status.get()
return status['status'] == 'stopped' and status['exitstatus'] == 'OK'
15 changes: 5 additions & 10 deletions plugins/modules/cloud/misc/proxmox.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,7 @@ def create_instance(self, vmid, node, disk, storage, cpus, memory, swap, timeout
taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)

while timeout:
if (proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' and
proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK'):
if self.api_task_ok(node, taskid):
return True
timeout -= 1
if timeout == 0:
Expand All @@ -496,8 +495,7 @@ def create_instance(self, vmid, node, disk, storage, cpus, memory, swap, timeout
def start_instance(self, vm, vmid, timeout):
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.start.post()
while timeout:
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
if self.api_task_ok(vm['node'], taskid):
return True
timeout -= 1
if timeout == 0:
Expand All @@ -513,8 +511,7 @@ def stop_instance(self, vm, vmid, timeout, force):
else:
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.shutdown.post()
while timeout:
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
if self.api_task_ok(vm['node'], taskid):
return True
timeout -= 1
if timeout == 0:
Expand All @@ -527,8 +524,7 @@ def stop_instance(self, vm, vmid, timeout, force):
def umount_instance(self, vm, vmid, timeout):
taskid = getattr(self.proxmox_api.nodes(vm['node']), VZ_TYPE)(vmid).status.umount.post()
while timeout:
if (self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['status'] == 'stopped' and
self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):
if self.api_task_ok(vm['node'], taskid):
return True
timeout -= 1
if timeout == 0:
Expand Down Expand Up @@ -775,8 +771,7 @@ def main():
taskid = getattr(proxmox.proxmox_api.nodes(vm['node']), VZ_TYPE).delete(vmid, **delete_params)

while timeout:
task_status = proxmox.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
if (task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK'):
if proxmox.api_task_ok(vm['node'], taskid):
module.exit_json(changed=True, msg="VM %s removed" % vmid)
timeout -= 1
if timeout == 0:
Expand Down
3 changes: 1 addition & 2 deletions plugins/modules/cloud/misc/proxmox_kvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -866,8 +866,7 @@ def wait_for_task(self, node, taskid):
timeout = self.module.params['timeout']

while timeout:
task = self.proxmox_api.nodes(node).tasks(taskid).status.get()
if task['status'] == 'stopped' and task['exitstatus'] == 'OK':
if self.api_task_ok(node, taskid):
# Wait an extra second as the API can be a ahead of the hypervisor
time.sleep(1)
return True
Expand Down
111 changes: 103 additions & 8 deletions plugins/modules/cloud/misc/proxmox_snap.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@
- For removal from config file, even if removing disk snapshot fails.
default: false
type: bool
unbind:
description:
- This option only applies to LXC containers.
- Allows to snapshot a container even if it has configured mountpoints.
- Temporarily disables all configured mountpoints, takes snapshot, and finally restores original configuration.
- If running, the container will be stopped and restarted to apply config changes.
- Due to restrictions in the Proxmox API this option can only be used authenticating as C(root@pam) with I(api_password), API tokens do not work either.
- See U(https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config) (PUT tab) for more details.
default: false
type: bool
nxet marked this conversation as resolved.
Show resolved Hide resolved
version_added: 5.6.0
nxet marked this conversation as resolved.
Show resolved Hide resolved
vmstate:
description:
- Snapshot includes RAM.
Expand Down Expand Up @@ -78,6 +89,16 @@
state: present
snapname: pre-updates

- name: Create new snapshot for a container with configured mountpoints
community.general.proxmox_snap:
api_user: root@pam
api_password: 1q2w3e
api_host: node1
vmid: 100
state: present
unbind: true # requires root@pam+password auth, API tokens are not supported
snapname: pre-updates

- name: Remove container snapshot
community.general.proxmox_snap:
api_user: root@pam
Expand Down Expand Up @@ -110,24 +131,98 @@ class ProxmoxSnapAnsible(ProxmoxAnsible):
def snapshot(self, vm, vmid):
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).snapshot

def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate):
def vmconfig(self, vm, vmid):
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).config

def vmstatus(self, vm, vmid):
return getattr(self.proxmox_api.nodes(vm['node']), vm['type'])(vmid).status

def _container_mp_get(self, vm, vmid):
cfg = self.vmconfig(vm, vmid).get()
mountpoints = {}
for key, value in cfg.items():
if key.startswith('mp'):
mountpoints[key] = value
return mountpoints

def _container_mp_disable(self, vm, vmid, timeout, unbind, mountpoints, vmstatus):
# shutdown container if running
if vmstatus == 'running':
self.shutdown_instance(vm, vmid, timeout)
# delete all mountpoints configs
self.vmconfig(vm, vmid).put(delete=' '.join(mountpoints))

def _container_mp_restore(self, vm, vmid, timeout, unbind, mountpoints, vmstatus):
# NOTE: requires auth as `root@pam`, API tokens are not supported
# see https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/lxc/{vmid}/config
# restore original config
self.vmconfig(vm, vmid).put(**mountpoints)
# start container (if was running before snap)
if vmstatus == 'running':
self.start_instance(vm, vmid, timeout)

def start_instance(self, vm, vmid, timeout):
taskid = self.vmstatus(vm, vmid).start.post()
while timeout:
if self.api_task_ok(vm['node'], taskid):
return True
timeout -= 1
if timeout == 0:
self.module.fail_json(msg='Reached timeout while waiting for VM to start. Last line in task before timeout: %s' %
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
time.sleep(1)
return False

def shutdown_instance(self, vm, vmid, timeout):
taskid = self.vmstatus(vm, vmid).shutdown.post()
while timeout:
if self.api_task_ok(vm['node'], taskid):
return True
timeout -= 1
if timeout == 0:
self.module.fail_json(msg='Reached timeout while waiting for VM to stop. Last line in task before timeout: %s' %
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])
time.sleep(1)
return False

def snapshot_create(self, vm, vmid, timeout, snapname, description, vmstate, unbind):
if self.module.check_mode:
return True

if vm['type'] == 'lxc':
if unbind is True:
# check if credentials will work
# WARN: it is crucial this check runs here!
# The correct permissions are required only to reconfig mounts.
# Not checking now would allow to remove the configuration BUT
# fail later, leaving the container in a misconfigured state.
if (
self.module.params['api_user'] != 'root@pam'
or not self.module.params['api_password']
):
self.module.fail_json(msg='`unbind=True` requires authentication as `root@pam` with `api_password`, API tokens are not supported.')
return False
mountpoints = self._container_mp_get(vm, vmid)
vmstatus = self.vmstatus(vm, vmid).current().get()['status']
if mountpoints:
self._container_mp_disable(vm, vmid, timeout, unbind, mountpoints, vmstatus)
taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description)
else:
taskid = self.snapshot(vm, vmid).post(snapname=snapname, description=description, vmstate=int(vmstate))

while timeout:
status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK':
if self.api_task_ok(vm['node'], taskid):
if vm['type'] == 'lxc' and unbind is True and mountpoints:
self._container_mp_restore(vm, vmid, timeout, unbind, mountpoints, vmstatus)
return True
if timeout == 0:
self.module.fail_json(msg='Reached timeout while waiting for creating VM snapshot. Last line in task before timeout: %s' %
self.proxmox_api.nodes(vm['node']).tasks(taskid).log.get()[:1])

time.sleep(1)
timeout -= 1
if vm['type'] == 'lxc' and unbind is True and mountpoints:
self._container_mp_restore(vm, vmid, timeout, unbind, mountpoints, vmstatus)
return False

def snapshot_remove(self, vm, vmid, timeout, snapname, force):
Expand All @@ -136,8 +231,7 @@ def snapshot_remove(self, vm, vmid, timeout, snapname, force):

taskid = self.snapshot(vm, vmid).delete(snapname, force=int(force))
while timeout:
status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK':
if self.api_task_ok(vm['node'], taskid):
return True
if timeout == 0:
self.module.fail_json(msg='Reached timeout while waiting for removing VM snapshot. Last line in task before timeout: %s' %
Expand All @@ -153,8 +247,7 @@ def snapshot_rollback(self, vm, vmid, timeout, snapname):

taskid = self.snapshot(vm, vmid)(snapname).post("rollback")
while timeout:
status_data = self.proxmox_api.nodes(vm['node']).tasks(taskid).status.get()
if status_data['status'] == 'stopped' and status_data['exitstatus'] == 'OK':
if self.api_task_ok(vm['node'], taskid):
return True
if timeout == 0:
self.module.fail_json(msg='Reached timeout while waiting for rolling back VM snapshot. Last line in task before timeout: %s' %
Expand All @@ -175,6 +268,7 @@ def main():
description=dict(type='str'),
snapname=dict(type='str', default='ansible_snap'),
force=dict(type='bool', default=False),
unbind=dict(type='bool', default=False),
vmstate=dict(type='bool', default=False),
)
module_args.update(snap_args)
Expand All @@ -193,6 +287,7 @@ def main():
snapname = module.params['snapname']
timeout = module.params['timeout']
force = module.params['force']
unbind = module.params['unbind']
vmstate = module.params['vmstate']

# If hostname is set get the VM id from ProxmoxAPI
Expand All @@ -209,7 +304,7 @@ def main():
if i['name'] == snapname:
module.exit_json(changed=False, msg="Snapshot %s is already present" % snapname)

if proxmox.snapshot_create(vm, vmid, timeout, snapname, description, vmstate):
if proxmox.snapshot_create(vm, vmid, timeout, snapname, description, vmstate, unbind):
if module.check_mode:
module.exit_json(changed=False, msg="Snapshot %s would be created" % snapname)
else:
Expand Down
3 changes: 1 addition & 2 deletions plugins/modules/cloud/misc/proxmox_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,7 @@ def task_status(self, node, taskid, timeout):
Check the task status and wait until the task is completed or the timeout is reached.
"""
while timeout:
task_status = self.proxmox_api.nodes(node).tasks(taskid).status.get()
if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK':
if self.api_task_ok(node, taskid):
return True
timeout = timeout - 1
if timeout == 0:
Expand Down