blob: 3359fdcd58b523e6a34e72e026391af8c302e855 [file] [log] [blame]
Petr Ospalý89583002018-12-19 13:14:54 +01001#!/bin/sh
2
3# COPYRIGHT NOTICE STARTS HERE
4
5# Copyright 2018 © Samsung Electronics Co., Ltd.
6#
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
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
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.
18
19# COPYRIGHT NOTICE ENDS HERE
20
21
22set -e
23
24CMD=$(basename "$0")
25UMOUNT_TIMEOUT=120 # 2mins
26
27
28#
29# functions
30#
31
32help()
33{
34 echo "
35NAME:
36 ${CMD} - run command in chrooted directory
37
38DESCRIPTION:
39 It will do necessary steps to be able chroot, optional mounts and it will
40 run commands inside the requested chroot directory.
41
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...
45
46 The chroot is run in it's own namespace for better containerization.
47 Therefore the utility 'unshare' is necessary requirement.
48
49 After exiting the chroot all of those necessary steps are undone.
50
51USAGE:
52 ${CMD} [-h|--help|help]
53 This help
54
55 ${CMD} [OPTIONS] execute <chroot-directory> [<command with args>...]
56
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
65
66 OPTIONS:
67
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
71 directory.
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.
75 For example:
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.
80
81 --workdir <inner-dir>
82 This will set working directory (PWD) inside the chroot.
83
84EXAMPLE:
85 ${CMD} --mount ro:/scripts/ansible:ansible \
86 --mount rw:/scripts/ansible/app:ansible/app \
87 --workdir /ansible execute /tmp/ansible_chroot
88 # pwd
89 /ansible
90 # mount
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)
97
98 Directory /ansible inside the chroot is not writable but subdirectory
99 /ansible/app is.
100
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.
104
105 Note: as you can see app directory is mounted over itself but read-write.
106"
107}
108
109# arg: <directory>
110is_mounted()
111{
112 mountpoint=$(echo "$1" | sed 's#//*#/#g')
113
114 LANG=C mount | grep -q "^[^[:space:]]\+[[:space:]]\+on[[:space:]]\+${mountpoint}[[:space:]]\+type[[:space:]]\+"
115}
116
117# layers are right to left! First is on the right, top/last is on the left
118do_overlay_mount()
119{
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
123 exit 1
124 fi
125
126 # prepare dirs
127 rm -rf "$overlay" "$upperdir" "$workdir"
128 mkdir -p "$overlay"
129 mkdir -p "$upperdir"
130 mkdir -p "$workdir"
131
132 # finally overlay mount
133 if ! mount -t overlay --make-rprivate \
134 -o lowerdir="$lowerdir",upperdir="$upperdir",workdir="$workdir" \
135 overlay "$overlay" ;
136 then
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!"
140
141 CHROOT_DIR="$lowerdir"
142 return 1
143 fi
144
145 CHROOT_DIR="$overlay"
146
147 return 0
148}
149
150cleanup()
151{
152 case "$OVERLAY_MOUNT" in
153 yes)
154 echo INFO: "Umounting overlay..." >&2
155 if ! umount_retry "$CHROOT_DIR" ; then
156 echo ERROR: "Cannot umount chroot: $CHROOT_DIR" >&2
157 return 1
158 fi
159
160 ;;
161 no)
162 echo INFO: "No overlay to umount" >&2
163 ;;
164 esac
165
166 if ! is_mounted "$overlay" ; then
167 echo INFO: "Deleting of temp directories..." >&2
168 rm -rf "$overlay" "$upperdir" "$workdir"
169 else
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
174 return 1
175 fi
176}
177
178check_external_mounts()
179{
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')
184
185 case "$mount_type" in
186 ro|rw)
187 :
188 ;;
189 *)
190 echo ERROR: "Wrong mount type (should be 'ro' or 'rw') in: ${mountexpr}" >&2
191 exit 1
192 ;;
193 esac
194
Petr Ospalýfb01a652019-01-07 13:28:57 +0100195 # sanity check that the mountpoint is not empty or the root directory itself
Petr Ospalý89583002018-12-19 13:14:54 +0100196 if echo "$internal" | grep -q '^/*$' ; then
197 echo ERROR: "Unacceptable internal path: ${internal}" >&2
198 exit 1
199 fi
200 done
201}
202
203do_external_mounts()
204{
205 echo INFO: "Bind mounting of external mounts..." >&2
206 echo "$EXTERNAL_MOUNTS" | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do
207 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
208 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
209 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g')
210
211 if is_mounted "${CHROOT_DIR}/${internal}" ; then
212 echo ERROR: "Mountpoint is already mounted: ${CHROOT_DIR}/${internal}" >&2
213 echo ERROR: "Fix the issue - cannot proceed" >&2
214 exit 1
215 fi
216
Petr Ospalýfb01a652019-01-07 13:28:57 +0100217 # trying to follow the behaviour of docker
218 if ! [ -e "$external" ] || [ -d "$external" ] ; then
219 # external is a dir
220 if ! mkdir -p "$external" ; then
221 echo ERROR: "Cannot create directory: ${external}" >&2
222 exit 1
223 fi
224 if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then
225 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
226 exit 1
227 fi
228 elif [ -f "$external" ] ; then
229 # if external is a file mount it as a file
230 if [ -e "${CHROOT_DIR}/${internal}" ] && ! [ -f "${CHROOT_DIR}/${internal}" ] ; then
231 echo ERROR: "Mounting a file but the mountpoint is not a file: ${CHROOT_DIR}/${internal}" >&2
232 exit 1
233 else
234 if ! touch "${CHROOT_DIR}/${internal}" ; then
235 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
236 exit 1
237 fi
238 fi
239 else
240 # anything but a simple file or a directory will fail
241 echo ERROR: "Unsupported mount: ${external} -> ${internal}" >&2
Petr Ospalý89583002018-12-19 13:14:54 +0100242 exit 1
243 fi
244
245 if ! mount --make-rprivate -o bind,${mount_type} "$external" "${CHROOT_DIR}/${internal}" ; then
246 echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2
247 exit 1
248 else
249 echo INFO: "Mount: ${external} -> ${internal}" >&2
250 fi
251 done
252}
253
254# arg: <mountpoint>
255umount_retry()
256{
257 mountpoint=$(echo "$1" | sed 's#//*#/#g')
258 timeout=${UMOUNT_TIMEOUT}
259
260 umount "$mountpoint" 2>/dev/null
261 while is_mounted "$mountpoint" && [ $timeout -gt 0 ] ; do
262 umount "$mountpoint" 2>/dev/null
263 sleep 1
264 timeout=$(( timeout - 1 ))
265 done
266
267 if ! is_mounted "$mountpoint" ; then
268 return 0
269 fi
270
271 return 1
272}
273
274undo_external_mounts()
275{
276 echo INFO: "Umount external mount points..." >&2
277 echo "$EXTERNAL_MOUNTS" | tac | sed '/^[[:space:]]*$/d' | while read -r mountexpr ; do
278 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
279 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
280 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}' | sed -e 's#^/*##' -e 's#//*#/#g')
281 if umount_retry "${CHROOT_DIR}/${internal}" ; then
282 echo INFO: "Unmounted: ${CHROOT_DIR}/${internal}" >&2
283 else
284 echo ERROR: "Failed to umount: ${CHROOT_DIR}/${internal}" >&2
285 fi
286 done
287}
288
289install_wrapper()
290{
291 cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <<EOF
292#!/bin/sh
293
294PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
295export PATH
296
297gid_tty=\$(getent group | sed -n '/^tty:/p' | cut -d: -f 3)
298
299mount -t proc proc /proc
300mount -t sysfs none /sys
301mount -t tmpfs none /dev
302
303mkdir -p /dev/shm
304mkdir -p /dev/pts
305mount -t devpts -o gid=\${gid_tty},mode=620 none /dev/pts
306
307[ -e /dev/full ] || mknod -m 666 /dev/full c 1 7
308[ -e /dev/ptmx ] || mknod -m 666 /dev/ptmx c 5 2
309[ -e /dev/random ] || mknod -m 644 /dev/random c 1 8
310[ -e /dev/urandom ] || mknod -m 644 /dev/urandom c 1 9
311[ -e /dev/zero ] || mknod -m 666 /dev/zero c 1 5
312[ -e /dev/tty ] || mknod -m 666 /dev/tty c 5 0
313[ -e /dev/console ] || mknod -m 622 /dev/console c 5 1
314[ -e /dev/null ] || mknod -m 666 /dev/null c 1 3
315
316chown root:tty /dev/console
317chown root:tty /dev/ptmx
318chown root:tty /dev/tty
319
320mkdir -p "\$1" || exit 1
321cd "\$1" || exit 1
322shift
323
324exec "\$@"
325
326EOF
327 chmod +x "$CHROOT_DIR"/usr/local/bin/fakeshell.sh
328}
329
330on_exit()
331{
332 set +e
333 echo
334
335 if [ -n "$OVERLAY_MOUNT" ] ; then
336 undo_external_mounts
337 fi
338 cleanup
339}
340
341
342#
343# parse arguments
344#
345
346state=nil
347action=nil
348EXTERNAL_MOUNTS=''
349CHROOT_WORKDIR=''
350CHROOT_METADIR=''
351CHROOT_DIR=''
352COMMAND=''
353while [ -n "$1" ] ; do
354 case "$state" in
355 nil)
356 case "$1" in
357 ''|-h|--help|help)
358 help
359 exit 0
360 ;;
361 --mount)
362 EXTERNAL_MOUNTS=$(printf "%s\n%s\n" "$EXTERNAL_MOUNTS" "${2}")
363 state=next
364 ;;
365 --workdir)
366 if [ -z "$CHROOT_WORKDIR" ] ; then
367 CHROOT_WORKDIR="$2"
368 state=next
369 else
370 echo ERROR: "Multiple working directory argument" >&2
371 help >&2
372 exit 1
373 fi
374 ;;
375 execute)
376 action=execute
377 state=execute
378 ;;
379 *)
380 echo ERROR: "Bad usage" >&2
381 help >&2
382 exit 1
383 ;;
384 esac
385 ;;
386 next)
387 state=nil
388 ;;
389 execute)
390 CHROOT_METADIR="$1"
391 shift
392 break
393 ;;
394 esac
395 shift
396done
397
398
399case "$action" in
400 ''|nil)
401 echo ERROR: "Nothing to do - missing command" >&2
402 help >&2
403 exit 1
404 ;;
405 execute)
406 # firstly do sanity checking ...
407
408 if [ -z "$CHROOT_METADIR" ] ; then
409 echo ERROR: "Missing argument" >&2
410 help >&2
411 exit 1
412 fi
413
414 # making sure that CHROOT_METADIR is absolute path
415 CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR")
416
417 if ! [ -d "$CHROOT_METADIR"/chroot ] ; then
418 echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2
419 exit 1
420 fi
421
422 # check external mounts if there are any
423 check_external_mounts
424
425 # check workdir
426 if [ -n "$CHROOT_WORKDIR" ] ; then
427 CHROOT_WORKDIR=$(echo "$CHROOT_WORKDIR" | sed -e 's#^/*##' -e 's#//*#/#g')
428 fi
429
430 # we must be root
431 if [ "$(id -u)" -ne 0 ] ; then
432 echo ERROR: "Need to be root and you are not: $(id -nu)" >&2
433 exit 1
434 fi
435
436 if ! which unshare >/dev/null 2>/dev/null ; then
437 echo ERROR: "'unshare' system command is missing - ABORT" >&2
438 echo INFO: "Try to install 'util-linux' package" >&2
439 exit 1
440 fi
441
442 # ... sanity checking done
443
444 # setup paths
445 lowerdir="$CHROOT_METADIR"/chroot
446 upperdir="$CHROOT_METADIR"/.overlay
447 workdir="$CHROOT_METADIR"/.workdir
448 overlay="$CHROOT_METADIR"/.merged
449
450 # set trap
451 trap on_exit QUIT TERM EXIT
452
453 # mount overlay
454 OVERLAY_MOUNT=''
455 if do_overlay_mount ; then
456 # overlay chroot
457 OVERLAY_MOUNT=yes
458 else
459 # non overlay mount
460 OVERLAY_MOUNT=no
461 fi
462
463 # do the user-specific mounts
464 do_external_mounts
465
466 # I need this wrapper to do some setup inside the chroot...
467 install_wrapper
468
469 # execute chroot
Petr Ospalý89583002018-12-19 13:14:54 +0100470 if [ -n "$1" ] ; then
471 :
472 else
473 set -- /bin/sh -l
474 fi
475 unshare -mfpi --propagation private \
476 chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@"
477 ;;
478esac
479
480exit 0
481