Update backup scripts for remote borg repo, add zvol-iscsi backup, and fix bugs
- Update copyright year to 2026 - Point BORG_REPO to remote backup host - Fix RBD pid file name to avoid collision with images backup - Fix snapshot count typo and off-by-one error in RBD cleanup - Add error handling for rbd snap create - Fix prune log messages to reference domain instead of image - Add new kvm-backup-zvol-iscsi.sh for ZVol over iSCSI backups
This commit is contained in:
parent
2df48f6b77
commit
25f11d5535
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2022 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
|
||||
Copyright (c) 2026 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@ -16,4 +16,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
SOFTWARE.
|
||||
|
||||
@ -24,7 +24,7 @@ fi
|
||||
echo $BASHPID >"$PIDFILE"
|
||||
|
||||
# The borg repository we're backing up to.
|
||||
export BORG_REPO='/media/Storage/Backup/kvm'
|
||||
export BORG_REPO='root@10.0.0.5:/media/Storage/Backup/kvm'
|
||||
# If you have a passphrase for your repository,
|
||||
# set it here or you can use bash to retrieve it.
|
||||
# export BORG_PASSPHRASE=''
|
||||
@ -203,7 +203,7 @@ while read -r _ DOMAIN DOMSTATUS; do
|
||||
|
||||
# Prune if options are configured.
|
||||
if [[ -n "$PRUNE_OPTIONS" ]]; then
|
||||
echo "Pruning backups for $IMAGE"
|
||||
echo "Pruning xml backups for $DOMAIN"
|
||||
if ! eval borg prune --list \
|
||||
--show-rc \
|
||||
--glob-archives "'$DOMAIN-xml-*'" \
|
||||
|
||||
18
kvm-backup-rbd.sh
Executable file → Normal file
18
kvm-backup-rbd.sh
Executable file → Normal file
@ -4,7 +4,7 @@
|
||||
# This is for backing up Rados Block Device (Ceph) storage.
|
||||
|
||||
# A file to prevent overlapping runs.
|
||||
PIDFILE="/tmp/backup-image.pid"
|
||||
PIDFILE="/tmp/backup-rbd.pid"
|
||||
|
||||
# If the pid file exists and process is running, exit.
|
||||
if [[ -f "$PIDFILE" ]]; then
|
||||
@ -19,7 +19,7 @@ fi
|
||||
echo $BASHPID >"$PIDFILE"
|
||||
|
||||
# The borg repository we're backing up to.
|
||||
export BORG_REPO='/media/Storage/Backup/kvm'
|
||||
export BORG_REPO='root@10.0.0.5:/media/Storage/Backup/kvm'
|
||||
# If you have a passphrase for your repository,
|
||||
# set it here or you can use bash to retrieve it.
|
||||
# export BORG_PASSPHRASE=''
|
||||
@ -52,7 +52,7 @@ fail() {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# If the domain is running, commit the changes saved to the snapshot to the image to finish the backup.
|
||||
# Cleanup old RBD snapshots, keeping SNAPSHOTS_KEEP most recent.
|
||||
cleanupSnapshots() {
|
||||
IMAGE="$1"
|
||||
snapshots=()
|
||||
@ -82,10 +82,10 @@ cleanupSnapshots() {
|
||||
# remove snapshots until count matches.
|
||||
# The snapshots are listed from oldest to newest, so this
|
||||
# should keep the newer snapshots.
|
||||
snpashot_count=${#snapshots[@]}
|
||||
if ((snpashot_count>=SNAPSHOTS_KEEP)); then
|
||||
snapshot_count=${#snapshots[@]}
|
||||
if ((snapshot_count>SNAPSHOTS_KEEP)); then
|
||||
# Loop through snapshots until we removed enough to equal keep count.
|
||||
for ((i = 0; snpashot_count-i > SNAPSHOTS_KEEP; i++)); do
|
||||
for ((i = 0; snapshot_count-i > SNAPSHOTS_KEEP; i++)); do
|
||||
NAME=${snapshots[$i]}
|
||||
echo "Removing snapshot: $IMAGE@$NAME"
|
||||
# Remove snapshot.
|
||||
@ -156,7 +156,9 @@ while read -r _ DOMAIN _; do
|
||||
# BACKUP_NAME="${RBD_POOL}_${RBD_IMAGE}"
|
||||
|
||||
# Create a snapshot.
|
||||
rbd snap create "$IMAGE@$SNAPSHOT_NAME"
|
||||
if ! rbd snap create "$IMAGE@$SNAPSHOT_NAME"; then
|
||||
fail "Failed to create snapshot for $IMAGE"
|
||||
fi
|
||||
|
||||
# Export volume to borg backup.
|
||||
echo "Creating backup for $IMAGE"
|
||||
@ -195,7 +197,7 @@ while read -r _ DOMAIN _; do
|
||||
|
||||
# Prune if options are configured.
|
||||
if [[ -n "$PRUNE_OPTIONS" ]]; then
|
||||
echo "Pruning backups for $IMAGE"
|
||||
echo "Pruning xml backups for $DOMAIN"
|
||||
if ! eval borg prune --list \
|
||||
--show-rc \
|
||||
--glob-archives "'$DOMAIN-xml-*'" \
|
||||
|
||||
308
kvm-backup-zvol-iscsi.sh
Normal file
308
kvm-backup-zvol-iscsi.sh
Normal file
@ -0,0 +1,308 @@
|
||||
#!/bin/bash
|
||||
# Copyright (c) 2026 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
|
||||
|
||||
# This is for backing up ZFS zvol-backed VMs exported via iSCSI.
|
||||
# Runs on the ZFS server where the zvols live. VM domains are
|
||||
# discovered via virsh on remote libvirt hosts connected over iSCSI,
|
||||
# matching the domain-driven approach of the other kvm-backup scripts.
|
||||
|
||||
# A file to prevent overlapping runs.
|
||||
PIDFILE="/tmp/backup-zvol-iscsi.pid"
|
||||
|
||||
# If the pid file exists and process is running, exit.
|
||||
if [[ -f "$PIDFILE" ]]; then
|
||||
PID=$(cat "$PIDFILE")
|
||||
if ps -p "$PID" >/dev/null; then
|
||||
echo "Backup process already running, exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create a new pid file for this process.
|
||||
echo $BASHPID >"$PIDFILE"
|
||||
|
||||
# The borg repository we're backing up to.
|
||||
export BORG_REPO='root@10.0.0.5:/media/Storage/Backup/kvm'
|
||||
# If you have a passphrase for your repository,
|
||||
# set it here or you can use bash to retrieve it.
|
||||
# export BORG_PASSPHRASE=''
|
||||
# Set answers for automation.
|
||||
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
|
||||
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
|
||||
export BORG_CHECK_I_KNOW_WHAT_I_AM_DOING=NO
|
||||
export BORG_DELETE_I_KNOW_WHAT_I_AM_DOING=NO
|
||||
# Set to empty string to disable pruning.
|
||||
PRUNE_OPTIONS="--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
|
||||
|
||||
# Number of ZFS snapshots to keep per zvol.
|
||||
SNAPSHOTS_KEEP=2
|
||||
|
||||
# The parent ZFS dataset containing zvols to back up.
|
||||
ZFS_PARENT="tank/kvm"
|
||||
|
||||
# Remote libvirt hosts for VM discovery and XML backup via SSH.
|
||||
# Each entry is an SSH destination (e.g. "root@hostname").
|
||||
# The script will run virsh list and domblklist on each host to
|
||||
# discover VMs with iSCSI-attached zvols, then back up the
|
||||
# corresponding local zvols.
|
||||
REMOTE_HOSTS=("root@kiki" "root@gaming-pc")
|
||||
|
||||
# Whether to also discover and backup VMs from local libvirt.
|
||||
BACKUP_LOCAL=true
|
||||
|
||||
# Remove PID file on exit.
|
||||
cleanup() {
|
||||
rm "$PIDFILE"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Name the snapshot today's date.
|
||||
SNAPSHOT_NAME=$(date '+%Y-%m-%dT%H-%M-%S')
|
||||
|
||||
# Allows providing an argument of a domain to specifically backup.
|
||||
BACKUP_DOMAIN="$1"
|
||||
|
||||
# Failures should remove pid file and exit with status code 1.
|
||||
fail() {
|
||||
echo "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Cleanup old ZFS snapshots, keeping SNAPSHOTS_KEEP most recent.
|
||||
cleanupSnapshots() {
|
||||
ZVOL="$1"
|
||||
snapshots=()
|
||||
|
||||
# Read list of snapshots for the provided zvol.
|
||||
SNAPLIST_STATUS_TMP="/tmp/backup-zvol-snap-tmp"
|
||||
while read -r NAME; do
|
||||
[[ -z "$NAME" ]] && continue
|
||||
snapshots+=("$NAME")
|
||||
done < <(
|
||||
zfs list -t snapshot -o name -s creation -H -r "$ZVOL" 2>/dev/null
|
||||
echo "${PIPESTATUS[0]}" >"$SNAPLIST_STATUS_TMP"
|
||||
)
|
||||
|
||||
# Get status from the snapshot listing.
|
||||
status=1
|
||||
if [[ -f $SNAPLIST_STATUS_TMP ]]; then
|
||||
status=$(cat "$SNAPLIST_STATUS_TMP")
|
||||
rm "$SNAPLIST_STATUS_TMP"
|
||||
fi
|
||||
|
||||
# If status has an error, exit.
|
||||
if ((status!=0)); then
|
||||
fail "Snapshot listing failed for $ZVOL"
|
||||
fi
|
||||
|
||||
# If the snapshot count is more than the number to keep,
|
||||
# remove snapshots until count matches.
|
||||
# The snapshots are listed from oldest to newest, so this
|
||||
# should keep the newer snapshots.
|
||||
snapshot_count=${#snapshots[@]}
|
||||
if ((snapshot_count>SNAPSHOTS_KEEP)); then
|
||||
for ((i = 0; snapshot_count-i > SNAPSHOTS_KEEP; i++)); do
|
||||
NAME=${snapshots[$i]}
|
||||
echo "Removing snapshot: $NAME"
|
||||
zfs destroy "$NAME"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the zvol name from an iSCSI by-path device path.
|
||||
# Example input: /dev/disk/by-path/ip-10.0.100.6:3260-iscsi-iqn.2026-03.im.gec.host:MainServer-lun-0
|
||||
# Example output: MainServer
|
||||
# The zvol name is the target name portion of the IQN (after the last colon,
|
||||
# before the -lun- suffix).
|
||||
extractZvolName() {
|
||||
local PATH_STR="$1"
|
||||
# Strip everything up to "-iscsi-" to get the IQN and lun.
|
||||
local IQN="${PATH_STR##*-iscsi-}"
|
||||
# Strip the "-lun-*" suffix.
|
||||
IQN="${IQN%-lun-*}"
|
||||
# The target name is after the last colon.
|
||||
echo "${IQN##*:}"
|
||||
}
|
||||
|
||||
# Back up domains from a virsh source.
|
||||
# Usage: backupDomains "ssh_prefix" "host_label"
|
||||
# ssh_prefix: empty string for local virsh, or "ssh user@host" for remote.
|
||||
# host_label: label for log messages and archive naming (e.g. "local", "kiki").
|
||||
backupDomains() {
|
||||
local SSH_PREFIX="$1"
|
||||
local HOST_LABEL="$2"
|
||||
|
||||
DOMLIST_STATUS_TMP="/tmp/backup-zvol-domlist-$HOST_LABEL-tmp"
|
||||
while read -r _ DOMAIN _; do
|
||||
# If the domain is empty, skip.
|
||||
if [[ -z "$DOMAIN" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# If a backup domain was provided, only backup that domain.
|
||||
if [[ -n "$BACKUP_DOMAIN" ]] && [[ "$BACKUP_DOMAIN" != "$DOMAIN" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get the block devices for this domain.
|
||||
DEVS=()
|
||||
ZVOL_NAMES=()
|
||||
BLKLIST_STATUS_TMP="/tmp/backup-zvol-blklist-$HOST_LABEL-tmp"
|
||||
while read -r DEV IMAGE; do
|
||||
# Ignore empty line or no image.
|
||||
if [[ -z "$IMAGE" ]] || [[ "$IMAGE" == "-" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Only process iSCSI by-path devices.
|
||||
if ! [[ "$IMAGE" =~ -iscsi- ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract the zvol name from the iSCSI path.
|
||||
ZVOL_NAME=$(extractZvolName "$IMAGE")
|
||||
if [[ -z "$ZVOL_NAME" ]]; then
|
||||
echo "Warning: Could not extract zvol name from $IMAGE, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Verify the zvol exists locally.
|
||||
ZVOL="$ZFS_PARENT/$ZVOL_NAME"
|
||||
if ! zfs list "$ZVOL" &>/dev/null; then
|
||||
echo "Warning: zvol $ZVOL does not exist locally, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
DEVS+=("$DEV")
|
||||
ZVOL_NAMES+=("$ZVOL_NAME")
|
||||
done < <(
|
||||
if [[ -n "$SSH_PREFIX" ]]; then
|
||||
$SSH_PREFIX "virsh domblklist '$DOMAIN'" 2>/dev/null | tail -n +3
|
||||
else
|
||||
virsh domblklist "$DOMAIN" | tail -n +3
|
||||
fi
|
||||
echo "${PIPESTATUS[0]}" >"$BLKLIST_STATUS_TMP"
|
||||
)
|
||||
|
||||
# Get status from the block listing.
|
||||
status=1
|
||||
if [[ -f $BLKLIST_STATUS_TMP ]]; then
|
||||
status=$(cat "$BLKLIST_STATUS_TMP")
|
||||
rm "$BLKLIST_STATUS_TMP"
|
||||
fi
|
||||
|
||||
# If status has an error, exit.
|
||||
if ((status!=0)); then
|
||||
fail "Domain block listing failed for $DOMAIN ($HOST_LABEL)"
|
||||
fi
|
||||
|
||||
# For each iSCSI disk, snapshot the zvol and back it up.
|
||||
for ((i = 0; i < ${#DEVS[@]}; i++)); do
|
||||
DEV=${DEVS[$i]}
|
||||
ZVOL_NAME=${ZVOL_NAMES[$i]}
|
||||
ZVOL="$ZFS_PARENT/$ZVOL_NAME"
|
||||
|
||||
# Create a ZFS snapshot.
|
||||
echo "Creating snapshot: $ZVOL@$SNAPSHOT_NAME"
|
||||
if ! zfs snapshot "$ZVOL@$SNAPSHOT_NAME"; then
|
||||
fail "Failed to create snapshot for $ZVOL"
|
||||
fi
|
||||
|
||||
# Read the raw disk image from the snapshot's block device.
|
||||
# This produces a portable raw image that can be restored to
|
||||
# any virtual disk or converted with qemu-img, without
|
||||
# requiring ZFS on the restore target.
|
||||
SNAP_DEV="/dev/zvol/$ZVOL@$SNAPSHOT_NAME"
|
||||
echo "Creating backup for $DOMAIN ($DEV [$ZVOL])"
|
||||
if ! dd if="$SNAP_DEV" bs=4M status=none | pv | borg create \
|
||||
--verbose \
|
||||
--stats \
|
||||
--show-rc \
|
||||
"::$DOMAIN-$DEV-{now}" -; then
|
||||
fail "Failed to backup $DOMAIN ($DEV)"
|
||||
fi
|
||||
|
||||
# Prune if options are configured.
|
||||
if [[ -n "$PRUNE_OPTIONS" ]]; then
|
||||
echo "Pruning backups for $DOMAIN ($DEV)"
|
||||
if ! eval borg prune --list \
|
||||
--show-rc \
|
||||
--glob-archives "'$DOMAIN-$DEV-*'" \
|
||||
"$PRUNE_OPTIONS"; then
|
||||
fail "Failed to prune $DOMAIN ($DEV)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup old ZFS snapshots.
|
||||
cleanupSnapshots "$ZVOL"
|
||||
done
|
||||
|
||||
# Backup the domain XML.
|
||||
echo "Backing up $DOMAIN xml ($HOST_LABEL)"
|
||||
if [[ -n "$SSH_PREFIX" ]]; then
|
||||
if ! $SSH_PREFIX "virsh dumpxml '$DOMAIN'" 2>/dev/null | borg create \
|
||||
--verbose \
|
||||
--stats \
|
||||
--show-rc \
|
||||
"::$DOMAIN-xml-{now}" -; then
|
||||
fail "Failed to backup $DOMAIN xml"
|
||||
fi
|
||||
else
|
||||
if ! virsh dumpxml "$DOMAIN" | borg create \
|
||||
--verbose \
|
||||
--stats \
|
||||
--show-rc \
|
||||
"::$DOMAIN-xml-{now}" -; then
|
||||
fail "Failed to backup $DOMAIN xml"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prune if options are configured.
|
||||
if [[ -n "$PRUNE_OPTIONS" ]]; then
|
||||
echo "Pruning xml backups for $DOMAIN"
|
||||
if ! eval borg prune --list \
|
||||
--show-rc \
|
||||
--glob-archives "'$DOMAIN-xml-*'" \
|
||||
"$PRUNE_OPTIONS"; then
|
||||
fail "Failed to prune $DOMAIN xml"
|
||||
fi
|
||||
fi
|
||||
done < <(
|
||||
if [[ -n "$SSH_PREFIX" ]]; then
|
||||
$SSH_PREFIX "virsh list --all" 2>/dev/null | tail -n +3
|
||||
else
|
||||
virsh list --all | tail -n +3
|
||||
fi
|
||||
echo "${PIPESTATUS[0]}" >"$DOMLIST_STATUS_TMP"
|
||||
)
|
||||
|
||||
# Get status from the domain listing.
|
||||
status=1
|
||||
if [[ -f $DOMLIST_STATUS_TMP ]]; then
|
||||
status=$(cat "$DOMLIST_STATUS_TMP")
|
||||
rm "$DOMLIST_STATUS_TMP"
|
||||
fi
|
||||
|
||||
# If status has an error, exit.
|
||||
if ((status!=0)); then
|
||||
if [[ -n "$SSH_PREFIX" ]]; then
|
||||
echo "Warning: Domain listing from $HOST_LABEL failed (host may be unreachable)"
|
||||
else
|
||||
fail "Local domain listing failed"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Backup domains from local libvirt.
|
||||
if [[ "$BACKUP_LOCAL" == true ]]; then
|
||||
backupDomains "" "local"
|
||||
fi
|
||||
|
||||
# Backup domains from remote libvirt hosts.
|
||||
for REMOTE_HOST in "${REMOTE_HOSTS[@]}"; do
|
||||
REMOTE_NAME="${REMOTE_HOST##*@}"
|
||||
backupDomains "ssh $REMOTE_HOST" "$REMOTE_NAME"
|
||||
done
|
||||
|
||||
# Shrink repo.
|
||||
borg compact
|
||||
Loading…
x
Reference in New Issue
Block a user