Files
Workstation/ansible/btrfs_backups/templates/btrfs_backup.sh.j2
ducoterra 8fbf384b2d Add fedora compatibility
Add fedora installation for Framework.
2022-02-06 17:29:55 -05:00

252 lines
7.8 KiB
Django/Jinja
Executable File

#!/bin/bash
# 1. Create uuid for backup mount
# 2. Unlock luks-uuid
# 3. Create /tmp/uuid
# 4. Mount /tmp/uuid
# 5. btrfs send
# 5.5 Update
# 6. umount
# 7. rm /tmp/uuid
# 8. luksclose
function exit_success {
# Unmount /tmp/uuid
log "INFO" "Unmounting $BACKUP_DRIVE_MNT"
umount $BACKUP_DRIVE_MNT
# Exit
exit 0
}
function exit_fail {
# Unmount /tmp/uuid
log "INFO" "Unmounting $BACKUP_DRIVE_MNT"
umount $BACKUP_DRIVE_MNT
# Exit
exit 1
}
function get_latest {
DIR=$1
if [ -f $DIR/$LATEST ]; then
echo $(cat $DIR/$LATEST)
else
echo ""
fi
}
function update_latest {
DIR=$1
NAME=$2
echo $2 > $DIR/$LATEST
}
function log {
LEVEL=$1
MESSAGE=$2
echo "$LEVEL: $MESSAGE"
}
function notify {
LEVEL=$1
MESSAGE=$2
log "$LEVEL" "$MESSAGE"
sudo -E -u $USER notify-send "$LEVEL" "$MESSAGE"
}
# Backup info
export BACKUP_DRIVE_UUID={{ disk.uuid }}
export BACKUP_DRIVE_PASSWORD={{ disk.password }}
export BACKUP_DRIVE_TMP_UUID=$(uuidgen)
export BACKUP_DRIVE_NAME=luks-$BACKUP_DRIVE_UUID
# For notifications
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{{ notifications.user.uid }}/bus
export USER={{ notifications.user.name }}
export SOURCE_DIR=${SOURCE_DIR:=/}
# Fix basename / showing up as "/" -> change to "root"
if [ $(basename $SOURCE_DIR) = / ]; then
export SNAPSHOT_PREFIX=${SNAPSHOT_PREFIX:=root}
else
export SNAPSHOT_PREFIX=${SNAPSHOT_PREFIX:=$(basename $SOURCE_DIR)}
fi
# Set snapshot prefix based on basename
export SNAPSHOT_TIME=$(date +"%y_%m_%d-%H.%M")
export SNAPSHOT_NAME=$SNAPSHOT_PREFIX-$SNAPSHOT_TIME
export SNAPSHOT_DIR=${SNAPSHOT_DIR:=/.snapshots}
export LATEST=$SNAPSHOT_PREFIX-latest
export BACKUP_DRIVE_MNT=/tmp/$BACKUP_DRIVE_TMP_UUID
export BACKUP_DIR=${BACKUP_DIR:=$BACKUP_DRIVE_MNT/$(hostname)}
# Show snapshot settings
echo "SOURCE_DIR" "$SOURCE_DIR"
echo "SNAPSHOT_PREFIX" "$SNAPSHOT_PREFIX"
echo "SNAPSHOT_TIME" "$SNAPSHOT_TIME"
echo "SNAPSHOT_NAME" "$SNAPSHOT_NAME"
echo "SNAPSHOT_DIR" "$SNAPSHOT_DIR"
echo "LATEST" "$LATEST"
echo "BACKUP_DRIVE_MNT" "$BACKUP_DRIVE_MNT"
echo "BACKUP_DIR" "$BACKUP_DIR"
# Create readonly snapshot
log "INFO" "Creating snapshot from $SOURCE_DIR as $SNAPSHOT_DIR/$SNAPSHOT_NAME"
if [ -d $SNAPSHOT_DIR/$SNAPSHOT_NAME ]; then
log "WARN" "Snapshot $SNAPSHOT_DIR/$SNAPSHOT_NAME already created. Skipping"
else
btrfs subvolume snapshot -r $SOURCE_DIR $SNAPSHOT_DIR/$SNAPSHOT_NAME
fi
# Update latest in snapshot dir
log "INFO" "Updating latest in $SNAPSHOT_DIR to $SNAPSHOT_NAME."
update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME
# Unlock backup drive
if [ -L /dev/disk/by-uuid/$BACKUP_DRIVE_UUID ]; then
cryptsetup luksOpen /dev/disk/by-uuid/$BACKUP_DRIVE_UUID $BACKUP_DRIVE_NAME --key-file=$BACKUP_DRIVE_PASSWORD
cryptsetup status /dev/mapper/$BACKUP_DRIVE_NAME
else
log "INFO" "Backup drive $BACKUP_DRIVE_UUID not found"
log "INFO" "Snapshot $SNAPSHOT_NAME completed successfully."
notify "WARN" "Drive $BACKUP_DRIVE_UUID could not be found. Snapshot completed without backup."
exit 0
fi
if [ $? = 0 ]; then
log "INFO" "Drive $BACKUP_DRIVE_UUID unlocked"
else
notify "ERROR" "Drive $BACKUP_DRIVE_UUID could not be unlocked."
exit_fail
fi
# Create /tmp/uuid
log "INFO" "Creating $BACKUP_DRIVE_MNT"
mkdir $BACKUP_DRIVE_MNT
# Mount /tmp/uuid
log "INFO" "Mounting /dev/mapper/$BACKUP_DRIVE_NAME"
mount -t btrfs -o compress=zstd /dev/mapper/$BACKUP_DRIVE_NAME $BACKUP_DRIVE_MNT
if [ $? = 0 ]; then
log "INFO" "Drive $BACKUP_DRIVE_UUID mounted at $BACKUP_DRIVE_MNT"
else
notify "ERROR" "Drive $BACKUP_DRIVE_NAME could not be mounted."
exit_fail
fi
# First check if the snapshot dir has a "latest" snapshot
# This will be needed to send an incremental snapshot
LATEST_SNAPSHOT="$(get_latest $SNAPSHOT_DIR)"
log "INFO" "Latest snapshot is $LATEST_SNAPSHOT"
# Next, check if the backup drive has a "latest" snapshot
LATEST_BACKUP="$(get_latest $BACKUP_DIR)"
log "INFO" "Latest backup is $LATEST_BACKUP"
# Now check if the "latest" snapshots match
# btrfs requires both the sending drive and receiving drive have
# matching parent snapshots.
#
# There are a few scenarios to cover
# 1. Neither the backup drive nor the local snapshot dir have a "latest"
# This can happen if the backup occurs before any snapshots are
# taken. Don't send anything.
# 2. The backup drive has a "latest" but the snapshot dir doesn't
# This can happen when the local drive is restored from backup
# but the snapshot dir didn't copy over. nothing to send.
# 3. The backup drive and snapshot dir have a "latest" and they are the
# same.
# Send backup with parent as normal.
# 4. The snapshot dir has a "latest" but the backup drive doesn't
# This can happen when backing up for the first time. Send the
# snapshot without a parent
# 5. Both the snapshot dir and backup drive have a latest, but they are
# out of sync.
# This can happen when snapshots are taken with the backup drive
# disconnected. There's a few sub-scenarios here:
# a. The snapshot dir has the "latest" snapshot from the backup dir,
# it's just older than the "latest" snapshot in the snapshot dir
# Re-sync the "latest" snapshot dir with the one in the
# backup dir. Send as normal with parents.
# b. The snapshot dir does not have the "latest" snapshot from the
# backup dir.
# Here be dragons. Something went wrong and will likely need
# to be manually reconfigured. Raise a critical alert.
# Scenario 1 and 2
if [ "$LATEST_SNAPSHOT" = "" ]; then
notify "WARN" "Neither the snapshot dir nor the backup drive has a 'latest' snapshot."
exit_success
fi
# Scenario 3
if [ "$LATEST_SNAPSHOT" = "$LATEST_BACKUP" ]; then
log "INFO" "Proceeding with backups as normal."
# Send incremental snapshot
btrfs send -p $SNAPSHOT_DIR/$LATEST_SNAPSHOT $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR
if [ $? != 0 ]; then
notify "ERROR" "btrfs send -p $SNAPSHOT_DIR/$LATEST_SNAPSHOT $SNAPSHOT_DIR/$SNAPSHOT_NAME failed."
exit_fail
fi
# Update latest in backup dir
update_latest $BACKUP_DIR $SNAPSHOT_NAME
# Update latest in snapshot dir
update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME
# Exit
sudo -E -u $USER notify-send "Backup completed" "INFO: Backup $SNAPSHOT_NAME completed successfully."
exit_success
fi
# Scenario 4
if [ "$LATEST_BACKUP" = "" ]; then
log "INFO" "No prior backups detected. Sending full backup."
# Send incremental snapshot
btrfs send $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR
if [ $? != 0 ]; then
notify "ERROR" "btrfs send $SNAPSHOT_DIR/$SNAPSHOT_NAME failed."
exit_fail
fi
# Update latest in backup dir
update_latest $BACKUP_DIR $SNAPSHOT_NAME
# Update latest in snapshot dir
update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME
# Exit
notify "INFO" "Backup $SNAPSHOT_NAME completed successfully."
exit_success
fi
# Scenario 5a
log "INFO" "Detected drift. Attempting to synchronize latest snapshot with backup. Set to $LATEST_BACKUP."
if [ -d $SNAPSHOT_DIR/$LATEST_BACKUP ]; then
log "INFO" "$LATEST_BACKUP found in snapshot dir. Synchronizing and proceeding."
btrfs send -p $SNAPSHOT_DIR/$LATEST_BACKUP $SNAPSHOT_DIR/$SNAPSHOT_NAME | btrfs receive $BACKUP_DIR
if [ $? != 0 ]; then
notify "ERROR" "btrfs send -p $SNAPSHOT_DIR/$LATEST_SNAPSHOT $SNAPSHOT_DIR/$SNAPSHOT_NAME failed."
exit_fail
fi
# Update latest in backup dir
update_latest $BACKUP_DIR $SNAPSHOT_NAME
# Update latest in snapshot dir
update_latest $SNAPSHOT_DIR $SNAPSHOT_NAME
# Exit
notify "INFO" "Backup $SNAPSHOT_NAME completed successfully."
exit_success
# Scenario 5b
else
log "ERROR" "Something went wrong. $LATEST_BACKUP not found in $SNAPSHOT_DIR."
notify "ERROR" "$LATEST_BACKUP not found in $SNAPSHOT_DIR."
exit_fail
fi