-# This file implements common functions for all boot methods
+# This file implements common functions for all boot scripts
+# Rerun with sudo if needed
+[ $(id -u) = 0 ] || exec sudo $0 $@
+
+# Function to write a message and exit with error code
die() {
echo "$*" >&2
exit 1
}
+# Function to setup subhost name and log file
+subhost_name() {
+ CONFIG="$1"
+ [ -r "$CONFIG" ] || die "Cannot use $CONFIG"
+ config NAME "$(basename $CONFIG .conf)"
+ config LOG /tmp/oly-$NAME.log
+}
+
+# Function to set up all subhost configuration
+subhost_config() {
+
+ config BASE
+ BASE="$(cd $(dirname $CONFIG); realpath $BASE)"
+ [ -z "$BASE" ] && die "BASE is unset; bogus $CONFIG ?"
+ [ -d "$BASE" ] || die "$BASE is not a directory; bogus $CONFIG ?"
+ cd "$BASE" || die "$BASE is inaccessible"
+
+ config CABLES ""
+ config LIVE "$BASE/live"
+ config UPPER "$BASE/root"
+ config WORK "$BASE/work"
+ config LOWER "/"
+ config START "!networking ssh"
+ config PREMOUNT "$PROGRAMDIR/overlay-premount"
+ config POSTMOUNT "$PROGRAMDIR/overlay-postmount"
+ config INIT "$PROGRAMDIR/overlay-init"
+ config RAM_SIZE 50M
+}
+
+# function to reverse the $* words
+reverse() {
+ local OUT=""
+ for w in $* ; do OUT="$w $OUT" ; done
+ echo "${OUT% }"
+}
+
# grab and set a configuration variable
# $1 = variable, [ $2 = default .. error otherwise ]
config() {
- eval $1="'$(sed "/^$1=.*/{s|^$1=||;b};d" $CONFIG)'"
- [ -z "$(eval echo "\$$1")" ] || return 0
- [ $# -lt 2 ] && die "Missing $1=... in $CONFIG"
- eval $1="'$2'"
- eval echo "$1=\$$1"
+ local V W
+ read V <<EOF
+$(sed "/^$1=.*/{s|^$1=||;s|^\\s*||;s|\\s*\$||;b};d" $CONFIG)
+EOF
+ if [ -z "$V" ] ; then
+ [ $# -lt 2 ] && die "Missing $1=... in $CONFIG"
+ V="$2" # use the given default
+ elif [ -z "${V##!*}" ] ; then
+ read W <<EOF
+$(${V#!})
+EOF
+ [ -z "$W" ] && die "bad $1 config: $V"
+ V="$W"
+ fi
+ eval $1="'$V'"
+ eval echo "$1=$V" >&2
}
# Install a default $1/etc/network/interfaces on the subhost root $1
done
}
-# (name live system root work)
-# Set up an overlay fmr $name on $live, with a new tmpfs on its /run,
+# Set up an overlay for $name on $live, with a new tmpfs on its /run,
# and "install" a "reaper" as the upcoming pid 1
setup_overlay() {
- local LIVE="$2" LOWER="$3" UPPER="$4" ROOT
+ local NAME="$1" LIVE="$2" LOWER="$3" UPPER="$4" WORK="$5"
- if grep -q "$1 $2" /proc/mounts ; then
- die "$1 is already mounted"
- fi
+ echo setup_overlay "$NAME" "$LIVE" "$LOWER" "$UPPER" "$WORK"
- if [ -f "${UPPER%% *}" ] ; then
- if [ -x "${UPPER%% *}" ] ; then
- echo "${UPPER%% *} appears to be executable" >&2
- # Giving a program/script as UPPER= asks for running this
- # first, to make a root filesystem available. The script takes
- # ACTION "setup" and "teardown", and on "setup" it must tell
- # where the ROOT is set up.
- ROOT="$(env ACTION=setup $UPPER)"
- if [ ! -d "$ROOT" ] ; then
- # setup failed
- die "root setup failed: $UPPER"
- fi
- UPPER="$ROOT"
- ## Now falling down to "normal overlay" setup
- else
- die "${UPPER%% *} (root setup program/script) is not executable"
- fi
+ if grep -qE "^[^ ]+ $LIVE " /proc/mounts ; then
+ die "$LIVE already has a mount"
fi
- # LIVE is the same as LOWER then skip the overlay; just assume
- # a proper chroot system exists at LIVE.
- if [ "$LIVE" != "$LOWER" ] ; then
+ [ -d "$UPPER" ] || die "UPPER=$UPPER is not a directory"
+ [ -d "$LOWER" ] || die "LOWER=LOWPER is not a directory"
+ [ -d "$LIVE" ] || die "LOWER=LOWPER is not a directory"
+ [ -x "${PREMOUNT%% *}" ] || die "PREMOUNT=${PREMOUNT%% *} not executable"
+ [ -f "${PREMOUNT%% *}" ] || die "PREMOUNT='$PREMOUNT' is not a command"
+ [ -x "${POSTMOUNT%% *}" ] || \
+ die "POSTMOUNT=${POSTMOUNT%% *} not executable"
+ [ -f "${POSTMOUNT%% *}" ] || \
+ die "POSTMOUNT='$POSTMOUNT' is not a command"
+
+ # UPPER is the same as LOWER then skip the overlay mount
+ if [ "$UPPER" != "$LOWER" ] ; then
# sanity check
[ -d "$WORK" ] || die "WORK=$WORK is not a directory"
- [ -d "$UPPER" ] || die "UPPER=$UPPER is not a directory"
- [ -d "$LOWER" ] || die "LOWER=LOWPER is not a directory"
- [ -d "$LIVE" ] || die "LOWER=LOWPER is not a directory"
- # setup $UPPER/dev
- mkdir -p "$UPPER/dev"
- mount -t tmpfs -osize=50M tmpfs "$UPPER/dev"
- mknod -m 622 "$UPPER/dev/console" c 5 1
- mknod -m 666 "$UPPER/dev/null" c 1 3
- mknod -m 666 "$UPPER/dev/zero" c 1 5
- mknod -m 666 "$UPPER/dev/ptmx" c 5 2
- mknod -m 666 "$UPPER/dev/tty" c 5 0
- mknod -m 444 "$UPPER/dev/random" c 1 8
- mknod -m 444 "$UPPER/dev/urandom" c 1 9
- chown root:tty "$UPPER/dev/console"
- chown root:tty "$UPPER/dev/ptmx"
- chown root:tty "$UPPER/dev/tty"
- ln -sTf /proc/self/fd "$UPPER/dev/fd"
- ln -sTf /proc/self/fd/0 "$UPPER/dev/stdin"
- ln -sTf /proc/self/fd/1 "$UPPER/dev/stdout"
- ln -sTf /proc/self/fd/2 "$UPPER/dev/stderr"
- ln -sTf /proc/kcore "$UPPER/dev/core"
- mkdir "$UPPER/dev/shm"
- mkdir "$UPPER/dev/pts"
- chmod 1777 "$UPPER/dev/shm"
-
- # all good so far ; now avoid using the host's networking setup
- setup_networking "$UPPER"
+
+ env CONFIG="$CONFIG" $PREMOUNT "$UPPER"
OLY="-olowerdir=$3,upperdir=$UPPER,workdir=$5"
if ! mount -t overlay "$OLY" $1 $2 ; then
umount "$UPPER/run"
die "Cannot set up the overlay mount $2"
fi
+ elif [ "$LIVE" != "$UPPER" ] ; then
+ # With UPPER = LOWER we rather make a bind mount to LIVE
+ env CONFIG="$CONFIG" $PREMOUNT "$UPPER"
+ mount --bind $UPPER $LOWER
fi
- echo "Installing $OVERLAYDIR/reaper to $LIVE/.reaper"
- cp -p $OVERLAYDIR/reaper $LIVE/.reaper
-}
-
-start_services() {
- for S in "$@" ; do
- service $S start
- done
+ env CONFIG="$CONFIG" $POSTMOUNT "LIVE" "$UPPER"
}
-# find the upperdir option for an overlay mount line
-getupper() {
- sed 's/.*upperdir=\([^,]*\).*/\1/'
-}
-
-# Check if $1 is "live" and echo the
-# unshare and reaper process pids
+# Find the "unshare" process for $1 and echo the its pid and the pids
+# of its child processes.
is_live() {
local NAME=$1
local USPID="$(pgrep -f "unshare.* $NAME ")"
[ -z "$USPID" ] && return 1
- echo $USPID $(pgrep -f ".reaper $NAME")
+ echo "$USPID $(ps -hopid --ppid=$USPID)"
}
+# Find all overlay-boot processes and list their config files
list_running() {
- for C in $(pgrep -a overlay-boot | awk '{print $4}') ; do
- eval NAME="$(sed "/^NAME=.*/{s|^NAME=||;b};d" $C)"
- [ -z "$NAME" ] && NAME=$(basename $C .conf)
- echo $NAME
- done
+ pgrep -a overlay-boot | awk '{print $4}'
}
#!/bin/sh
#
-# This boot method runs a service subhost with a root filesystem that
-# is an overlay of the subhost's root and an OS root. The service
-# subhost is defined by a configuration file named on teh command line
+# This boot scripts runs a service subhost as defined by the
+# configuration file named on the command line.
+# See "man overlay-boot" for details.
-OVERLAYDIR="$(dirname $(realpath $0))"
+PROGRAMDIR="$(dirname $(realpath $0))"
+. $PROGRAMDIR/functions
-[ $(id -u) = 0 ] || exec sudo $0 $@
-. $OVERLAYDIR/functions $*
-
-CONFIG="$1"
-[ -r "$CONFIG" ] || die "Missing configuration $CONFIG"
-
-config NAME $(basename $1 .${1##*.})
-config LOG /tmp/oly-$NAME.log
+subhost_name $1
if [ -z "$UNSHARED" ] ; then
- # Pre-unsharing:
- #
- # Create the network namespace for the subhost, then trigger
- # detached re-run with unshared mount namespace
- [ -r /run/netns/$NAME ] || {
+ if [ ! -r /run/netns/$NAME ] ; then
ip netns add $NAME
- ip netns exec $NAME ip link set lo up
- }
+ ip netns exec $NAME ip link set lo up || exit 1
+ fi
exec env UNSHARED=yes unshare -m $0 $@ > $LOG 2>&1 &
echo "Logging to $LOG" >&2
exit 0
fi
-config BASE
-BASE="$(cd $(dirname $CONFIG); realpath $BASE)"
-
-[ -z "$BASE" ] && die "BASE is unset; bogus $CONFIG ?"
-[ -d "$BASE" ] || die "$BASE is not a directory; bogus $CONFIG ?"
-cd "$BASE" || die "$BASE is inaccessible"
-
-config LIVE "$BASE/live"
-config UPPER "$BASE/root"
-config WORK "$BASE/work"
-config LOWER "/"
-config CABLES ""
-config START "networking ssh"
-config SUBSHELL /bin/sh
-
-# Setup virtual cabling
+subhost_config
setup_veth_cables $NAME $CABLES
-
-# Set up the mount for this subhost, including a new tmpfs on its /run
-# and a default $UPPER/etc/network/interfaces if needed
-echo setup_overlay "$NAME" "$LIVE" "$LOWER" "$UPPER" "$WORK"
setup_overlay "$NAME" "$LIVE" "$LOWER" "$UPPER" "$WORK"
exithandler() {
ip netns del $NAME
- [ "$LOWER" != "$LIVE" ] && umount -R "$LIVE"
- [ -f "${UPPER%% *}" ] && [ -x "${UPPER%% *}" ] && \
- env ACTION=teardown $UPPER
+ [ "$UPPER" != "$LIVE" ] && umount -R "$LIVE"
}
trap "exithandler" 0
CMD="unshare -fp --mount-proc -i -u ip netns exec $NAME chroot $LIVE /bin/sh"
echo "$CMD"
-
-config RAM_SIZE 50M
-
-cat <<EOF | $CMD
-set -x
-mount -t proc proc /proc
-mount -t devpts devpts /dev/pts
-mount -t sysfs sysfs /sys
-if [ "$RAM_SIZE" != "none" ] && ! grep -q '/run tmpfs' /proc/mounts ; then
- mount -t tmpfs -osize=$RAM_SIZE,mode=755 tmpfs /run
-fi
-for srv in $START ; do service \$srv start ; done
-dummy_service() {
- [ \$# -gt 3 ] && return 0
- echo "Starting dummy service" >&2
- set +x
- [ -p /run/dummy_service ] || mkfifo /run/dummy_service
- ( printf dummy_service > /proc/self/comm ; read X < /run/dummy_service ) &
- set -x
-}
-dummy_service /proc/*/comm
-exec /.reaper $NAME
-EOF
+env CONFIG="$CONFIG" $INIT | $CMD
echo "EXITED $CMD"
--- /dev/null
+overlay-boot(8)
+===============
+:doctype: manpage
+:revdate: {sys:date "+%Y-%m-%d %H:%M:%S"}
+:COLON: :
+:EQUALS: =
+
+NAME
+----
+overlay-boot - Start a subhost with overlay root filesystem.
+
+SYNOPSIS
+--------
+*overlay-boot* _conf_
+
+DESCRIPTION
+-----------
+*overlay-boot* is the main script on a small collection of
+administration scripts for containerizing services with minimal ado.
+The script starts a "subhost" with a dedicated network namespace, and
+the mount and pid namespaces separated from the main host by means of
++unshare+. A subhost root file system may in particular be set up as
+an overlay of the main host filesystem to keep the specifics of a
+service distinctly separate from the main host while sharing files
+wherever sensible.
+
+A subhost is started by identifyinf its configuration file on the
+command line for *overlay-boot*. The configuration file is a plain
+text file with a small collection of "variables" that tell how the
+subhost is set up. When all is good, *overlay-boot* spawns a
+subprocess that invokes a command shell within an chroot into
+"unshared" subhost root filesystem, all similar to the bootup of any
+odd computer.
+
+The subhost execution environment may be "entered" to perform
+adminstrative tasks with *overlay-go*, and it is later stopped with
+*overlay-stop*.
+
+OPTIONS
+-------
+
+An overlay-boot subhost is defined in the configuration file, which is
+a plain text file with a number of variable assignments. Each
+assignment is written with the varable name flush left and immediately
+followed by an equal sign, The rest of that line (ignoring leading and
+trailing spaces) is its value, or if that value startes with an
+exclamation mark, then the line is a command to run so as to generate
+the value. See examples below.
+
+*NAME*::
+
+This variable declares a short name for the subhost, and should be no
+more than 12 printable ascii characters. The base name of the
+configuration file is used by default. I.e., a configuration file
+named +foo.conf+ by default names its subhost +foo+ unless there is a
++NAME+ variable says differently.
+
+*BASE*::
+
+This variable declares a pathname for a directory that is considered
+to be a "base" for the subhost setup. This is the only required
+variable.
+
+*CABLES*::
+
+This variable declares the subhost networking in terms of its virtual
+cables. The value is a space separated list of "virtual cable
+specifiers", each consisting of an equal sign optionally with a bridge
+name to the left and optinally a MAC address to the right. See the
+section on Networking below for more details.
+
+INIT::
+
+This variable is a command line to run, with envirnment variable
+CONFIG set, for producing the initial commands to the running subhost,
+similar to the initrd phase of a computer bootup. The default value
+for this variable is +overlay-init+.
+
+*LIVE*::
+
+This variable nominates the mount point for the running subhost's root
+file system. It defaults to +$BASE/live+ The nominated directory must
+exist, and depending on the directory pathnames in the +UPPER+ and
++LOWER+ variables, the subhost root filesystem is either of a
+pre-mounted directory, bind mounted or overlay mounted directory. See
+the details of this with the UPPER variable below.
+
+*LOG*::
+
+This variable nominates the logfile to use by +overlay-boot+ when
+running the subhost. The default is +/tmp/overlay-$NAME.log+.
+
+*LOWER*::
+
+This variable nominates the "lower" filesystem of an overlay mount.
+This will be accessed read-only, an it is intended to be the operating
+system root file system. The default is +/+, i.e. the main host root
+filesystem. When overlay is not desired, then LOWER should be the smae
+as UPPER.
+
+*POSTMOUNT*::
+
+This variable is a command line to run, with envirnment variable
+CONFIG set, just after the setup of the subhost root filesystem and
+before the services are started. The default for this variable is
++overlay-postmount+
+
+*PREMOUNT*::
+
+This variable is a command line to run, with envirnment variable
+CONFIG set, just before the setup of the subhost root filesystem and
+before the services are started. The default for this variable is
++overlay-premount+
+
+*UPPER*::
+
+This variable nominates the "upper" filesystem for an overlay mount.
+This will be accessed read-write and it constitutes the "private"
+files of the subhost.
+
+If UPPER is different from LOWER, then the root file system is set up
+as an overlay mount. In that case WORK (below) must be defined as a
+directory outside of but on the same mount as UPPER.
+
+If UPPER is the same as LOWER, then the subhost root filesystem is not
+setup as an overla. Rather it is set up as a bind mount, if UPPER is
+different from LIVE, or just as a pre-mounted LIVE filesystem.
+
+*WORK*::
+
+This variable nominates the "work" directory for an overlay mount. It
+has to be a writable directory on the same mount device as the UPPER
+directory.
+
+*START*::
+
+This variable tells which services should be started on the
+overlay-boot startup. The default value is +networking ssh+.
+
+Note that if no services are started, then +overlay-init+ starts a
++dummy_service+ so as to keep +/.reaper+ from running out of child
+processes, as it otherwise will exit and thereby terminate
++overlay-boot+.
+
+*RAM_SIZE*::
+
+This variable may be used to configure the +/run+ directory which by
+defult gets mounted on an overlay mount as a +tmpfs+ of 50M. Use
++none+ to avoid that mount, and otherwise the desired +tmpfs+ size.
+
+Networking
+----------
+
+*overlay-boot* sets up a nework namespace named by the subhost NAME,
+and uses the CABLES variable to set up +veth+ virtual cables. The host
+end of such cables are named by NAME followed by a number from 0 and
+up while the subhost end are named by +eth+ followed by the same
+number.
+
+As mentioned above, CABLES consists of a space separated list of cable
+specifiers, each consisting of a bridge interface name and a with an
+equal sign ("=") between them. The equal sign is required while either
+or both of the bridge and MAC address may be left empty.
+
+The bridge interface name, if given, will be given control of the host
+end cable interface. A cable specification with empty bridge name part
+results in that the host end is brought up at link level and then
++ifup $IF+ is attempted.
+
+The MAC address, if given, is used for the subhost end cable
+interface. A cable specification with empty MAC address part results
+in that the interface get its MAC address from the kernel, possibly a
+different one upon each start.
+
+EXAMPLES
+--------
+
+./opt/subhost/mta/mta.conf
+****
+BASE=.
+START= rsyslog newtorking ssh postfix
+****
+
+The above example assumes a directory +/opt/subhost/mta+ that contains
+the configuration file +mta.conf+ and directories +root+, +work+ and
++live+. *overlay-boot* will set up an overlay mount on +live+ with
++root+ as UPPER, +work+ as WORK and +/+ as LOWER, i.e. an overlay of
+the main host filesystem.
+
--- /dev/null
+#!/bin/sh
+#
+# This script performs default actions. It is invoked with CONFIG set
+# for the subhost.
+
+OVERLAYDIR="$(dirname $(realpath $0))"
+. $OVERLAYDIR/functions
+
+subhost_name "$CONFIG"
+subhost_config
+
+# Print the default init script
+cat <<EOF
+set -x
+mount -t proc proc /proc
+mount -t devpts devpts /dev/pts
+mount -t sysfs sysfs /sys
+if [ "$RAM_SIZE" != "none" ] && ! grep -q '/run tmpfs' /proc/mounts ; then
+ mount -t tmpfs -osize=$RAM_SIZE,mode=755 tmpfs /run
+fi
+for srv in $START ; do service \$srv start ; done
+dummy_service() {
+ [ \$# -gt 3 ] && return 0
+ echo "Starting dummy service" >&2
+ set +x
+ [ -p /run/dummy_service ] || mkfifo /run/dummy_service
+ ( printf dummy_service > /proc/self/comm ; read X < /run/dummy_service ) &
+ set -x
+}
+dummy_service /proc/*/comm
+exec /.reaper $NAME
+EOF
+
+
--- /dev/null
+#!/bin/sh
+#
+# This script performs default actions. It is invoked with CONFIG set
+# for the subhost.
+
+OVERLAYDIR="$(dirname $(realpath $0))"
+. $OVERLAYDIR/functions
+
+subhost_name "$CONFIG"
+subhost_config
+
+echo "Installing $OVERLAYDIR/reaper to $LIVE/.reaper"
+cp -p $OVERLAYDIR/reaper $LIVE/.reaper
--- /dev/null
+#!/bin/sh
+#
+# This script performs default actions. It is invoked with CONFIG set
+# for the subhost.
+
+OVERLAYDIR="$(dirname $(realpath $0))"
+. $OVERLAYDIR/functions
+
+subhost_name "$CONFIG"
+subhost_config
+
+# setup $UPPER/dev
+mkdir -p "$UPPER/dev"
+mount -t tmpfs -osize=50M tmpfs "$UPPER/dev"
+mknod -m 622 "$UPPER/dev/console" c 5 1
+mknod -m 666 "$UPPER/dev/null" c 1 3
+mknod -m 666 "$UPPER/dev/zero" c 1 5
+mknod -m 666 "$UPPER/dev/ptmx" c 5 2
+mknod -m 666 "$UPPER/dev/tty" c 5 0
+mknod -m 444 "$UPPER/dev/random" c 1 8
+mknod -m 444 "$UPPER/dev/urandom" c 1 9
+chown root:tty "$UPPER/dev/console"
+chown root:tty "$UPPER/dev/ptmx"
+chown root:tty "$UPPER/dev/tty"
+ln -sTf /proc/self/fd "$UPPER/dev/fd"
+ln -sTf /proc/self/fd/0 "$UPPER/dev/stdin"
+ln -sTf /proc/self/fd/1 "$UPPER/dev/stdout"
+ln -sTf /proc/self/fd/2 "$UPPER/dev/stderr"
+ln -sTf /proc/kcore "$UPPER/dev/core"
+mkdir "$UPPER/dev/shm"
+mkdir "$UPPER/dev/pts"
+chmod 1777 "$UPPER/dev/shm"
+
+# all good so far ; now avoid using the host's networking setup
+setup_networking "$UPPER"
# Script to stop the nominated overlay subhost
OVERLAYDIR="$(dirname $(realpath $0))"
-
-[ $(id -u) = 0 ] || exec sudo $0 $@
. $OVERLAYDIR/functions $*
-CONFIG="$1"
-[ -r "$CONFIG" ] || die "Missing configuration $CONFIG"
-
-config NAME $(basename $1 .${1##*.})
-config START "ssh networking"
-config LIVE
+subhost_name "$1"
+subhost_config
read USPID RSPID <<EOF
$(is_live $NAME)
EOF
-[ -z "$USPID" ] && echo "$NAME is snot running" >&2 && exit 1
+if [ -z "$USPID" ] ; then
+ [ -r /run/netns/$NAME ] && ip netns del $NAME
+ echo "$NAME is not running" >&2
+ exit 1
+fi
if [ -z "$RSPID" ] ; then
cat <<EOF >&2
exit 1
fi
-# function to reverse the $* words
-reverse() {
- local OUT=""
- for w in $* ; do OUT="$w $OUT" ; done
- echo "${OUT% }"
-}
-
START="$(reverse "$START")"
-if nsenter -t $RSPID -p -m -i -u ip netns exec $NAME chroot $LIVE /bin/sh \
- -c "for srv in $START ; do service \$srv stop ; done" ; then
- CHILDPIDS="$(nsenter -t $RSPID -p -m ps -hopid --ppid 1)"
- nsenter -t $RSPID -p -m kill $CHILDPIDS
-fi
+nsenter -t $RSPID -p -m -i -u \
+ ip netns exec $NAME chroot $(realpath $LIVE) \
+ /bin/sh -c "for srv in $START ; do service \$srv stop ; done"
+
+for p in $RSPID $USPID ; do
+ for S in 15 1 2 9 ; do
+ ps -hocmd $p || break
+ kill -$S $p
+ done
+done
+
+[ -r /run/netns/$NAME ] && ip netns del $NAME
+
+true