incus-copy.sh: add -r <src-remote> (pull all of a remote's instances to the local host) and -p <dest-project> (copy local instances into another local project/pool, for the on-host backup leg on a second disk). Copy all instances instead of only running ones, abort cleanly if the instance list fails, and keep boot.autostart=false on every replica. incus-backup.sh: add a local destination mode (-d local, now the default) that writes dumps/rsyncs straight to the backup root without ssh; add -b to override the /backup root, flock against overlapping runs, set -u, non-zero exit when any step fails, and fix the invalid `incus exec -n` flag that made MariaDB dumps fail on modern incus. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
3.6 KiB
Bash
Executable File
107 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Backup instances FS and DBs, driven by a JSON manifest (see
|
|
# incus-backup.db: {"<instance>": {"DB": [...], "FS": [...]}, ...}).
|
|
# Destination is either a remote (rsync over ssh) or, with -d local,
|
|
# a local directory (e.g. a dedicated backup zpool mounted on /backup).
|
|
#
|
|
# Usage: incus-backup.sh -f <manifest> -s <storage-pool>
|
|
# [-d <dest-host|local>] [-u <ssh-user>] [-i <ssh-key>]
|
|
# [-p <ssh-port>] [-b <backup-root>]
|
|
#
|
|
# Layout under <backup-root> (default /backup): <hostname>/<instance>/
|
|
# plus <hostname>/incus-{local,global}-db.sql
|
|
|
|
set -u
|
|
|
|
DEST=local
|
|
SSH_USER=""
|
|
SSH_KEY=""
|
|
SSH_PORT=22
|
|
DB_FILE=""
|
|
STORAGE_POOL=""
|
|
BKP_ROOT=/backup
|
|
LOCKFILE=/run/lock/incus-backup.lock
|
|
|
|
usage() {
|
|
echo "Usage: $0 -f <manifest.db> -s <storage-pool> [-d <dest-host|local>] [-u <ssh-user>] [-i <ssh-key>] [-p <ssh-port>] [-b <backup-root>]" >&2
|
|
exit 2
|
|
}
|
|
|
|
while getopts d:u:i:p:f:s:b: flag; do
|
|
case "${flag}" in
|
|
d) DEST=${OPTARG};;
|
|
u) SSH_USER=${OPTARG};;
|
|
i) SSH_KEY=${OPTARG};;
|
|
p) SSH_PORT=${OPTARG};;
|
|
f) DB_FILE=${OPTARG};;
|
|
s) STORAGE_POOL=${OPTARG};;
|
|
b) BKP_ROOT=${OPTARG};;
|
|
*) usage;;
|
|
esac
|
|
done
|
|
[ -n "$DB_FILE" ] && [ -n "$STORAGE_POOL" ] || usage
|
|
if [ "$DEST" != "local" ]; then
|
|
[ -n "$SSH_USER" ] && [ -n "$SSH_KEY" ] || usage
|
|
fi
|
|
|
|
HOST=$(hostname -s)
|
|
BKP_DIR="${BKP_ROOT}/${HOST}"
|
|
CT_PREFIX="/var/lib/incus/storage-pools/${STORAGE_POOL}/containers"
|
|
|
|
# save <dst-file> — write stdin to a file at the destination
|
|
# transfer <opts> <src> <dst-dir> — rsync a path to the destination
|
|
if [ "$DEST" = "local" ]; then
|
|
save() { mkdir -p "$(dirname "$1")" && cat - > "$1"; }
|
|
transfer() { mkdir -p "$3" && /usr/bin/rsync $1 "$2" "$3"; }
|
|
else
|
|
SSH_CMD="ssh -i ${SSH_KEY} -p ${SSH_PORT}"
|
|
save() { ${SSH_CMD} "${SSH_USER}@${DEST}" "mkdir -p $(dirname "$1") && cat - > $1"; }
|
|
transfer() { /usr/bin/rsync $1 -e "${SSH_CMD}" "$2" "${SSH_USER}@${DEST}:$3"; }
|
|
fi
|
|
|
|
# refuse to overlap with a previous, still-running invocation
|
|
exec 9> "$LOCKFILE"
|
|
if ! flock -n 9; then
|
|
echo "another incus-backup run holds $LOCKFILE, aborting" >&2
|
|
exit 1
|
|
fi
|
|
|
|
RC=0
|
|
|
|
# Backup incus DB
|
|
/usr/bin/incus admin sql local .dump | save "${BKP_DIR}/incus-local-db.sql" || RC=1
|
|
/usr/bin/incus admin sql global .dump | save "${BKP_DIR}/incus-global-db.sql" || RC=1
|
|
|
|
for CT in $(jq -r 'keys[]' "${DB_FILE}") ; do
|
|
SRC_DIR="${CT_PREFIX}/${CT}"
|
|
DST_DIR="${BKP_DIR}/${CT}"
|
|
|
|
# Backup container info
|
|
if [ -f "${SRC_DIR}/backup.yaml" ] ; then
|
|
echo "[$(date '+%F %T')] Starting $CT backup.yaml"
|
|
transfer "-a --del" "${SRC_DIR}/backup.yaml" "${DST_DIR}/" || RC=1
|
|
fi
|
|
|
|
# Backup Mysql dumps
|
|
for DB in $(jq -r ".${CT} | select(.DB != null) | .DB[]" "${DB_FILE}") ; do
|
|
echo "[$(date '+%F %T')] Starting $CT $DB database backup"
|
|
/usr/bin/incus exec "$CT" -- mariadb-dump --single-transaction --databases "${DB}" \
|
|
| save "${DST_DIR}/mysql-${DB}.sql" || RC=1
|
|
done
|
|
|
|
# Backup container rootfs paths
|
|
for FS in $(jq -r ".${CT} | select(.FS != null) | .FS[]" "${DB_FILE}") ; do
|
|
# Skip missing rootfs dir
|
|
if [ ! -d "${SRC_DIR}/rootfs" ] ; then
|
|
echo "[$(date '+%F %T')] WARNING : Missing rootfs for container ${CT}, skipping FS ${FS} ..."
|
|
continue
|
|
fi
|
|
# "/." used by rsync to limit the amount of path information that is sent as implied directories
|
|
echo "[$(date '+%F %T')] Starting $CT $FS filesystem backup"
|
|
transfer "-aR --del" "${SRC_DIR}/rootfs/.${FS}" "${DST_DIR}/" || RC=1
|
|
done
|
|
done
|
|
|
|
exit $RC
|