From 8958300392b42325dbf928996795a0cd689fc0e7 Mon Sep 17 00:00:00 2001 From: Petr Ospalý Date: Wed, 19 Dec 2018 13:14:54 +0100 Subject: Add a wrapper script to run ansible in chroot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Script uses namespaces and overlayfs to be more robust. Also it supports bind mounting to extend the chroot with external files and directories. Issue-ID: OOM-1551 Change-Id: I8c554e1b86fca86f86e6c0d0e1b106c97a08ef15 Signed-off-by: Petr Ospalý --- ansible/docker/run_chroot.sh | 465 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100755 ansible/docker/run_chroot.sh (limited to 'ansible/docker') diff --git a/ansible/docker/run_chroot.sh b/ansible/docker/run_chroot.sh new file mode 100755 index 00000000..b38c1295 --- /dev/null +++ b/ansible/docker/run_chroot.sh @@ -0,0 +1,465 @@ +#!/bin/sh + +# COPYRIGHT NOTICE STARTS HERE + +# Copyright 2018 © Samsung Electronics Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# COPYRIGHT NOTICE ENDS HERE + + +set -e + +CMD=$(basename "$0") +UMOUNT_TIMEOUT=120 # 2mins + + +# +# functions +# + +help() +{ + echo " +NAME: + ${CMD} - run command in chrooted directory + +DESCRIPTION: + It will do necessary steps to be able chroot, optional mounts and it will + run commands inside the requested chroot directory. + + It does overlay mount so nothing inside the chroot is modified - if there + is no way to do overlay mount it will just do chroot directly - which means + that user has power to render chroot useless - beware... + + The chroot is run in it's own namespace for better containerization. + Therefore the utility 'unshare' is necessary requirement. + + After exiting the chroot all of those necessary steps are undone. + +USAGE: + ${CMD} [-h|--help|help] + This help + + ${CMD} [OPTIONS] execute [...] + + It will do some necessary steps after which it will execute chroot + command and gives you prompt inside the chroot. When you leave the + prompt it will undo those steps. + On top of the ordinary chroot it will make overlay, so every change + inside the chroot is only temporary and chroot is kept stateless - + like inside a docker container. If there is no way to do overlay - + ordinary chroot is done. + Default command is: /bin/sh -l + + OPTIONS: + + --mount (ro|rw):: + This option will mount 'src-dir' which is full path on the host + system into the relative path 'inner-dir' within the chroot + directory. + It can be mounted as read-only (ro) or read-write (rw). + Multiple usage of this argument can be used to create complex + hierarchy. Order is significant. + For example: + --mount ro:/scripts/ANSIBLE_DIR:/ansible \ + --mount rw:/scripts/ANSIBLE_DIR/app:/ansible/app + This will mount directory ansible as read-only into chroot, + but it's subdirectory 'app' will be writeable. + + --workdir + This will set working directory (PWD) inside the chroot. + +EXAMPLE: + ${CMD} --mount ro:/scripts/ansible:ansible \ + --mount rw:/scripts/ansible/app:ansible/app \ + --workdir /ansible execute /tmp/ansible_chroot + # pwd + /ansible + # mount + overlay on / type overlay ... + /dev/disk on /ansible type ext4 (ro,relatime,errors=remount-ro) + /dev/disk on /ansible/application type ext4 (rw,relatime,errors=remount-ro) + none on /proc type proc (rw,relatime) + none on /sys type sysfs (rw,relatime) + none on /dev/shm type tmpfs (rw,relatime) + + Directory /ansible inside the chroot is not writable but subdirectory + /ansible/app is. + + Rest of the chroot is under overlay and all changes will be lost when + chroot command ends. Only changes in app directory persists bacause it + was bind mounted as read-write and is not part of overlay. + + Note: as you can see app directory is mounted over itself but read-write. +" +} + +# arg: +is_mounted() +{ + mountpoint=$(echo "$1" | sed 's#//*#/#g') + + LANG=C mount | grep -q "^[^[:space:]]\+[[:space:]]\+on[[:space:]]\+${mountpoint}[[:space:]]\+type[[:space:]]\+" +} + +# layers are right to left! First is on the right, top/last is on the left +do_overlay_mount() +{ + if [ -d "$overlay" ] && is_mounted "$overlay" ; then + echo ERROR: "The overlay directory is already mounted: $overlay" >&2 + echo ERROR: "Fix the issue - cannot proceed" >&2 + exit 1 + fi + + # prepare dirs + rm -rf "$overlay" "$upperdir" "$workdir" + mkdir -p "$overlay" + mkdir -p "$upperdir" + mkdir -p "$workdir" + + # finally overlay mount + if ! mount -t overlay --make-rprivate \ + -o lowerdir="$lowerdir",upperdir="$upperdir",workdir="$workdir" \ + overlay "$overlay" ; + then + echo ERROR: "Failed to do overlay mount!" >&2 + echo ERROR: "Please check that your system supports overlay!" >&2 + echo NOTE: "Continuing with the ordinary chroot without overlay!" + + CHROOT_DIR="$lowerdir" + return 1 + fi + + CHROOT_DIR="$overlay" + + return 0 +} + +cleanup() +{ + case "$OVERLAY_MOUNT" in + yes) + echo INFO: "Umounting overlay..." >&2 + if ! umount_retry "$CHROOT_DIR" ; then + echo ERROR: "Cannot umount chroot: $CHROOT_DIR" >&2 + return 1 + fi + + ;; + no) + echo INFO: "No overlay to umount" >&2 + ;; + esac + + if ! is_mounted "$overlay" ; then + echo INFO: "Deleting of temp directories..." >&2 + rm -rf "$overlay" "$upperdir" "$workdir" + else + echo ERROR: "Overlay is still mounted: $CHROOT_DIR" >&2 + echo ERROR: "Cannot delete: $overlay" >&2 + echo ERROR: "Cannot delete: $upperdir" >&2 + echo ERROR: "Cannot delete: $workdir" >&2 + return 1 + fi +} + +check_external_mounts() +{ + echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + + case "$mount_type" in + ro|rw) + : + ;; + *) + echo ERROR: "Wrong mount type (should be 'ro' or 'rw') in: ${mountexpr}" >&2 + exit 1 + ;; + esac + + if ! [ -d "$external" ] ; then + echo ERROR: "Directory for mounting does not exist: ${external}" >&2 + exit 1 + fi + + if echo "$internal" | grep -q '^/*$' ; then + echo ERROR: "Unacceptable internal path: ${internal}" >&2 + exit 1 + fi + done +} + +do_external_mounts() +{ + echo INFO: "Bind mounting of external mounts..." >&2 + echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + + if is_mounted "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Mountpoint is already mounted: ${CHROOT_DIR}/${internal}" >&2 + echo ERROR: "Fix the issue - cannot proceed" >&2 + exit 1 + fi + + if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2 + exit 1 + fi + + if ! mount --make-rprivate -o bind,${mount_type} "$external" "${CHROOT_DIR}/${internal}" ; then + echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2 + exit 1 + else + echo INFO: "Mount: ${external} -> ${internal}" >&2 + fi + done +} + +# arg: +umount_retry() +{ + mountpoint=$(echo "$1" | sed 's#//*#/#g') + timeout=${UMOUNT_TIMEOUT} + + umount "$mountpoint" 2>/dev/null + while is_mounted "$mountpoint" && [ $timeout -gt 0 ] ; do + umount "$mountpoint" 2>/dev/null + sleep 1 + timeout=$(( timeout - 1 )) + done + + if ! is_mounted "$mountpoint" ; then + return 0 + fi + + return 1 +} + +undo_external_mounts() +{ + echo INFO: "Umount external mount points..." >&2 + echo "$EXTERNAL_MOUNTS" | tac | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do + mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}') + external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}') + internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g') + if umount_retry "${CHROOT_DIR}/${internal}" ; then + echo INFO: "Unmounted: ${CHROOT_DIR}/${internal}" >&2 + else + echo ERROR: "Failed to umount: ${CHROOT_DIR}/${internal}" >&2 + fi + done +} + +install_wrapper() +{ + cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <&2 + help >&2 + exit 1 + fi + ;; + execute) + action=execute + state=execute + ;; + *) + echo ERROR: "Bad usage" >&2 + help >&2 + exit 1 + ;; + esac + ;; + next) + state=nil + ;; + execute) + CHROOT_METADIR="$1" + shift + break + ;; + esac + shift +done + + +case "$action" in + ''|nil) + echo ERROR: "Nothing to do - missing command" >&2 + help >&2 + exit 1 + ;; + execute) + # firstly do sanity checking ... + + if [ -z "$CHROOT_METADIR" ] ; then + echo ERROR: "Missing argument" >&2 + help >&2 + exit 1 + fi + + # making sure that CHROOT_METADIR is absolute path + CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR") + + if ! [ -d "$CHROOT_METADIR"/chroot ] ; then + echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2 + exit 1 + fi + + # check external mounts if there are any + check_external_mounts + + # check workdir + if [ -n "$CHROOT_WORKDIR" ] ; then + CHROOT_WORKDIR=$(echo "$CHROOT_WORKDIR" | sed -e 's#^/*##' -e 's#//*#/#g') + fi + + # we must be root + if [ "$(id -u)" -ne 0 ] ; then + echo ERROR: "Need to be root and you are not: $(id -nu)" >&2 + exit 1 + fi + + if ! which unshare >/dev/null 2>/dev/null ; then + echo ERROR: "'unshare' system command is missing - ABORT" >&2 + echo INFO: "Try to install 'util-linux' package" >&2 + exit 1 + fi + + # ... sanity checking done + + # setup paths + lowerdir="$CHROOT_METADIR"/chroot + upperdir="$CHROOT_METADIR"/.overlay + workdir="$CHROOT_METADIR"/.workdir + overlay="$CHROOT_METADIR"/.merged + + # set trap + trap on_exit QUIT TERM EXIT + + # mount overlay + OVERLAY_MOUNT='' + if do_overlay_mount ; then + # overlay chroot + OVERLAY_MOUNT=yes + else + # non overlay mount + OVERLAY_MOUNT=no + fi + + # do the user-specific mounts + do_external_mounts + + # I need this wrapper to do some setup inside the chroot... + install_wrapper + + # execute chroot + # copy resolv.conf + cp -a /etc/resolv.conf "$CHROOT_DIR"/etc/resolv.conf + + if [ -n "$1" ] ; then + : + else + set -- /bin/sh -l + fi + unshare -mfpi --propagation private \ + chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@" + ;; +esac + +exit 0 + -- cgit 1.2.3-korg