blob: b38c1295bbc08f2cc244f2a0b1d65e744dfddbfa [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
195 if ! [ -d "$external" ] ; then
196 echo ERROR: "Directory for mounting does not exist: ${external}" >&2
197 exit 1
198 fi
199
200 if echo "$internal" | grep -q '^/*$' ; then
201 echo ERROR: "Unacceptable internal path: ${internal}" >&2
202 exit 1
203 fi
204 done
205}
206
207do_external_mounts()
208{
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')
214
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
218 exit 1
219 fi
220
221 if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then
222 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
223 exit 1
224 fi
225
226 if ! mount --make-rprivate -o bind,${mount_type} "$external" "${CHROOT_DIR}/${internal}" ; then
227 echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2
228 exit 1
229 else
230 echo INFO: "Mount: ${external} -> ${internal}" >&2
231 fi
232 done
233}
234
235# arg: <mountpoint>
236umount_retry()
237{
238 mountpoint=$(echo "$1" | sed 's#//*#/#g')
239 timeout=${UMOUNT_TIMEOUT}
240
241 umount "$mountpoint" 2>/dev/null
242 while is_mounted "$mountpoint" && [ $timeout -gt 0 ] ; do
243 umount "$mountpoint" 2>/dev/null
244 sleep 1
245 timeout=$(( timeout - 1 ))
246 done
247
248 if ! is_mounted "$mountpoint" ; then
249 return 0
250 fi
251
252 return 1
253}
254
255undo_external_mounts()
256{
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
264 else
265 echo ERROR: "Failed to umount: ${CHROOT_DIR}/${internal}" >&2
266 fi
267 done
268}
269
270install_wrapper()
271{
272 cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <<EOF
273#!/bin/sh
274
275PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
276export PATH
277
278gid_tty=\$(getent group | sed -n '/^tty:/p' | cut -d: -f 3)
279
280mount -t proc proc /proc
281mount -t sysfs none /sys
282mount -t tmpfs none /dev
283
284mkdir -p /dev/shm
285mkdir -p /dev/pts
286mount -t devpts -o gid=\${gid_tty},mode=620 none /dev/pts
287
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
296
297chown root:tty /dev/console
298chown root:tty /dev/ptmx
299chown root:tty /dev/tty
300
301mkdir -p "\$1" || exit 1
302cd "\$1" || exit 1
303shift
304
305exec "\$@"
306
307EOF
308 chmod +x "$CHROOT_DIR"/usr/local/bin/fakeshell.sh
309}
310
311on_exit()
312{
313 set +e
314 echo
315
316 if [ -n "$OVERLAY_MOUNT" ] ; then
317 undo_external_mounts
318 fi
319 cleanup
320}
321
322
323#
324# parse arguments
325#
326
327state=nil
328action=nil
329EXTERNAL_MOUNTS=''
330CHROOT_WORKDIR=''
331CHROOT_METADIR=''
332CHROOT_DIR=''
333COMMAND=''
334while [ -n "$1" ] ; do
335 case "$state" in
336 nil)
337 case "$1" in
338 ''|-h|--help|help)
339 help
340 exit 0
341 ;;
342 --mount)
343 EXTERNAL_MOUNTS=$(printf "%s\n%s\n" "$EXTERNAL_MOUNTS" "${2}")
344 state=next
345 ;;
346 --workdir)
347 if [ -z "$CHROOT_WORKDIR" ] ; then
348 CHROOT_WORKDIR="$2"
349 state=next
350 else
351 echo ERROR: "Multiple working directory argument" >&2
352 help >&2
353 exit 1
354 fi
355 ;;
356 execute)
357 action=execute
358 state=execute
359 ;;
360 *)
361 echo ERROR: "Bad usage" >&2
362 help >&2
363 exit 1
364 ;;
365 esac
366 ;;
367 next)
368 state=nil
369 ;;
370 execute)
371 CHROOT_METADIR="$1"
372 shift
373 break
374 ;;
375 esac
376 shift
377done
378
379
380case "$action" in
381 ''|nil)
382 echo ERROR: "Nothing to do - missing command" >&2
383 help >&2
384 exit 1
385 ;;
386 execute)
387 # firstly do sanity checking ...
388
389 if [ -z "$CHROOT_METADIR" ] ; then
390 echo ERROR: "Missing argument" >&2
391 help >&2
392 exit 1
393 fi
394
395 # making sure that CHROOT_METADIR is absolute path
396 CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR")
397
398 if ! [ -d "$CHROOT_METADIR"/chroot ] ; then
399 echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2
400 exit 1
401 fi
402
403 # check external mounts if there are any
404 check_external_mounts
405
406 # check workdir
407 if [ -n "$CHROOT_WORKDIR" ] ; then
408 CHROOT_WORKDIR=$(echo "$CHROOT_WORKDIR" | sed -e 's#^/*##' -e 's#//*#/#g')
409 fi
410
411 # we must be root
412 if [ "$(id -u)" -ne 0 ] ; then
413 echo ERROR: "Need to be root and you are not: $(id -nu)" >&2
414 exit 1
415 fi
416
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
420 exit 1
421 fi
422
423 # ... sanity checking done
424
425 # setup paths
426 lowerdir="$CHROOT_METADIR"/chroot
427 upperdir="$CHROOT_METADIR"/.overlay
428 workdir="$CHROOT_METADIR"/.workdir
429 overlay="$CHROOT_METADIR"/.merged
430
431 # set trap
432 trap on_exit QUIT TERM EXIT
433
434 # mount overlay
435 OVERLAY_MOUNT=''
436 if do_overlay_mount ; then
437 # overlay chroot
438 OVERLAY_MOUNT=yes
439 else
440 # non overlay mount
441 OVERLAY_MOUNT=no
442 fi
443
444 # do the user-specific mounts
445 do_external_mounts
446
447 # I need this wrapper to do some setup inside the chroot...
448 install_wrapper
449
450 # execute chroot
451 # copy resolv.conf
452 cp -a /etc/resolv.conf "$CHROOT_DIR"/etc/resolv.conf
453
454 if [ -n "$1" ] ; then
455 :
456 else
457 set -- /bin/sh -l
458 fi
459 unshare -mfpi --propagation private \
460 chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@"
461 ;;
462esac
463
464exit 0
465