From 7f766756c6d328a4a8d57a274a215f67376af68d Mon Sep 17 00:00:00 2001 From: Ralph Ronnquist Date: Sun, 27 Mar 2022 18:11:43 +1100 Subject: [PATCH] refactoring for more flexible config --- functions | 166 +++++++++++++++++++------------------- overlay-boot | 77 +++--------------- overlay-boot.8.adoc | 189 ++++++++++++++++++++++++++++++++++++++++++++ overlay-init | 34 ++++++++ overlay-postmount | 13 +++ overlay-premount | 35 ++++++++ overlay-stop | 42 +++++----- 7 files changed, 390 insertions(+), 166 deletions(-) create mode 100644 overlay-boot.8.adoc create mode 100755 overlay-init create mode 100755 overlay-postmount create mode 100755 overlay-premount diff --git a/functions b/functions index 67ea3ad..2c819fa 100644 --- a/functions +++ b/functions @@ -1,18 +1,69 @@ -# 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 <&2 } # Install a default $1/etc/network/interfaces on the subhost root $1 @@ -57,67 +108,33 @@ setup_veth_cables() { 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 @@ -125,36 +142,25 @@ setup_overlay() { 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}' } diff --git a/overlay-boot b/overlay-boot index e94620e..d635da4 100755 --- a/overlay-boot +++ b/overlay-boot @@ -1,88 +1,35 @@ #!/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 <&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" diff --git a/overlay-boot.8.adoc b/overlay-boot.8.adoc new file mode 100644 index 0000000..9e39eec --- /dev/null +++ b/overlay-boot.8.adoc @@ -0,0 +1,189 @@ +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. + diff --git a/overlay-init b/overlay-init new file mode 100755 index 0000000..60e2634 --- /dev/null +++ b/overlay-init @@ -0,0 +1,34 @@ +#!/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 <&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 + + diff --git a/overlay-postmount b/overlay-postmount new file mode 100755 index 0000000..d1a3c0b --- /dev/null +++ b/overlay-postmount @@ -0,0 +1,13 @@ +#!/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 diff --git a/overlay-premount b/overlay-premount new file mode 100755 index 0000000..607c1ea --- /dev/null +++ b/overlay-premount @@ -0,0 +1,35 @@ +#!/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" diff --git a/overlay-stop b/overlay-stop index 2788413..132da76 100755 --- a/overlay-stop +++ b/overlay-stop @@ -3,22 +3,20 @@ # 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 <&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 <&2 @@ -28,16 +26,18 @@ EOF 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 -- 2.39.2