zfs snapshots?! awe yis!

now I just need a straight forward way to `virt-restore` ...

	modified:   .gitignore
	modified:   virt-back
This commit is contained in:
Russell Ballestrini 2024-07-07 19:22:37 -04:00
parent 87d6979d7b
commit fd1592c3b1
2 changed files with 98 additions and 17 deletions

7
.gitignore vendored
View file

@ -1,2 +1,9 @@
venv
virt_back.egg-info
*.tar
*.tar.gz
*.tar.*
*.tar.gz.*
*.gz.*

108
virt-back
View file

@ -22,7 +22,7 @@ Use the Domfetcher class to aquire lists of dom objects."""
import libvirt
import tarfile
import syslog
from re import findall
import re
from time import sleep
from datetime import date
from sys import exit
@ -31,6 +31,8 @@ from os import path
from os import remove
from shutil import move, copy2
import subprocess
try:
from operator import methodcaller
except ImportError:
@ -118,10 +120,11 @@ def backup(doms):
xml = dom.XMLDesc(0)
xmlfile = path.join(options.backpath, dom.name() + ".xml")
f = open(xmlfile, "w")
f.write(xml)
f.close()
disklist = findall("<source file='(.*)'/>\n", xml)
with open(xmlfile, "w") as f:
f.write(xml)
# Updated regular expression to match file= and dev= within <disk> elements
disklist = re.findall(r"<disk.*?<source (?:file|dev)='(.*?)'.*?</disk>", xml, re.DOTALL)
logit("backup", "invoking backup for " + dom.name())
@ -132,14 +135,55 @@ def backup(doms):
)
continue
disk_file = disk_source.split("/")[-1]
disk_dest = path.join(options.backpath, disk_file)
if is_zfs_dataset(disk_source):
# Handle ZFS dataset
zfs_dataset = disk_source[len('/dev/zvol/'):] if disk_source.startswith('/dev/zvol/') else disk_source
zfs_snapshot_base = f"{zfs_dataset}@backup-{TODAY}"
zfs_snapshot = zfs_snapshot_base
suffix = 1
logit(
"backup",
"copying %s to %s for %s" % (disk_source, disk_dest, dom.name()),
)
copy2(disk_source, disk_dest)
# Increment snapshot name until an available name is found
while subprocess.run(["zfs", "list", zfs_snapshot], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
zfs_snapshot = f"{zfs_snapshot_base}-{suffix}"
suffix += 1
zfs_file = path.join(options.backpath, f"{dom.name()}-{TODAY}.zfs")
if not options.nogzip:
zfs_file += ".gz"
# Create ZFS snapshot
logit("backup", f"creating ZFS snapshot {zfs_snapshot} for {dom.name()}")
try:
subprocess.run(["zfs", "snapshot", zfs_snapshot], check=True)
except subprocess.CalledProcessError as e:
logit("error", f"Failed to create ZFS snapshot {zfs_snapshot}: {e}")
continue
# Send ZFS snapshot to file with optional compression
logit("backup", f"sending ZFS snapshot {zfs_snapshot} to {zfs_file} for {dom.name()}")
try:
with open(zfs_file, "wb") as f:
if options.nogzip:
subprocess.run(["zfs", "send", zfs_snapshot], stdout=f, check=True)
else:
send_proc = subprocess.Popen(["zfs", "send", zfs_snapshot], stdout=subprocess.PIPE)
gzip_proc = subprocess.Popen(["gzip"], stdin=send_proc.stdout, stdout=f)
send_proc.stdout.close() # Allow send_proc to receive a SIGPIPE if gzip_proc exits
gzip_proc.communicate()
except subprocess.CalledProcessError as e:
logit("error", f"Failed to send ZFS snapshot {zfs_snapshot}: {e}")
continue
else:
# Handle QCOW2 or other file-based disk
logit("backup", f"{disk_source} is not a ZFS dataset")
disk_file = disk_source.split("/")[-1]
disk_dest = path.join(options.backpath, disk_file)
logit(
"backup",
"copying %s to %s for %s" % (disk_source, disk_dest, dom.name()),
)
copy2(disk_source, disk_dest)
if recreate: # if true, start guest after backup
create([dom]) # start dom
@ -169,17 +213,47 @@ def backup(doms):
if disk_source.endswith(".iso"):
continue
disk_file = disk_source.split("/")[-1]
disk_dest = path.join(options.backpath, disk_file)
logit("backup", "archiving %s for %s" % (disk_dest, dom.name()))
tar.add(disk_dest) # add img to tar
remove(disk_dest) # cleanup tmp files
if is_zfs_dataset(disk_source):
zfs_file = path.join(options.backpath, f"{dom.name()}-{TODAY}.zfs")
if not options.nogzip:
zfs_file += ".gz"
if path.isfile(zfs_file):
logit("backup", "archiving %s for %s" % (zfs_file, dom.name()))
tar.add(zfs_file) # add zfs snapshot to tar
remove(zfs_file) # cleanup tmp files
else:
logit("error", f"ZFS snapshot file {zfs_file} not found for {dom.name()}")
else:
disk_file = disk_source.split("/")[-1]
disk_dest = path.join(options.backpath, disk_file)
logit("backup", "archiving %s for %s" % (disk_dest, dom.name()))
tar.add(disk_dest) # add img to tar
remove(disk_dest) # cleanup tmp files
tar.close()
logit("backup", "finished backup for " + dom.name())
def is_zfs_dataset(disk_source):
"""Check if the disk source is a ZFS dataset"""
logit("backup", f"checking if {disk_source} is a ZFS dataset")
try:
# Extract the ZFS dataset name from the device path
if disk_source.startswith('/dev/zvol/'):
zfs_dataset = disk_source[len('/dev/zvol/'):]
else:
zfs_dataset = disk_source
result = subprocess.run(["zfs", "list", zfs_dataset], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
is_zfs = result.returncode == 0
logit("backup", f"{zfs_dataset} is {'a' if is_zfs else 'not a'} ZFS dataset")
return is_zfs
except Exception as e:
logit("error", f"Error checking ZFS dataset: {e}")
return False
def shutdown(doms, wait=180):
"""Accept a list of dom objects, attempt to shutdown the active ones"""
# get all running guests from list and invoke shutdown