Skip to content

Instantly share code, notes, and snippets.

@jhujhiti
Last active June 19, 2021 11:10
Show Gist options
  • Save jhujhiti/ea15bdb11acd1165cd4d to your computer and use it in GitHub Desktop.
Save jhujhiti/ea15bdb11acd1165cd4d to your computer and use it in GitHub Desktop.
Run a command with a consistent view of (many, recursively-mounted) zfs filesystems
#!/bin/sh
# Copyright (c) 2016 Erick Turnquist <jhujhiti@adjectivism.org>
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# Usage:
# with-zfs-snapshots <mountpoint> <zpool>[ <zpool>...] -- <command>
# This script will run the supplied command on a recursive snapshot of the supplied zpools. Snapshots will be taken of each
# filesystem on <zpool>s and those snapshots will be nullfs-mounted rooted at <mountpoint>. The script will then cd to that
# directory (*not* chroot) and run the supplied command. Mounts and snapshots are cleaned up afterwards.
# N.B. Don't put spaces or pipes (|) in your filesystem names or paths. Bad things will probably happen.
# This was written and tested on FreeBSD 10.2. I use a 132-column wide terminal. Deal with it.
# 1) Snapshot the pools
# 2) Set holds on the snapshots. This both protects the snapshots while we work with them and makes it easier to identify the
# filesystems we want for the next step.
# 3) Find the filesystems with a mountpoint set.
# 4) Sort the identified filesystems by depth of the mount, shallow first.
# 5) nullfs-mount the filesystems in that order.
# 6) Move into the mountpoint directory.
# 7) Run the supplied command.
# 8) Unmount the filesystems in the reverse order.
# 9) Release the holds.
# 10) Destroy the snapshots.
tmpfile=$(mktemp)
savepwd=${PWD}
mountpoint=$1
shift
pools=
for it in $@
do
[ ${it} == '--' ] && break
pools="${pools} ${it}"
shift
done
if [ "$1" == '--' ]
then
shift
else
>&2 echo "Supply me a command to run. I will cd to the root of the mount before running it."
exit 1
fi
snapname=backup-$(date -u +%s)
if [ ! -d $mountpoint ]
then
>&2 echo "${mountpoint} doesn't exist."
exit 1
fi
poolsnaps=
for pool in $pools
do
poolsnaps="${poolsnaps} ${pool}@${snapname}"
done
cleanup() {
set +e
cd ${savepwd}
if [ $mounted ]
then
for path in $(cat <<EOF | tail -r)
do
${mountorder}
EOF
[ -d ${path}/.zfs ] && umount $(echo ${mountpoint}/${path} | tr -s /)
done
fi
[ $zfs_hold ] && zfs release -r ${snapname} ${poolsnaps}
if [ $zfs_snapshot ]
then
for snap in ${poolsnaps}
do
zfs destroy -r ${snap}
done
fi
[ -f $tmpfile ] && rm $tmpfile
}
set -e
trap cleanup INT TERM EXIT
zfs_snapshot=1
for snap in ${poolsnaps}
do
zfs snapshot -r ${snap}
done
zfs hold -r ${snapname} ${poolsnaps}
zfs_hold=1
# find snapshots that are of mounted filesystems only
allsnaps=$(zfs holds -Hr ${poolsnaps} | awk '{ print $1; }')
cat <<EOF | sed -e "s/@${snapname}\$//g" | xargs zfs list -Hpo name,mountpoint,jailed | awk '$2 != "none" { print $0; }' > ${tmpfile}
${allsnaps}
EOF
# order the mounts.
# we need to mount things shallow-first
mountorder=$(while read name path jailed
do
# if it's jailed, we need to figure out where it's mounted from the host's perspective...
if [ ${jailed} == 'on' ]
then
path="$(mount | awk '$1 == "'"${name}"'" { print $3; }')"
fi
if [ ${path} != '/' ]
then
echo "${name}|${path}|$(echo "${path}" | tr -dc / | wc -c)"
else
echo "${name}|${path}|0"
fi
done < ${tmpfile} | sort -n -t\| -k3 -k2 | awk -F\| '{ print $2; }')
rm ${tmpfile}
for path in ${mountorder}
do
mounted=1
path=$(echo ${path} | tr -s /)
if [ -d "${path}/.zfs" ]
then
mount -o ro -t nullfs $(echo "${path}/.zfs/snapshot/${snapname}" | tr -s /) ${mountpoint}/${path}
else
# silently do nothing
# this path is unmounted, presumably because the canmount property is set to no
fi
done
cd ${mountpoint}
"$@"
# vim: sw=4:sts=4:et:tw=132:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment