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:
parent
87d6979d7b
commit
fd1592c3b1
2 changed files with 98 additions and 17 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -1,2 +1,9 @@
|
||||||
venv
|
venv
|
||||||
virt_back.egg-info
|
virt_back.egg-info
|
||||||
|
|
||||||
|
*.tar
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
*.tar.*
|
||||||
|
*.tar.gz.*
|
||||||
|
*.gz.*
|
||||||
|
|
|
||||||
108
virt-back
108
virt-back
|
|
@ -22,7 +22,7 @@ Use the Domfetcher class to aquire lists of dom objects."""
|
||||||
import libvirt
|
import libvirt
|
||||||
import tarfile
|
import tarfile
|
||||||
import syslog
|
import syslog
|
||||||
from re import findall
|
import re
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
|
@ -31,6 +31,8 @@ from os import path
|
||||||
from os import remove
|
from os import remove
|
||||||
from shutil import move, copy2
|
from shutil import move, copy2
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from operator import methodcaller
|
from operator import methodcaller
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -118,10 +120,11 @@ def backup(doms):
|
||||||
|
|
||||||
xml = dom.XMLDesc(0)
|
xml = dom.XMLDesc(0)
|
||||||
xmlfile = path.join(options.backpath, dom.name() + ".xml")
|
xmlfile = path.join(options.backpath, dom.name() + ".xml")
|
||||||
f = open(xmlfile, "w")
|
with open(xmlfile, "w") as f:
|
||||||
f.write(xml)
|
f.write(xml)
|
||||||
f.close()
|
|
||||||
disklist = findall("<source file='(.*)'/>\n", 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())
|
logit("backup", "invoking backup for " + dom.name())
|
||||||
|
|
||||||
|
|
@ -132,14 +135,55 @@ def backup(doms):
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
disk_file = disk_source.split("/")[-1]
|
if is_zfs_dataset(disk_source):
|
||||||
disk_dest = path.join(options.backpath, disk_file)
|
# 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(
|
# Increment snapshot name until an available name is found
|
||||||
"backup",
|
while subprocess.run(["zfs", "list", zfs_snapshot], stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode == 0:
|
||||||
"copying %s to %s for %s" % (disk_source, disk_dest, dom.name()),
|
zfs_snapshot = f"{zfs_snapshot_base}-{suffix}"
|
||||||
)
|
suffix += 1
|
||||||
copy2(disk_source, disk_dest)
|
|
||||||
|
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
|
if recreate: # if true, start guest after backup
|
||||||
create([dom]) # start dom
|
create([dom]) # start dom
|
||||||
|
|
@ -169,17 +213,47 @@ def backup(doms):
|
||||||
if disk_source.endswith(".iso"):
|
if disk_source.endswith(".iso"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
disk_file = disk_source.split("/")[-1]
|
if is_zfs_dataset(disk_source):
|
||||||
disk_dest = path.join(options.backpath, disk_file)
|
zfs_file = path.join(options.backpath, f"{dom.name()}-{TODAY}.zfs")
|
||||||
logit("backup", "archiving %s for %s" % (disk_dest, dom.name()))
|
if not options.nogzip:
|
||||||
tar.add(disk_dest) # add img to tar
|
zfs_file += ".gz"
|
||||||
remove(disk_dest) # cleanup tmp files
|
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()
|
tar.close()
|
||||||
|
|
||||||
logit("backup", "finished backup for " + dom.name())
|
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):
|
def shutdown(doms, wait=180):
|
||||||
"""Accept a list of dom objects, attempt to shutdown the active ones"""
|
"""Accept a list of dom objects, attempt to shutdown the active ones"""
|
||||||
# get all running guests from list and invoke shutdown
|
# get all running guests from list and invoke shutdown
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue