Add /etc/hosts to the chroot environment
[oom/offline-installer.git] / ansible / docker / run_chroot.sh
1 #!/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
22 set -e
23
24 CMD=$(basename "$0")
25 UMOUNT_TIMEOUT=120 # 2mins
26
27
28 #
29 # functions
30 #
31
32 help()
33 {
34     echo "
35 NAME:
36     ${CMD} - run command in chrooted directory
37
38 DESCRIPTION:
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
51 USAGE:
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
84 EXAMPLE:
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>
110 is_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
118 do_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
150 cleanup()
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
178 check_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
207 do_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>
236 umount_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
255 undo_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
270 install_wrapper()
271 {
272     cat > "$CHROOT_DIR"/usr/local/bin/fakeshell.sh <<EOF
273 #!/bin/sh
274
275 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
276 export PATH
277
278 gid_tty=\$(getent group | sed -n '/^tty:/p' | cut -d: -f 3)
279
280 mount -t proc proc /proc
281 mount -t sysfs none /sys
282 mount -t tmpfs none /dev
283
284 mkdir -p /dev/shm
285 mkdir -p /dev/pts
286 mount -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
297 chown root:tty /dev/console
298 chown root:tty /dev/ptmx
299 chown root:tty /dev/tty
300
301 mkdir -p "\$1" || exit 1
302 cd "\$1" || exit 1
303 shift
304
305 exec "\$@"
306
307 EOF
308     chmod +x "$CHROOT_DIR"/usr/local/bin/fakeshell.sh
309 }
310
311 on_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
327 state=nil
328 action=nil
329 EXTERNAL_MOUNTS=''
330 CHROOT_WORKDIR=''
331 CHROOT_METADIR=''
332 CHROOT_DIR=''
333 COMMAND=''
334 while [ -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
377 done
378
379
380 case "$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 and hosts file
452         cp -a /etc/resolv.conf "$CHROOT_DIR"/etc/resolv.conf
453         cp -a /etc/hosts "$CHROOT_DIR"/etc/hosts
454
455         if [ -n "$1" ] ; then
456             :
457         else
458             set -- /bin/sh -l
459         fi
460         unshare -mfpi --propagation private \
461             chroot "$CHROOT_DIR" /usr/local/bin/fakeshell.sh "${CHROOT_WORKDIR:-/}" "$@"
462         ;;
463 esac
464
465 exit 0
466