3 # COPYRIGHT NOTICE STARTS HERE
5 # Copyright 2018 © Samsung Electronics Co., Ltd.
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # COPYRIGHT NOTICE ENDS HERE
25 UMOUNT_TIMEOUT=120 # 2mins
36 ${CMD} - run command in chrooted directory
39 It will do necessary steps to be able chroot, optional mounts and it will
40 run commands inside the requested chroot directory.
42 It does overlay mount so nothing inside the chroot is modified - if there
43 is no way to do overlay mount it will just do chroot directly - which means
44 that user has power to render chroot useless - beware...
46 The chroot is run in it's own namespace for better containerization.
47 Therefore the utility 'unshare' is necessary requirement.
49 After exiting the chroot all of those necessary steps are undone.
52 ${CMD} [-h|--help|help]
55 ${CMD} [OPTIONS] execute <chroot-directory> [<command with args>...]
57 It will do some necessary steps after which it will execute chroot
58 command and gives you prompt inside the chroot. When you leave the
59 prompt it will undo those steps.
60 On top of the ordinary chroot it will make overlay, so every change
61 inside the chroot is only temporary and chroot is kept stateless -
62 like inside a docker container. If there is no way to do overlay -
63 ordinary chroot is done.
64 Default command is: /bin/sh -l
68 --mount (ro|rw):<src-dir>:<inner-dir>
69 This option will mount 'src-dir' which is full path on the host
70 system into the relative path 'inner-dir' within the chroot
72 It can be mounted as read-only (ro) or read-write (rw).
73 Multiple usage of this argument can be used to create complex
74 hierarchy. Order is significant.
76 --mount ro:/scripts/ANSIBLE_DIR:/ansible \
77 --mount rw:/scripts/ANSIBLE_DIR/app:/ansible/app
78 This will mount directory ansible as read-only into chroot,
79 but it's subdirectory 'app' will be writeable.
82 This will set working directory (PWD) inside the chroot.
85 ${CMD} --mount ro:/scripts/ansible:ansible \
86 --mount rw:/scripts/ansible/app:ansible/app \
87 --workdir /ansible execute /tmp/ansible_chroot
91 overlay on / type overlay ...
92 /dev/disk on /ansible type ext4 (ro,relatime,errors=remount-ro)
93 /dev/disk on /ansible/application type ext4 (rw,relatime,errors=remount-ro)
94 none on /proc type proc (rw,relatime)
95 none on /sys type sysfs (rw,relatime)
96 none on /dev/shm type tmpfs (rw,relatime)
98 Directory /ansible inside the chroot is not writable but subdirectory
101 Rest of the chroot is under overlay and all changes will be lost when
102 chroot command ends. Only changes in app directory persists bacause it
103 was bind mounted as read-write and is not part of overlay.
105 Note: as you can see app directory is mounted over itself but read-write.
112 mountpoint=$(echo "$1" | sed 's#//*#/#g')
114 LANG=C mount | grep -q "^[^[:space:]]\+[[:space:]]\+on[[:space:]]\+${mountpoint}[[:space:]]\+type[[:space:]]\+"
117 # layers are right to left! First is on the right, top/last is on the left
120 if [ -d "$overlay" ] && is_mounted "$overlay" ; then
121 echo ERROR: "The overlay directory is already mounted: $overlay" >&2
122 echo ERROR: "Fix the issue - cannot proceed" >&2
127 rm -rf "$overlay" "$upperdir" "$workdir"
132 # finally overlay mount
133 if ! mount -t overlay --make-rprivate \
134 -o lowerdir="$lowerdir",upperdir="$upperdir",workdir="$workdir" \
137 echo ERROR: "Failed to do overlay mount!" >&2
138 echo ERROR: "Please check that your system supports overlay!" >&2
139 echo NOTE: "Continuing with the ordinary chroot without overlay!"
141 CHROOT_DIR="$lowerdir"
145 CHROOT_DIR="$overlay"
152 case "$OVERLAY_MOUNT" in
154 echo INFO: "Umounting overlay..." >&2
155 if ! umount_retry "$CHROOT_DIR" ; then
156 echo ERROR: "Cannot umount chroot: $CHROOT_DIR" >&2
162 echo INFO: "No overlay to umount" >&2
166 if ! is_mounted "$overlay" ; then
167 echo INFO: "Deleting of temp directories..." >&2
168 rm -rf "$overlay" "$upperdir" "$workdir"
170 echo ERROR: "Overlay is still mounted: $CHROOT_DIR" >&2
171 echo ERROR: "Cannot delete: $overlay" >&2
172 echo ERROR: "Cannot delete: $upperdir" >&2
173 echo ERROR: "Cannot delete: $workdir" >&2
178 check_external_mounts()
180 echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do
181 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
182 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
183 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g')
185 case "$mount_type" in
190 echo ERROR: "Wrong mount type (should be 'ro' or 'rw') in: ${mountexpr}" >&2
195 if ! [ -d "$external" ] ; then
196 echo ERROR: "Directory for mounting does not exist: ${external}" >&2
200 if echo "$internal" | grep -q '^/*$' ; then
201 echo ERROR: "Unacceptable internal path: ${internal}" >&2
209 echo INFO: "Bind mounting of external mounts..." >&2
210 echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do
211 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
212 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
213 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g')
215 if is_mounted "${CHROOT_DIR}/${internal}" ; then
216 echo ERROR: "Mountpoint is already mounted: ${CHROOT_DIR}/${internal}" >&2
217 echo ERROR: "Fix the issue - cannot proceed" >&2
221 if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then
222 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
226 if ! mount --make-rprivate -o bind,${mount_type} "$external" "${CHROOT_DIR}/${internal}" ; then
227 echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2
230 echo INFO: "Mount: ${external} -> ${internal}" >&2
238 mountpoint=$(echo "$1" | sed 's#//*#/#g')
239 timeout=${UMOUNT_TIMEOUT}
241 umount "$mountpoint" 2>/dev/null
242 while is_mounted "$mountpoint" && [ $timeout -gt 0 ] ; do
243 umount "$mountpoint" 2>/dev/null
245 timeout=$(( timeout - 1 ))
248 if ! is_mounted "$mountpoint" ; then
255 undo_external_mounts()
257 echo INFO: "Umount external mount points..." >&2
258 echo "$EXTERNAL_MOUNTS" | tac | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do
259 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
260 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
261 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g')
262 if umount_retry "${CHROOT_DIR}/${internal}" ; then
263 echo INFO: "Unmounted: ${CHROOT_DIR}/${internal}" >&2
265 echo ERROR: "Failed to umount: ${CHROOT_DIR}/${internal}" >&2
272 cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <<EOF
275 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
278 gid_tty=\$(getent group | sed -n '/^tty:/p' | cut -d: -f 3)
280 mount -t proc proc /proc
281 mount -t sysfs none /sys
282 mount -t tmpfs none /dev
286 mount -t devpts -o gid=\${gid_tty},mode=620 none /dev/pts
288 [ -e /dev/full ] || mknod -m 666 /dev/full c 1 7
289 [ -e /dev/ptmx ] || mknod -m 666 /dev/ptmx c 5 2
290 [ -e /dev/random ] || mknod -m 644 /dev/random c 1 8
291 [ -e /dev/urandom ] || mknod -m 644 /dev/urandom c 1 9
292 [ -e /dev/zero ] || mknod -m 666 /dev/zero c 1 5
293 [ -e /dev/tty ] || mknod -m 666 /dev/tty c 5 0
294 [ -e /dev/console ] || mknod -m 622 /dev/console c 5 1
295 [ -e /dev/null ] || mknod -m 666 /dev/null c 1 3
297 chown root:tty /dev/console
298 chown root:tty /dev/ptmx
299 chown root:tty /dev/tty
301 mkdir -p "\$1" || exit 1
308 chmod +x "$CHROOT_DIR"/usr/local/bin/fakeshell.sh
316 if [ -n "$OVERLAY_MOUNT" ] ; then
334 while [ -n "$1" ] ; do
343 EXTERNAL_MOUNTS=$(printf "%s\n%s\n" "$EXTERNAL_MOUNTS" "${2}")
347 if [ -z "$CHROOT_WORKDIR" ] ; then
351 echo ERROR: "Multiple working directory argument" >&2
361 echo ERROR: "Bad usage" >&2
382 echo ERROR: "Nothing to do - missing command" >&2
387 # firstly do sanity checking ...
389 if [ -z "$CHROOT_METADIR" ] ; then
390 echo ERROR: "Missing argument" >&2
395 # making sure that CHROOT_METADIR is absolute path
396 CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR")
398 if ! [ -d "$CHROOT_METADIR"/chroot ] ; then
399 echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2
403 # check external mounts if there are any
404 check_external_mounts
407 if [ -n "$CHROOT_WORKDIR" ] ; then
408 CHROOT_WORKDIR=$(echo "$CHROOT_WORKDIR" | sed -e 's#^/*##' -e 's#//*#/#g')
412 if [ "$(id -u)" -ne 0 ] ; then
413 echo ERROR: "Need to be root and you are not: $(id -nu)" >&2
417 if ! which unshare >/dev/null 2>/dev/null ; then
418 echo ERROR: "'unshare' system command is missing - ABORT" >&2
419 echo INFO: "Try to install 'util-linux' package" >&2
423 # ... sanity checking done
426 lowerdir="$CHROOT_METADIR"/chroot
427 upperdir="$CHROOT_METADIR"/.overlay
428 workdir="$CHROOT_METADIR"/.workdir
429 overlay="$CHROOT_METADIR"/.merged
432 trap on_exit QUIT TERM EXIT
436 if do_overlay_mount ; then
444 # do the user-specific mounts
447 # I need this wrapper to do some setup inside the chroot...
451 # copy resolv.conf and hosts file
452 cp -a /etc/resolv.conf "$CHROOT_DIR"/etc/resolv.conf
453 cp -a /etc/hosts "$CHROOT_DIR"/etc/hosts
455 if [ -n "$1" ] ; then
460 unshare -mfpi --propagation private \
461 chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@"