diff --git a/.gitignore b/.gitignore
index 3b76227..ee77078 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,9 @@
venv
virt_back.egg-info
+
+*.tar
+*.tar.gz
+
+*.tar.*
+*.tar.gz.*
+*.gz.*
diff --git a/virt-back b/virt-back
index 761133f..3aa71b4 100755
--- a/virt-back
+++ b/virt-back
@@ -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("\n", xml)
+ with open(xmlfile, "w") as f:
+ f.write(xml)
+
+ # Updated regular expression to match file= and dev= within elements
+ disklist = re.findall(r"", 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