Skip to content

Instantly share code, notes, and snippets.

@stackcoder
Last active October 1, 2024 18:56
Show Gist options
  • Save stackcoder/ccb3b17812ed11700ee83d762b970b98 to your computer and use it in GitHub Desktop.
Save stackcoder/ccb3b17812ed11700ee83d762b970b98 to your computer and use it in GitHub Desktop.
Atomic restic backup using zfs snapshots
#!/usr/bin/env bash
set -euo pipefail
# dataset to backup
dataset="my-pool/data"
# destroy leftover snapshots, usually already cleaned up by
for snap in $(zfs list -rt snap -Ho name "${dataset}"); do
if [[ "${snap}" =~ @restic-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$ ]]; then
zfs destroy "${snap}" && echo "leftover zfs snapshot '${snap}' destroyed"
fi
done
# create temporary snapshot for backup
snapshot="restic-$(cat /proc/sys/kernel/random/uuid)"
zfs snap -r "${dataset}@$snapshot"
# clean up temporary snapshot
_clean() {
zfs destroy -r "${dataset}@$snapshot" && echo "zfs snapshot '${dataset}@$snapshot' destroyed"
}
trap _clean EXIT
# get snapshot mountpoints of (sub) datasets
restic_args=()
for ds in $(zfs list -r -Ho name "${dataset}"); do
if [[ "$(zfs get -Ho value mountpoint "$ds")" == "none" ]]; then
continue
fi
path=$(findmnt -nr -o target -S "$ds")
snap="${path}/.zfs/snapshot/$snapshot"
restic_args+=( "${snap}" )
done
# backup temporary snapshot
restic backup \
--exclude-caches \
"${restic_args[@]}"
#!/usr/bin/env bash
set -euo pipefail
# dataset to backup
export ZFS_DATASET="my-pool/data"
# destroy leftover snapshots, usually already cleaned up
if [ -z "${EXEC_UNSHARED:-}" ]; then
for snap in $(zfs list -rt snap -Ho name "${ZFS_DATASET}"); do
if [[ "${snap}" =~ @restic-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$ ]]; then
zfs destroy "${snap}" && echo "leftover zfs snapshot '${snap}' destroyed"
fi
done
fi
# create temporary snapshot for backup
if [[ -z "${ZFS_SNAP:-}" ]]; then
ZFS_SNAP="restic-$(cat /proc/sys/kernel/random/uuid)"
zfs snap -r "${ZFS_DATASET}@${ZFS_SNAP}"
# clean up temporary snapshot on exit
_clean_snap() {
zfs destroy -r "${ZFS_DATASET}@${ZFS_SNAP}" && echo "zfs snapshot '${ZFS_DATASET}@${ZFS_SNAP}' destroyed"
}
trap _clean_snap EXIT
export ZFS_SNAP
fi
# mount snapshot and backup within private mount namespace
if [ -z "${EXEC_UNSHARED:-}" ]; then
export EXEC_UNSHARED=1
echo "re-executing '$0' in new private mount namespace.."
unshare --mount --propagation private "$0"
exit 0
fi
# use tmpfs for further mountpoints
root_mnt="${TMPDIR:-/var/tmp}/zsnapmounts"
mount -t tmpfs -o X-mount.mkdir tmpfs "${root_mnt}"
# mount snapshot of (sub) datasets
snap_mnts=()
for ds in $(zfs list -r -Ho name "${ZFS_DATASET}"); do
if [[ "$(zfs get -Ho value mountpoint "$ds")" == "none" ]]; then
continue
fi
mount -t zfs -o X-mount.mkdir "$ds@${ZFS_SNAP}" "${root_mnt}/$ds"
snap_mnts+=( "$ds" )
echo "mounted dataset '$ds@${ZFS_SNAP}'"
done
# backup temporary snapshot
echo -e "start restic backup\n"
pushd "${root_mnt}" > /dev/null
restic backup -v \
--exclude-caches \
"${snap_mnts[@]}"
echo -e "\nrestic backup done"
@awehrfritz
Copy link

Thanks for this script @stackcoder! I was wondering if line 49 is indeed correct, or if it should rather be

    snap_mnts+=( "${root_mnt}/$ds" )

instead of

    snap_mnts+=( "$ds" )

I might be missing something here though.

@stackcoder
Copy link
Author

@awehrfritz I wanted to strip away \var\tmp\zsnapmounts from the path recognized by restic. There's a pushd in line 59, so the paths passed to restic are relative.

@awehrfritz
Copy link

There's a pushd in line 59, so the paths passed to restic are relative.

Arrrg, I missed that one. Good point, it’s nice to strip away the tmp path!

Thanks for clarifying!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment