comparison sbin/ftjail @ 233:f745d3a216a6

Erste Basis für "ftjail": Management von Thin Jails
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 05 Sep 2022 09:45:13 +0200
parents
children 8682cfa74f6a
comparison
equal deleted inserted replaced
232:0c8acf1021e3 233:f745d3a216a6
1 #!/bin/sh
2 # -*- indent-tabs-mode: nil; -*-
3 : 'A very minimal BSD Thin Jail management tool.
4
5 :Author: Franz Glasner
6 :Copyright: (c) 2022 Franz Glasner.
7 All rights reserved.
8 :License: BSD 3-Clause "New" or "Revised" License.
9 See LICENSE for details.
10 If you cannot find LICENSE see
11 <https://opensource.org/licenses/BSD-3-Clause>
12 :ID: @(#)@@PKGORIGIN@@ $HGid$
13
14 '
15
16 set -eu
17
18 VERSION="@@VERSION@@"
19
20 USAGE='
21 USAGE: ftjail [ OPTIONS ] COMMAND [ COMMAND OPTIONS ] [ ARG ... ]
22
23 OPTIONS:
24
25 -V Print the program name and version number to stdout and exit
26
27 -h Print this help message to stdout and exit
28
29 COMMANDS:
30
31 datasets [OPTIONS] PARENT-BASE PARENT-SKELETON NAME
32
33 Create ZFS datasets for the ro base and the rw skeleton to be used
34 within for jails
35
36 PARENT-BASE and PARENT-SKELETON must exist already and NAME must
37 not exist.
38
39 The datasets will not be mounted.
40
41 ENVIRONMENT:
42
43 All environment variables that affect "zfs" are effective also.
44
45 DESCRIPTION:
46
47 All commands with the exception of "populate" require ZFS as
48 filesystem.
49 '
50
51 # Reset to standard umask
52 umask 0022
53
54
55 _get_dataset_for_mountpoint() {
56 : 'Use `mount -t zfs -p` to determine the ZFS dataset for a given mountpoint.
57
58 '
59 local _mountpoint
60 local _ds _mount _rest
61
62 _mountpoint="$1"
63
64 mount -t zfs -p \
65 | {
66 while IFS=' '$'\t' read -r _ds _mount _rest ; do
67 if [ "$_mount" = "$_mountpoint" ]; then
68 echo "${_ds}"
69 return 0
70 fi
71 done
72 return 1
73 }
74 }
75
76
77 #
78 # "datasets" -- create the ZFS dataset tree
79 #
80 command_datasets() {
81 # parent ZFS dataset -- child ZFS dataset name
82 local _pds _cds
83 # and its mount point
84 local _pmp _get
85 # full name of the dataset
86 local _ds
87 # dynamic ZFS options -- create cache for freebsd-update -- use a more tiny layout
88 local _zfsopts _fbsdupdate _tiny _zfsnoauto _varempty_ro
89
90 _zfsopts=""
91 _fbsdupdate=""
92 _tiny="no"
93 _zfsnoauto=""
94 _varempty_ro="-o readonly=on"
95 while getopts "oustAT" _opt ; do
96 case ${_opt} in
97 A)
98 #
99 # set canmount=noauto where otherwise canmount=on would have been set
100 # or inherited
101 #
102 _zfsnoauto="-o canmount=noauto"
103 ;;
104 o)
105 # Clear out the default setting of creating var/empty as read-only dataset
106 _varempty_ro=""
107 ;;
108 t)
109 # use a more tiny layout
110 _tiny="yes"
111 ;;
112 T) # extra tiny layout
113 _tiny="extra"
114 ;;
115 u)
116 # do not mount newly created datasets
117 _zfsopts="${_zfsopts} -u"
118 ;;
119 s)
120 # create also a dataset for freebsd-update data
121 _fbsdupdate="yes"
122 ;;
123 \?|:)
124 return 2;
125 ;;
126 esac
127 done
128 shift $((OPTIND-1))
129 OPTIND=1
130
131 _pds="$1"
132 if [ -z "${_pds}" ]; then
133 echo "ERROR: no parent dataset given" >&2
134 return 2
135 fi
136 _pmp=$(zfs list -H -o mountpoint -t filesystem "${_pds}" 2>/dev/null) || { echo "ERROR: dataset \`${_pds}' does not exist" >&2; return 1; }
137 case "${_pmp}" in
138 none)
139 echo "ERROR: dataset \`${_pds}' has no mountpoint" >&2
140 return 1
141 ;;
142 legacy)
143 echo "ERROR: dataset \`${_pds}' has a \`${_mp}' mountpoint" >&2
144 return 1
145 ;;
146 *)
147 # VOID
148 ;;
149 esac
150 _cds="$2"
151 if [ -z "${_cds}" ]; then
152 echo "ERROR: no child dataset given" >&2
153 return 2
154 fi
155 _ds="${_pds}/${_cds}"
156 echo "Resulting new root dataset is \`${_ds}' at mountpoint \`${_pmp}/${_cds}'"
157 if zfs list -H -o mountpoint -t filesystem "${_ds}" >/dev/null 2>/dev/null; then
158 echo "ERROR: dataset \`${_ds}' does already exist" >&2
159 return 1
160 fi
161
162 #
163 # NOTE: For BEs these directory will be *excluded* from the BE
164 #
165 # /tmp
166 # /usr/home
167 # /usr/ports
168 # /usr/src
169 # /var/audit
170 # /var/crash
171 # /var/log
172 # /var/mail
173 # /var/tmp
174 #
175 zfs create ${_zfsopts} ${_zfsnoauto} -o atime=off "${_ds}"
176 zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off "${_ds}/tmp"
177 if [ "${_tiny}" != "extra" ]; then
178 if [ "${_tiny}" = "yes" ]; then
179 zfs create ${_zfsopts} -o canmount=off "${_ds}/usr"
180 else
181 zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/usr"
182 fi
183 zfs create ${_zfsopts} ${_zfsnoauto} -o setuid=off "${_ds}/usr/home"
184 zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/usr/local"
185 fi
186 if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then
187 zfs create ${_zfsopts} -o canmount=off "${_ds}/var"
188 else
189 zfs create ${_zfsopts} ${_zfsnoauto} "${_ds}/var"
190 fi
191 if [ "${_tiny}" != "extra" ]; then
192 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/audit"
193 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/cache"
194 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off "${_ds}/var/cache/pkg"
195 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o compression=off "${_ds}/var/crash"
196 fi
197 if [ "$_fbsdupdate" = "yes" ]; then
198 if [ \( "${_tiny}" = "yes" \) -o \( "${_tiny}" = "extra" \) ]; then
199 zfs create ${_zfsopts} -o canmount=off -o exec=off -o setuid=off "${_ds}/var/db"
200 else
201 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off "${_ds}/var/db"
202 fi
203 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata -o compression=off "${_ds}/var/db/freebsd-update"
204 fi
205 zfs create ${_zfsopts} ${_zfsnoauto} ${_varempty_ro} -o exec=off -o setuid=off "${_ds}/var/empty"
206 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o primarycache=metadata "${_ds}/var/log"
207 zfs create ${_zfsopts} ${_zfsnoauto} -o exec=off -o setuid=off -o atime=on "${_ds}/var/mail"
208 zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o exec=off -o setuid=off -o compression=off -o primarycache=all "${_ds}/var/run"
209 zfs create ${_zfsopts} ${_zfsnoauto} -o sync=disabled -o setuid=off "${_ds}/var/tmp"
210 }
211
212
213 #
214 # "populate" -- populate the datasets with content from a FreeBSD base.txz
215 #
216 # command_populate mountpoint basetxz
217 #
218 command_populate() {
219 # MOUNTPOINT -- base.txz
220 local _mp _basetxz
221
222 _mp="$1"
223 _basetxz="$2"
224
225 if [ -z "${_mp}" ]; then
226 echo "ERROR: no mountpoint given" >&2
227 return 2
228 fi
229 if [ -z "${_basetxz}" ]; then
230 echo "ERROR: no base.txz given" >&2
231 return 2
232 fi
233 if [ ! -d "${_mp}" ]; then
234 echo "ERROR: mountpoint \`${_mp}' does not exist" >&2
235 return 1
236 fi
237 if [ ! -r "${_basetxz}" ]; then
238 echo "ERROR: file \`${_basetxz}' is not readable" >&2
239 return 1
240 fi
241
242 #
243 # Handle /var/empty separately later: could be already there and
244 # mounted read-only.
245 #
246 tar -C "${_mp}" --exclude=./var/empty -xJp -f "${_basetxz}" || { echo "ERROR: tar encountered errors" >&2; return 1; }
247 if [ -d "${_mp}/var/empty" ]; then
248 #
249 # If /var/empty exists already try to extract with changing the
250 # flags (e.g. `schg'). But be ignore errors here.
251 #
252 tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "tar warnings for handling ./var/empty ignored because ./var/empty exists already" >&2; }
253 else
254 # Just extract /var/empty normally
255 tar -C "${_mp}" -xJp -f "${_basetxz}" ./var/empty || { echo "ERROR: tar encountered errors" >&2; return 1; }
256 fi
257
258 find "${_mp}/boot" -type f -delete
259 }
260
261
262 #
263 # "mount" -- recursively mount a dataset including subordinate datasets
264 #
265 # command_mount dataset mountpoint
266 #
267 command_mount() {
268 local _dsname _mountpoint
269 local _name _mp _canmount _mounted
270 local _rootds_mountpoint _relative_mp _real_mp
271 local _dry_run _mount_outside _mount_natural
272
273 _dry_run=""
274 _mount_outside=""
275 _mount_natural=""
276 while getopts "ONnu" _opt ; do
277 case ${_opt} in
278 O)
279 _mount_outside="yes"
280 ;;
281 N)
282 _mount_natural="yes"
283 ;;
284 n|u)
285 _dry_run="yes"
286 ;;
287 \?|:)
288 return 2;
289 ;;
290 esac
291 done
292 shift $((OPTIND-1))
293 OPTIND=1
294
295 _dsname="${1-}"
296 _mountpoint="${2-}"
297
298 if [ -z "${_dsname}" ]; then
299 echo "ERROR: no dataset given" >&2
300 return 2
301 fi
302
303 _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")" || \
304 { echo "ERROR: root dataset does not exist" >&2; return 1; }
305
306 if [ -z "${_mountpoint}" ]; then
307 if [ "${_mount_natural}" = "yes" ]; then
308 _mountpoint="${_rootds_mountpoint}"
309 else
310 echo "ERROR: no mountpoint given" >&2
311 return 2
312 fi
313 else
314 if [ "${_mount_natural}" = "yes" ]; then
315 echo "ERROR: Cannot have a custom mountpoint when \"-O\" is given" >&2
316 return 2
317 fi
318 fi
319
320 # Eventually remove a trailing slash
321 _mountpoint="${_mountpoint%/}"
322 if [ -z "${_mountpoint}" ]; then
323 echo "ERROR: would mount over the root filesystem" >&2
324 return 1
325 fi
326
327 zfs list -H -o name,mountpoint,canmount,mounted -s mountpoint -t filesystem -r "${_dsname}" \
328 | {
329 while IFS=$'\t' read -r _name _mp _canmount _mounted ; do
330 # Skip filesystems that are already mounted
331 [ "${_mounted}" = "yes" ] && continue
332 # Skip filesystems that must not be mounted
333 [ "${_canmount}" = "off" ] && continue
334 case "${_mp}" in
335 "none"|"legacy")
336 # Do nothing for filesystem with unset or legacy mountpoints
337 ;;
338 "${_rootds_mountpoint}"|"${_rootds_mountpoint}/"*)
339 #
340 # Handle only mountpoints that have a mountpoint below
341 # the parent datasets mountpoint
342 #
343
344 # Determine the mountpoint relative to the parent mountpoint
345 _relative_mp="${_mp#${_rootds_mountpoint}}"
346 # Eventually remove a trailing slash
347 _relative_mp="${_relative_mp%/}"
348 # The real effective full mountpoint
349 _real_mp="${_mountpoint}${_relative_mp}"
350
351 #
352 # Consistency and sanity check: computed real mountpoint must
353 # be equal to the configured mountpoint when no custom mountpoint
354 # is given.
355 #
356 if [ "${_mount_natural}" = "yes" ]; then
357 if [ "${_real_mp}" != "${_mp}" ]; then
358 echo "ERROR: mountpoint mismatch" >&2
359 return 1
360 fi
361 fi
362
363 if [ "${_dry_run}" = "yes" ]; then
364 echo "Would mount ${_name} on ${_real_mp}"
365 else
366 mkdir -p "${_real_mp}" 1> /dev/null 2> /dev/null || \
367 { echo "ERROR: cannot create mountpoint ${_real_mp}" >&2; return 1; }
368 echo "Mounting ${_name} on ${_real_mp}"
369 mount -t zfs "${_name}" "${_real_mp}" || return 1
370 fi
371 ;;
372 *)
373 if [ "${_mount_outside}" = "yes" ]; then
374 if [ "${_dry_run}" = "yes" ]; then
375 echo "Would mount ${_name} on configured ZFS dataset mountpoint ${_mp}"
376 else
377 echo "Mounting ${_name} on configured ZFS dataset mountpoint ${_mp}"
378 zfs mount "${_name}" || return 1
379 fi
380 else
381 echo "Skipping ${_name} because its configured ZFS mountpoint is not relative to given root dataset" 2>&1
382 fi
383 ;;
384 esac
385 done
386
387 return 0
388 }
389 }
390
391
392 #
393 # "umount" -- Recursively unmount ZFS datasets
394 #
395 # command_umount dataset
396 #
397 command_umount() {
398 local _dsname
399 local _name _mp _rest
400 local _rootds_mountpoint
401
402 _dsname="${1-}"
403 [ -z "${_dsname}" ] && \
404 { echo "ERROR: no dataset given" >&2; return 2; }
405
406 # Just determine whether the given dataset name exists
407 _rootds_mountpoint="$(zfs list -H -o mountpoint -t filesystem "${_dsname}")" || { echo "ERROR: dataset not found" >&2; return 1; }
408
409 mount -t zfs -p \
410 | grep -E "^${_dsname}(/|\s)" \
411 | sort -n -r \
412 | {
413 while IFS=' '$'\t' read -r _name _mp _rest ; do
414 echo "Umounting ${_name} on ${_mp}"
415 umount "${_mp}" || return 1
416 done
417 return 0
418 }
419 }
420
421
422 #
423 # Global option handling
424 #
425 while getopts "Vh" _opt ; do
426 case ${_opt} in
427 V)
428 printf 'ftjail v%s (rv:%s)\n' "${VERSION}" '@@HGREVISION@@'
429 exit 0
430 ;;
431 h)
432 echo "${USAGE}"
433 exit 0
434 ;;
435 \?)
436 exit 2;
437 ;;
438 *)
439 echo "ERROR: option handling failed" >&2
440 exit 2
441 ;;
442 esac
443 done
444
445 #
446 # Reset the Shell's option handling system to prepare for handling
447 # command-local options.
448 #
449 shift $((OPTIND-1))
450 OPTIND=1
451
452 test $# -gt 0 || { echo "ERROR: no command given" >&2; exit 2; }
453
454 command="$1"
455 shift
456
457 case "${command}" in
458 datasets)
459 command_datasets "$@"
460 ;;
461 populate)
462 command_populate "$@"
463 ;;
464 *)
465 echo "ERROR: unknown command \`${command}'" >&2
466 exit 2
467 ;;
468 esac