Skip to content

Instantly share code, notes, and snippets.

@PhrozenByte
Last active September 29, 2023 21:02
Show Gist options
  • Save PhrozenByte/7aefc19767f51103045d12538173d1b4 to your computer and use it in GitHub Desktop.
Save PhrozenByte/7aefc19767f51103045d12538173d1b4 to your computer and use it in GitHub Desktop.
Creates snapshots of a btrfs filesytem and runs a given command.
#!/bin/bash
# btrfs-snapshot-run
# Creates snapshots of a btrfs filesytem and runs a given command
#
# This script was created to ease backing up a btrfs filesystem. It expects
# Snapper's subvolume layout, with subvolumes directly below in the hierarchy
# of the top level subvolume (ID 5), all prefixed by '@'. Other subvolumes are
# being ignored.
#
# This script will create readonly snapshots for either all, or the provided
# subvolumes (pass one or more '--subvol SUBVOLUME' options) of the given btrfs
# filesystem (pass the 'BTRFS_DEVICE' argument). It then mounts these snapshots
# below either a temporary, or the provided mount point (pass '--mount MOUNT'),
# and runs the given command (pass as arguments: 'COMMAND [ARGUMENT]') from
# within this mount point. All temporarily created snapshots are deleted right
# after the command exits.
#
# If you want to mount the toplevel subvolume of the btrfs filesystem only,
# pass the '--root' option. The script will then create a readonly snapshot of
# the toplevel subvolume below the required '/.snapshots' directory instead,
# mount this snapshot at either a temporary, or the provided mount point, and
# run the given command.
#
# Copyright (C) 2022 Daniel Rudolf (<https://www.daniel-rudolf.de>)
# License: The MIT License <http://opensource.org/licenses/MIT>
#
# SPDX-License-Identifier: MIT
set -eu -o pipefail
APP_NAME="$(basename "${BASH_SOURCE[0]}")"
function print_usage {
echo "Usage:"
echo " $APP_NAME BTRFS_DEVICE [--subvol SUBVOLUME]... [--mount MOUNT] COMMAND [ARGUMENT]..."
echo " $APP_NAME BTRFS_DEVICE --root [--mount MOUNT] COMMAND [ARGUMENT]..."
}
function get_random {
tr -dc 'A-Za-z0-9' < /dev/urandom 2> /dev/null | head -c "$1" || true
}
function btrfs_subvolumes {
LC_ALL=C btrfs subvolume list "$1" \
| sed -ne 's/^ID [0-9]* gen [0-9]* top level [0-9]* path \(@[^/]*\)$/\1/p' \
| sed -e '/_snap-[A-Za-z0-9]\{10\}$/d' \
| sort -u
}
# prepare cleanup trap
TRAPS_EXIT=()
function trap_exit {
TRAPS_EXIT+=( "${*@Q}" )
}
function trap_exit_cleanup {
EXIT=$?
for (( INDEX=${#TRAPS_EXIT[@]}-1 ; INDEX >= 0 ; INDEX-- )); do
eval "${TRAPS_EXIT[$INDEX]}"
done
trap - ERR EXIT
exit $EXIT
}
trap 'trap_exit_cleanup' ERR EXIT
# read parameters
DEVICE=""
SUBVOLUMES=()
ROOT_SUBVOLUME=""
MOUNT=""
COMMAND=()
while [ $# -gt 0 ]; do
if [ "$1" == "--" ]; then
if [ -z "$DEVICE" ]; then
DEVICE="${2:-}"
COMMAND=( "${@:3}" )
else
COMMAND=( "${@:2}" )
fi
set --
break
fi
if [ "$1" == "--mount" ]; then
if [ $# -lt 2 ]; then
echo "Missing required argument for option '--mount': MOUNT" >&2
exit 1
fi
MOUNT="$2"
shift 2
elif [ "$1" == "--subvol" ]; then
if [ $# -lt 2 ]; then
echo "Missing required argument for option '--subvol': SUBVOLUME" >&2
exit 1
fi
SUBVOLUMES+=( "$2" )
shift 2
elif [ "$1" == "--root" ]; then
ROOT_SUBVOLUME="yes"
shift
elif [ -z "$DEVICE" ]; then
DEVICE="$1"
shift
else
COMMAND=( "$@" )
set --
fi
done
# check parameters
if [ -z "$DEVICE" ]; then
print_usage >&2
exit 1
elif [ ! -e "$DEVICE" ]; then
echo "Invalid btrfs block device '$DEVICE': No such file or directory" >&2
exit 1
else
DEVICE_TYPE="$(blkid --match-tag TYPE --output value "$DEVICE" 2> /dev/null || true)"
if [ -z "$DEVICE_TYPE" ]; then
echo "Invalid btrfs block device '$DEVICE': Not a btrfs block device" >&2
exit 1
elif [ "$DEVICE_TYPE" != "btrfs" ]; then
echo "Invalid btrfs block device '$DEVICE': Not a btrfs block device, but a '$DEVICE_TYPE' block device" >&2
exit 1
fi
fi
if [ "${#SUBVOLUMES[@]}" -gt 0 ] && [ -n "$ROOT_SUBVOLUME" ]; then
echo "Invalid options: '--subvol' and '--root' are mutually exclusive" >&2
exit 1
fi
if [ -n "$MOUNT" ]; then
if [ ! -e "$MOUNT" ]; then
echo "Invalid mount point '$MOUNT': No such file or directory" >&2
exit 1
elif [ ! -d "$MOUNT" ]; then
echo "Invalid mount point '$MOUNT': Not a directory" >&2
exit 1
elif [ -z "$(find "$MOUNT" -maxdepth 0 -empty)" ]; then
echo "Invalid mount point '$MOUNT': Directory is not empty" >&2
exit 1
fi
fi
if [ "${#COMMAND[@]}" -eq 0 ]; then
print_usage >&2
exit 1
elif ! which "${COMMAND[0]}" > /dev/null 2>&1; then
echo "Invalid command '${COMMAND[0]}': Command not found or not executable" >&2
exit 1
fi
# get random run ID
RUN_ID="$(get_random 10)"
while [ -e "/run/btrfs-snapshot-run/$RUN_ID" ]; do
RUN_ID="$(get_random 10)"
done
# mount btrfs device
if [ ! -d "/run/btrfs-snapshot-run" ]; then
mkdir -m 0700 "/run/btrfs-snapshot-run"
fi
mkdir -m 0700 "/run/btrfs-snapshot-run/$RUN_ID"
trap_exit rmdir "/run/btrfs-snapshot-run/$RUN_ID"
mount -o "subvol=/" "$DEVICE" "/run/btrfs-snapshot-run/$RUN_ID"
trap_exit umount -fl "/run/btrfs-snapshot-run/$RUN_ID"
# prepare subvolumes
if [ -n "$ROOT_SUBVOLUME" ]; then
# bail if '/.snapshots' directory doesn't exist
if [ ! -e "/run/btrfs-snapshot-run/$RUN_ID/.snapshots" ]; then
echo "Invalid btrfs block device '$DEVICE': Invalid '/.snapshots' directory: No such file or directory" >&2
exit 1
elif [ ! -d "/run/btrfs-snapshot-run/$RUN_ID/.snapshots" ]; then
echo "Invalid btrfs block device '$DEVICE': Invalid '/.snapshots' directory: Not a directory" >&2
exit 1
fi
elif [ ${#SUBVOLUMES[@]} -eq 0 ]; then
# read subvolumes to create snapshots of
readarray -t SUBVOLUMES < <(btrfs_subvolumes "/run/btrfs-snapshot-run/$RUN_ID")
# bail if there are no subvolumes
if [ ${#SUBVOLUMES[@]} -eq 0 ]; then
echo "Invalid btrfs block device '$DEVICE': No subvolumes found" >&2
exit 1
fi
else
# bail if invalid subvolumes were given
readarray -t INVALID_SUBVOLUMES < <(comm -13 \
<(btrfs_subvolumes "/run/btrfs-snapshot-run/$RUN_ID") \
<(printf '%s\n' "${SUBVOLUMES[@]}" | sort -u)
)
if [ "${#INVALID_SUBVOLUMES[@]}" -gt 0 ]; then
echo "Unknown btrfs subvolumes of device '$DEVICE': ${INVALID_SUBVOLUMES[*]}" >&2
exit 1
fi
fi
# create mount dir, if necessary
if [ -z "$MOUNT" ]; then
MOUNT="/run/btrfs-snapshot-run/${RUN_ID}_mount"
mkdir -m 0700 "$MOUNT"
trap_exit rmdir "$MOUNT"
fi
# create root snapshot
if [ -n "$ROOT_SUBVOLUME" ]; then
SNAPSHOT="snap-${RUN_ID}"
btrfs subvolume snapshot -r \
"/run/btrfs-snapshot-run/$RUN_ID" \
"/run/btrfs-snapshot-run/$RUN_ID/.snapshots/$SNAPSHOT"
trap_exit btrfs subvolume delete \
"/run/btrfs-snapshot-run/$RUN_ID/.snapshots/$SNAPSHOT"
mount -o "subvol=/.snapshots/$SNAPSHOT","ro" "$DEVICE" "$MOUNT"
trap_exit umount -fl "$MOUNT"
fi
# create snapshots
for SUBVOLUME in "${SUBVOLUMES[@]}"; do
SNAPSHOT="${SUBVOLUME}_snap-${RUN_ID}"
btrfs subvolume snapshot -r \
"/run/btrfs-snapshot-run/$RUN_ID/$SUBVOLUME" \
"/run/btrfs-snapshot-run/$RUN_ID/$SNAPSHOT"
trap_exit btrfs subvolume delete \
"/run/btrfs-snapshot-run/$RUN_ID/$SNAPSHOT"
mkdir "$MOUNT/$SUBVOLUME"
trap_exit rmdir "$MOUNT/$SUBVOLUME"
mount -o "subvol=/$SNAPSHOT","ro" "$DEVICE" "$MOUNT/$SUBVOLUME"
trap_exit umount -fl "$MOUNT/$SUBVOLUME"
done
# run command
cd "$MOUNT"
set +e +o pipefail
"$(which "${COMMAND[0]}")" "${COMMAND[@]:1}"
exit $?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment