blob: 23afc7312c1500c271348d3423721faed467e4a8 [file] [log] [blame]
#!/bin/bash
set -e
set -o pipefail
me=$(basename $0)
# Main entry point, called at the end of the script
main()
{
# We either need to be root or have password-less sudo access to run as root. Make sure of it.
if [ $EUID -ne 0 ] ; then
if ! sudo -n true >/dev/null 2>&1 ; then
echo "$me: error: This script must be run as root or as a user with nopassword sudo authority" >&2
return 1
fi
echo "$me: Running as $USER, re-running with sudo"
exec sudo -E bash "$0" "$@"
return 0
else
echo "$me: Runnig as root"
fi
# Parse command line
COMMAND=()
for arg in "$@" ; do
case "$next" in
workspace) WORKSPACE="$arg" ; next= ; continue ;;
preserve) PRESERVE="$arg" ; next= ; continue ;;
reffile) REF_FILE="$arg" ; next= ; continue ;;
esac
case "$arg" in
-p) next=preserve ;;
-r) next=reffile ;;
-w) next=workspace ;;
*) COMMAND+=("$arg") ;;
esac
done
# Make sure we have any required variable
if [ -z "$COMMAND" ] ; then
echo "$me: error: No commands passed on the command line" >&2
usage
return 1
elif [ -z "$WORKSPACE" ] ; then
echo "$me: error: The WORKSPACE environment variable is not defined or specified with -w" >&2
usage
return 1
elif ! command -v rsync >/dev/null 2>&1 ; then
echo "$me: error: rsync is not installed" >&2
return 1
fi
if [ -n "$REF_FILE" ] ; then
REF_UID=$(stat -c %u "$REF_FILE")
REF_GID=$(stat -c %g "$REF_FILE")
fi
for v in WORKSPACE PRESERVE REF_FILE REF_UID REF_GID ; do
echo "$me: $v: ${!v}"
done
# Given WORKSPACE, create a new directory there to play in
for ((i=0; i < 100; ++i)) ; do
if out=$(mkdir "$WORKSPACE/chroot_$i" 2>&1) ; then
BASE="$WORKSPACE/chroot_$i"
add_exit_trap "rm -rf $BASE"
break
fi
done
if [ -z "$BASE" ] ; then
echo "$me: error: Unable to create chroot directory in $WORKSPACE: $out" >&2
return 1
fi
# Set up a directory to mount the overlayfs on
ROOT=$BASE/root
# We're going to run things from the current directory, but in the chroot
RUNDIR=$(pwd)
mkdir $ROOT
# Mount / (ro) with an overlay (rw) to our new root
mount_overlay / $ROOT $BASE
# Handle a few well-known filesystem mounts point that might be needed
for mp in /boot /var /usr ; do
if mountpoint -q $mp ; then
mount_overlay $mp $ROOT$mp $BASE
fi
done
# Walk up the path from / to RUNDIR to make sure none of those are mount point
local dirs=()
local d="$RUNDIR"
while [ "$d" != "/" ] ; do
dirs+=("$d")
d="$(dirname "$d")"
done
for ((i=${#dirs[@]}; i >= 0; --i)) ; do
d="${dirs[i]}"
if mountpoint -q "$d" ; then
mount_overlay "$d" $ROOT$d $BASE
fi
done
# Bind mount /dev and /proc into the new root since we can't overlay these types of filesystems
# It would be nice to then remount these read-only, but things like /dev/null need to be writable
mount --bind /proc $ROOT/proc
add_exit_trap "umount $ROOT/proc"
mount --bind /dev $ROOT/dev
add_exit_trap "umount $ROOT/dev"
# Run the command in the chroot
chroot "$ROOT" bash -c "cd $RUNDIR && eval ${COMMAND[*]}"
if [ -n "$PRESERVE" ] ; then
if [ "${PRESERVE:0:1}" != "/" ] ; then
PRESERVE="$(pwd)/$PRESERVE"
fi
cp -a $ROOT$PRESERVE $BASE/copy
if [ -n "$REF_UID" ] ; then
find $BASE/copy -uid 0 | xargs chown $REF_UID.$REF_GID
fi
rsync -a $BASE/copy/ "$PRESERVE/"
rm -rf $BASE/copy
fi
# And run the cleanup
run_exit_trap
}
usage()
{
cat <<EOF
usage: $me [-w WORKSPACE] [-p DIR] [-r REF-FILE] <command>
command - The name of an executable or script to run
-w WORKSPACE - The directory in which to create working directories for
the chroot environment. Not required if the WORKSPACE
environment variable is defined.
-p DIR - Preserve anything in the specified directory. Anything that
is changed in DIR will be merged back into the live directory
after the executable has completed.
-r REF-FILE - If -p is used to preserve a directory, because the executable
runs as root any new files ar owned by root. This will change
the ownership of any files owner by root to be owned by the
same user and group as REF-FILE.
This script will run another executable in a chroot environment. This chroot will look just like / except
any changes that are made will not persist. The executable will run as root. After the executable is run
a directory can be recursively merged back into the live directory, overriding the root ownership if desired.
EOF
}
# Mount an overlayfs
# $1 - The directory to mount
# $2 - The location where the overlay should be mounted
# $3 - Base directory where we can put the upperdir and workdir
mount_overlay()
{
local LOWER="$1"
local MOUNT="$2"
local BASE="$3"
local UPPER="$BASE/upper-${LOWER//\//_}"
local WORK="$BASE/work-${LOWER//\//_}"
mkdir -p "$UPPER" "$WORK" "$MOUNT"
mount -t overlay -o lowerdir=${LOWER},upperdir=${UPPER},workdir=${WORK} overlay $MOUNT
add_exit_trap "umount $MOUNT"
}
# Add a command to the existing EXIT trap
# $1 - The command to add
trapcmds=()
add_exit_trap()
{
trapcmds=("$1" "${trapcmds[@]}")
# On any exit, run all the commands
trap run_exit_trap EXIT
# On Ctrl-C, run all the commands (which disables the EXIT trap, too)
trap "run_exit_trap ; exit 2" SIGINT
}
run_exit_trap()
{
trap - EXIT
for cmd in "${trapcmds[@]}" ; do
eval "$cmd"
done
trapcmds=()
}
# Retry wrapper around umount
umount()
{
local out i
for ((i=0; i < 20; ++i)) ; do
if out=$(command umount "$@" 2>&1) ; then
#echo "umount $@"
return 0
fi
if echo "$out" | grep -q "target is busy" ; then
# If umount failed with "target is busy" then retry for a bit
sleep 0.5
continue
else
# Failed and not sure why
echo "$out" >&2
return 1
fi
done
echo "$me: error: Unable to unmount $@ after 10 attempts"
return 1
}
main "$@"