Last active
September 29, 2023 21:02
-
-
Save PhrozenByte/7aefc19767f51103045d12538173d1b4 to your computer and use it in GitHub Desktop.
Creates snapshots of a btrfs filesytem and runs a given command.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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