blob: 8ae9c1884fb01163cebfc26c2f875d724f705ab4 [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")
Petr Ospalý89583002018-12-19 13:14:54 +010025
26
27#
28# functions
29#
30
31help()
32{
33 echo "
34NAME:
35 ${CMD} - run command in chrooted directory
36
37DESCRIPTION:
38 It will do necessary steps to be able chroot, optional mounts and it will
39 run commands inside the requested chroot directory.
40
41 It does overlay mount so nothing inside the chroot is modified - if there
42 is no way to do overlay mount it will just do chroot directly - which means
43 that user has power to render chroot useless - beware...
44
45 The chroot is run in it's own namespace for better containerization.
46 Therefore the utility 'unshare' is necessary requirement.
47
48 After exiting the chroot all of those necessary steps are undone.
49
50USAGE:
51 ${CMD} [-h|--help|help]
52 This help
53
54 ${CMD} [OPTIONS] execute <chroot-directory> [<command with args>...]
55
56 It will do some necessary steps after which it will execute chroot
57 command and gives you prompt inside the chroot. When you leave the
58 prompt it will undo those steps.
59 On top of the ordinary chroot it will make overlay, so every change
60 inside the chroot is only temporary and chroot is kept stateless -
61 like inside a docker container. If there is no way to do overlay -
62 ordinary chroot is done.
63 Default command is: /bin/sh -l
64
65 OPTIONS:
66
67 --mount (ro|rw):<src-dir>:<inner-dir>
68 This option will mount 'src-dir' which is full path on the host
69 system into the relative path 'inner-dir' within the chroot
70 directory.
71 It can be mounted as read-only (ro) or read-write (rw).
72 Multiple usage of this argument can be used to create complex
73 hierarchy. Order is significant.
74 For example:
75 --mount ro:/scripts/ANSIBLE_DIR:/ansible \
76 --mount rw:/scripts/ANSIBLE_DIR/app:/ansible/app
77 This will mount directory ansible as read-only into chroot,
78 but it's subdirectory 'app' will be writeable.
79
80 --workdir <inner-dir>
81 This will set working directory (PWD) inside the chroot.
82
83EXAMPLE:
84 ${CMD} --mount ro:/scripts/ansible:ansible \
85 --mount rw:/scripts/ansible/app:ansible/app \
86 --workdir /ansible execute /tmp/ansible_chroot
87 # pwd
88 /ansible
89 # mount
90 overlay on / type overlay ...
91 /dev/disk on /ansible type ext4 (ro,relatime,errors=remount-ro)
92 /dev/disk on /ansible/application type ext4 (rw,relatime,errors=remount-ro)
93 none on /proc type proc (rw,relatime)
94 none on /sys type sysfs (rw,relatime)
95 none on /dev/shm type tmpfs (rw,relatime)
96
97 Directory /ansible inside the chroot is not writable but subdirectory
98 /ansible/app is.
99
100 Rest of the chroot is under overlay and all changes will be lost when
101 chroot command ends. Only changes in app directory persists bacause it
102 was bind mounted as read-write and is not part of overlay.
103
104 Note: as you can see app directory is mounted over itself but read-write.
105"
106}
107
Petr Ospalý89583002018-12-19 13:14:54 +0100108# layers are right to left! First is on the right, top/last is on the left
109do_overlay_mount()
110{
Petr Ospalý89583002018-12-19 13:14:54 +0100111 # prepare dirs
Michal Zeganbacf0a12019-03-22 14:16:04 +0100112mkdir -p $ovtempdir
113mount -t tmpfs -o mode=0755 tmpfs $ovtempdir
Petr Ospalý89583002018-12-19 13:14:54 +0100114 mkdir -p "$overlay"
115 mkdir -p "$upperdir"
116 mkdir -p "$workdir"
117
118 # finally overlay mount
Michal Zeganbacf0a12019-03-22 14:16:04 +0100119 if ! mount -t overlay \
Petr Ospalý89583002018-12-19 13:14:54 +0100120 -o lowerdir="$lowerdir",upperdir="$upperdir",workdir="$workdir" \
121 overlay "$overlay" ;
122 then
123 echo ERROR: "Failed to do overlay mount!" >&2
124 echo ERROR: "Please check that your system supports overlay!" >&2
125 echo NOTE: "Continuing with the ordinary chroot without overlay!"
126
127 CHROOT_DIR="$lowerdir"
128 return 1
129 fi
130
131 CHROOT_DIR="$overlay"
132
133 return 0
134}
135
Petr Ospalý89583002018-12-19 13:14:54 +0100136check_external_mounts()
137{
Michal Zeganbacf0a12019-03-22 14:16:04 +0100138 echo "$EXTERNAL_MOUNTS" | while read -r mountexpr ; do
139 #Skip empty lines, done with if for readability.
140 if [ -z $mountexpr ]; then
141 continue
142 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100143 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
144 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
Michal Zeganbacf0a12019-03-22 14:16:04 +0100145 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}')
Petr Ospalý89583002018-12-19 13:14:54 +0100146
147 case "$mount_type" in
148 ro|rw)
149 :
150 ;;
151 *)
152 echo ERROR: "Wrong mount type (should be 'ro' or 'rw') in: ${mountexpr}" >&2
153 exit 1
154 ;;
155 esac
156
Petr Ospalýfb01a652019-01-07 13:28:57 +0100157 # sanity check that the mountpoint is not empty or the root directory itself
Petr Ospalý89583002018-12-19 13:14:54 +0100158 if echo "$internal" | grep -q '^/*$' ; then
159 echo ERROR: "Unacceptable internal path: ${internal}" >&2
160 exit 1
161 fi
162 done
163}
164
165do_external_mounts()
166{
167 echo INFO: "Bind mounting of external mounts..." >&2
Michal Zeganbacf0a12019-03-22 14:16:04 +0100168 echo "$EXTERNAL_MOUNTS" | while read -r mountexpr ; do
169 if [ -z $mountexpr ]; then
170 continue
171 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100172 mount_type=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $1;}')
173 external=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $2;}')
Michal Zeganbacf0a12019-03-22 14:16:04 +0100174 internal=$(echo "$mountexpr" | awk 'BEGIN{FS=":"}{print $3;}')
Petr Ospalý89583002018-12-19 13:14:54 +0100175
Petr Ospalýfb01a652019-01-07 13:28:57 +0100176 # trying to follow the behaviour of docker
177 if ! [ -e "$external" ] || [ -d "$external" ] ; then
178 # external is a dir
179 if ! mkdir -p "$external" ; then
180 echo ERROR: "Cannot create directory: ${external}" >&2
181 exit 1
182 fi
183 if ! mkdir -p "${CHROOT_DIR}/${internal}" ; then
184 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
185 exit 1
186 fi
187 elif [ -f "$external" ] ; then
188 # if external is a file mount it as a file
189 if [ -e "${CHROOT_DIR}/${internal}" ] && ! [ -f "${CHROOT_DIR}/${internal}" ] ; then
190 echo ERROR: "Mounting a file but the mountpoint is not a file: ${CHROOT_DIR}/${internal}" >&2
191 exit 1
192 else
193 if ! touch "${CHROOT_DIR}/${internal}" ; then
194 echo ERROR: "Cannot create mountpoint: ${CHROOT_DIR}/${internal}" >&2
195 exit 1
196 fi
197 fi
198 else
199 # anything but a simple file or a directory will fail
200 echo ERROR: "Unsupported mount: ${external} -> ${internal}" >&2
Petr Ospalý89583002018-12-19 13:14:54 +0100201 exit 1
202 fi
203
Michal Zeganbacf0a12019-03-22 14:16:04 +0100204#Note, this double mounting is needed to support older util-linux.
205 if ! mount -o bind "${external}" "${CHROOT_DIR}/${internal}" ||
206 ! mount -o remount,bind,${mount_type} "${CHROOT_DIR}/${internal}" ; then
Petr Ospalý89583002018-12-19 13:14:54 +0100207 echo ERROR: "Failed to mount: ${external} -> ${internal}" >&2
208 exit 1
209 else
210 echo INFO: "Mount: ${external} -> ${internal}" >&2
211 fi
212 done
213}
214
Petr Ospalý89583002018-12-19 13:14:54 +0100215
216
217#
Michal Zeganbacf0a12019-03-22 14:16:04 +0100218# parse arguments out of namespace.
Petr Ospalý89583002018-12-19 13:14:54 +0100219#
220
Michal Zeganbacf0a12019-03-22 14:16:04 +0100221if [ -z $IN_NAMESPACE ]; then
222 export state=nil
223 export action=nil
224 export EXTERNAL_MOUNTS=''
225 export CHROOT_WORKDIR=''
226 export CHROOT_METADIR=''
227 export CHROOT_DIR=''
228 export COMMAND=''
229 while [ -n "$1" ] ; do
230 case "$state" in
231 nil)
232 case "$1" in
233 ''|-h|--help|help)
234 help
235 exit 0
236 ;;
237 --mount)
238 EXTERNAL_MOUNTS=$(printf "%s\n%s" "$EXTERNAL_MOUNTS" "${2}")
Petr Ospalý89583002018-12-19 13:14:54 +0100239 state=next
Michal Zeganbacf0a12019-03-22 14:16:04 +0100240 ;;
241 --workdir)
242 if [ -z "$CHROOT_WORKDIR" ] ; then
243 CHROOT_WORKDIR="$2"
244 state=next
245 else
246 echo ERROR: "Multiple working directory argument" >&2
247 help >&2
248 exit 1
249 fi
250 ;;
251 execute)
252 action=execute
253 state=execute
254 ;;
255 *)
256 echo ERROR: "Bad usage" >&2
Petr Ospalý89583002018-12-19 13:14:54 +0100257 help >&2
258 exit 1
Michal Zeganbacf0a12019-03-22 14:16:04 +0100259 ;;
260 esac
261 ;;
262 next)
263 state=nil
264 ;;
265 execute)
266 CHROOT_METADIR="$1"
267 shift
268 break
269 ;;
270 esac
271 shift
272 done
Petr Ospalý89583002018-12-19 13:14:54 +0100273
274
Michal Zeganbacf0a12019-03-22 14:16:04 +0100275 if [ $action = "nil" ]; then
Petr Ospalý89583002018-12-19 13:14:54 +0100276 echo ERROR: "Nothing to do - missing command" >&2
277 help >&2
278 exit 1
Michal Zeganbacf0a12019-03-22 14:16:04 +0100279 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100280
Michal Zeganbacf0a12019-03-22 14:16:04 +0100281 # do sanity checking ...
Petr Ospalý89583002018-12-19 13:14:54 +0100282
Michal Zeganbacf0a12019-03-22 14:16:04 +0100283 if [ -z "$CHROOT_METADIR" ] ; then
284 echo ERROR: "Missing argument" >&2
285 help >&2
286 exit 1
287 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100288
Michal Zeganbacf0a12019-03-22 14:16:04 +0100289 # making sure that CHROOT_METADIR is absolute path
290 CHROOT_METADIR=$(readlink -f "$CHROOT_METADIR")
Petr Ospalý89583002018-12-19 13:14:54 +0100291
Michal Zeganbacf0a12019-03-22 14:16:04 +0100292 if ! [ -d "$CHROOT_METADIR"/chroot ] ; then
293 echo ERROR: "Filepath does not exist: ${CHROOT_METADIR}/chroot" >&2
294 exit 1
295 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100296
Michal Zeganbacf0a12019-03-22 14:16:04 +0100297 # check external mounts if there are any
298 check_external_mounts
Petr Ospalý89583002018-12-19 13:14:54 +0100299
Michal Zeganbacf0a12019-03-22 14:16:04 +0100300 # we must be root
301 if [ "$(id -u)" -ne 0 ] ; then
302 echo ERROR: "Need to be root and you are not: $(id -nu)" >&2
303 exit 1
304 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100305
Michal Zeganbacf0a12019-03-22 14:16:04 +0100306 if ! which unshare >/dev/null 2>/dev/null ; then
307 echo ERROR: "'unshare' system command is missing - ABORT" >&2
308 echo INFO: "Try to install 'util-linux' package" >&2
309 exit 1
310 fi
Petr Ospalý89583002018-12-19 13:14:54 +0100311
Michal Zeganbacf0a12019-03-22 14:16:04 +0100312 # ... sanity checking done
Petr Ospalý89583002018-12-19 13:14:54 +0100313
Michal Zeganbacf0a12019-03-22 14:16:04 +0100314 #Reexec ourselves in new pid and mount namespace (isolate!).
315 #Note: newly executed shell will be pid1 in a new namespace. Killing it will kill
316 #every other process in the whole process tree with sigkill. That will in turn
317 #destroy namespaces and undo all mounts done previously.
318 IN_NAMESPACE=1 exec unshare -mpf "$0" "$@"
319fi
Petr Ospalý89583002018-12-19 13:14:54 +0100320
Michal Zeganbacf0a12019-03-22 14:16:04 +0100321#We are namespaced.
322# setup paths
323lowerdir="$CHROOT_METADIR"/chroot
324ovtempdir="$CHROOT_METADIR"/tmp
325upperdir="$ovtempdir"/.overlay
326workdir="$ovtempdir"/.workdir
327overlay="$CHROOT_METADIR"/.merged
Petr Ospalý89583002018-12-19 13:14:54 +0100328
Michal Zeganbacf0a12019-03-22 14:16:04 +0100329#In case we are using a realy old unshare, make the whole tree into private mounts manually.
330mount --make-rprivate /
331#New mounts are private always from now on.
Petr Ospalý89583002018-12-19 13:14:54 +0100332
Michal Zeganbacf0a12019-03-22 14:16:04 +0100333do_overlay_mount
Petr Ospalý89583002018-12-19 13:14:54 +0100334
Michal Zeganbacf0a12019-03-22 14:16:04 +0100335# do the user-specific mounts
336do_external_mounts
Petr Ospalý89583002018-12-19 13:14:54 +0100337
Michal Zeganbacf0a12019-03-22 14:16:04 +0100338#And setup api filesystems.
339mount -t proc proc "${CHROOT_DIR}/proc"
340mount -t sysfs none "${CHROOT_DIR}/sys"
341mount -t tmpfs none "${CHROOT_DIR}/dev"
342
343mkdir -p "${CHROOT_DIR}/dev/shm"
344mkdir -p "${CHROOT_DIR}/dev/pts"
345mount -t devpts none "${CHROOT_DIR}/dev/pts"
346
347mknod -m 666 "${CHROOT_DIR}/dev/full" c 1 7
348mknod -m 666 "${CHROOT_DIR}/dev/ptmx" c 5 2
349mknod -m 644 "${CHROOT_DIR}/dev/random" c 1 8
350mknod -m 644 "${CHROOT_DIR}/dev/urandom" c 1 9
351mknod -m 666 "${CHROOT_DIR}/dev/zero" c 1 5
352mknod -m 666 "${CHROOT_DIR}/dev/tty" c 5 0
353mknod -m 622 "${CHROOT_DIR}/dev/console" c 5 1
354mknod -m 666 "${CHROOT_DIR}/dev/null" c 1 3
355ln -s /proc/self/fd/0 "$CHROOT_DIR/dev/stdin"
356ln -s /proc/self/fd/1 "$CHROOT_DIR/dev/stdout"
357ln -s /proc/self/fd/2 "$CHROOT_DIR/dev/stderr"
358
359# execute chroot
360if [ -z "$1" ] ; then
361 set -- /bin/sh -l
362fi
363
364#The redirection is to save our stdin, because we use it to pipe commands and we
365#may want interactivity.
366exec chroot "${CHROOT_DIR}" /bin/sh /dev/stdin "${CHROOT_WORKDIR:-/}" "$@" 3<&0 << "EOF"
Petr Ospalýbef7cab2019-04-05 09:59:30 +0200367PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
368export PATH
Michal Zeganbacf0a12019-03-22 14:16:04 +0100369mkdir -p $1
370cd $1
371shift
372#I intend to reset stdin back *and* close the copy.
373exec "$@" <&3 3<&-
374EOF
Petr Ospalý89583002018-12-19 13:14:54 +0100375
376exit 0
377