#!/bin/bash # # Backup instances FS and DBs, driven by a JSON manifest (see # incus-backup.db: {"": {"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 -s # [-d ] [-u ] [-i ] # [-p ] [-b ] # # Layout under (default /backup): // # plus /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 -s [-d ] [-u ] [-i ] [-p ] [-b ]" >&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 — write stdin to a file at the destination # transfer — 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