]> CyberLeo.Net >> Repos - FreeBSD/FreeBSD.git/blob - sys/contrib/openzfs/etc/systemd/system-generators/zfs-mount-generator.in
ZFS: MFV 2.0-rc1-gfd20a8
[FreeBSD/FreeBSD.git] / sys / contrib / openzfs / etc / systemd / system-generators / zfs-mount-generator.in
1 #!/bin/sh
2
3 # zfs-mount-generator - generates systemd mount units for zfs
4 # Copyright (c) 2017 Antonio Russo <antonio.e.russo@gmail.com>
5 # Copyright (c) 2020 InsanePrawn <insane.prawny@gmail.com>
6 #
7 # Permission is hereby granted, free of charge, to any person obtaining
8 # a copy of this software and associated documentation files (the
9 # "Software"), to deal in the Software without restriction, including
10 # without limitation the rights to use, copy, modify, merge, publish,
11 # distribute, sublicense, and/or sell copies of the Software, and to
12 # permit persons to whom the Software is furnished to do so, subject to
13 # the following conditions:
14 #
15 # The above copyright notice and this permission notice shall be
16 # included in all copies or substantial portions of the Software.
17 #
18 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
26 set -e
27
28 FSLIST="@sysconfdir@/zfs/zfs-list.cache"
29
30 [ -d "${FSLIST}" ] || exit 0
31
32 do_fail() {
33   printf 'zfs-mount-generator: %s\n' "$*" > /dev/kmsg
34   exit 1
35 }
36
37 # test if $1 is in space-separated list $2
38 is_known() {
39   query="$1"
40   IFS=' '
41   for element in $2 ; do
42     if [ "$query" = "$element" ] ; then
43       return 0
44     fi
45   done
46   return 1
47 }
48
49 # create dependency on unit file $1
50 # of type $2, i.e. "wants" or "requires"
51 # in the target units from space-separated list $3
52 create_dependencies() {
53   unitfile="$1"
54   suffix="$2"
55   IFS=' '
56   for target in $3 ; do
57     target_dir="${dest_norm}/${target}.${suffix}/"
58     mkdir -p "${target_dir}"
59     ln -s "../${unitfile}" "${target_dir}"
60   done
61 }
62
63 # see systemd.generator
64 if [ $# -eq 0 ] ; then
65   dest_norm="/tmp"
66 elif [ $# -eq 3 ] ; then
67   dest_norm="${1}"
68 else
69   do_fail "zero or three arguments required"
70 fi
71
72 pools=$(zpool list -H -o name || true)
73
74 # All needed information about each ZFS is available from
75 # zfs list -H -t filesystem -o <properties>
76 # cached in $FSLIST, and each line is processed by the following function:
77 # See the list below for the properties and their order
78
79 process_line() {
80
81   # zfs list -H -o name,...
82   # fields are tab separated
83   IFS="$(printf '\t')"
84   # shellcheck disable=SC2086
85   set -- $1
86
87   dataset="${1}"
88   pool="${dataset%%/*}"
89   p_mountpoint="${2}"
90   p_canmount="${3}"
91   p_atime="${4}"
92   p_relatime="${5}"
93   p_devices="${6}"
94   p_exec="${7}"
95   p_readonly="${8}"
96   p_setuid="${9}"
97   p_nbmand="${10}"
98   p_encroot="${11}"
99   p_keyloc="${12}"
100   p_systemd_requires="${13}"
101   p_systemd_requiresmountsfor="${14}"
102   p_systemd_before="${15}"
103   p_systemd_after="${16}"
104   p_systemd_wantedby="${17}"
105   p_systemd_requiredby="${18}"
106   p_systemd_nofail="${19}"
107   p_systemd_ignore="${20}"
108
109   # Minimal pre-requisites to mount a ZFS dataset
110   # By ordering before zfs-mount.service, we avoid race conditions.
111   after="zfs-import.target"
112   before="zfs-mount.service"
113   wants="zfs-import.target"
114   requires=""
115   requiredmounts=""
116   bindsto=""
117   wantedby=""
118   requiredby=""
119   noauto="off"
120
121   # If the pool is already imported, zfs-import.target is not needed.  This
122   # avoids a dependency loop on root-on-ZFS systems:
123   # systemd-random-seed.service After (via RequiresMountsFor) var-lib.mount
124   # After zfs-import.target After zfs-import-{cache,scan}.service After
125   # cryptsetup.service After systemd-random-seed.service.
126   #
127   # Pools are newline-separated and may contain spaces in their names.
128   # There is no better portable way to set IFS to just a newline.  Using
129   # $(printf '\n') doesn't work because $(...) strips trailing newlines.
130   IFS="
131 "
132   for p in $pools ; do
133     if [ "$p" = "$pool" ] ; then
134       after=""
135       wants=""
136       break
137     fi
138   done
139
140   if [ -n "${p_systemd_after}" ] && \
141       [ "${p_systemd_after}" != "-" ] ; then
142     after="${p_systemd_after} ${after}"
143   fi
144
145   if [ -n "${p_systemd_before}" ] && \
146       [ "${p_systemd_before}" != "-" ] ; then
147     before="${p_systemd_before} ${before}"
148   fi
149
150   if [ -n "${p_systemd_requires}" ] && \
151       [ "${p_systemd_requires}" != "-" ] ; then
152     requires="Requires=${p_systemd_requires}"
153   fi
154
155   if [ -n "${p_systemd_requiresmountsfor}" ] && \
156       [ "${p_systemd_requiresmountsfor}" != "-" ] ; then
157     requiredmounts="RequiresMountsFor=${p_systemd_requiresmountsfor}"
158   fi
159
160   # Handle encryption
161   if [ -n "${p_encroot}" ] &&
162       [ "${p_encroot}" != "-" ] ; then
163     keyloadunit="zfs-load-key-$(systemd-escape "${p_encroot}").service"
164     if [ "${p_encroot}" = "${dataset}" ] ; then
165       keymountdep=""
166       if [ "${p_keyloc%%://*}" = "file" ] ; then
167         if [ -n "${requiredmounts}" ] ; then
168           keymountdep="${requiredmounts} '${p_keyloc#file://}'"
169         else
170           keymountdep="RequiresMountsFor='${p_keyloc#file://}'"
171         fi
172         keyloadscript="@sbindir@/zfs load-key \"${dataset}\""
173       elif [ "${p_keyloc}" = "prompt" ] ; then
174         keyloadscript="\
175 count=0;\
176 while [ \$\$count -lt 3 ];do\
177   systemd-ask-password --id=\"zfs:${dataset}\"\
178     \"Enter passphrase for ${dataset}:\"|\
179     @sbindir@/zfs load-key \"${dataset}\" && exit 0;\
180   count=\$\$((count + 1));\
181 done;\
182 exit 1"
183       else
184         printf 'zfs-mount-generator: (%s) invalid keylocation\n' \
185           "${dataset}" >/dev/kmsg
186       fi
187       keyloadcmd="\
188 /bin/sh -c '\
189 set -eu;\
190 keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\
191 [ \"\$\$keystatus\" = \"unavailable\" ] || exit 0;\
192 ${keyloadscript}'"
193       keyunloadcmd="\
194 /bin/sh -c '\
195 set -eu;\
196 keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\
197 [ \"\$\$keystatus\" = \"available\" ] || exit 0;\
198 @sbindir@/zfs unload-key \"${dataset}\"'"
199
200
201
202       # Generate the key-load .service unit
203       #
204       # Note: It is tempting to use a `<<EOF` style here-document for this, but
205       #   bash requires a writable /tmp or $TMPDIR for that. This is not always
206       #   available early during boot.
207       #
208       echo \
209 "# Automatically generated by zfs-mount-generator
210
211 [Unit]
212 Description=Load ZFS key for ${dataset}
213 SourcePath=${cachefile}
214 Documentation=man:zfs-mount-generator(8)
215 DefaultDependencies=no
216 Wants=${wants}
217 After=${after}
218 ${requires}
219 ${keymountdep}
220
221 [Service]
222 Type=oneshot
223 RemainAfterExit=yes
224 # This avoids a dependency loop involving systemd-journald.socket if this
225 # dataset is a parent of the root filesystem.
226 StandardOutput=null
227 StandardError=null
228 ExecStart=${keyloadcmd}
229 ExecStop=${keyunloadcmd}"   > "${dest_norm}/${keyloadunit}"
230     fi
231     # Update the dependencies for the mount file to want the
232     # key-loading unit.
233     wants="${wants}"
234     bindsto="BindsTo=${keyloadunit}"
235     after="${after} ${keyloadunit}"
236   fi
237
238   # Prepare the .mount unit
239
240   # skip generation of the mount unit if org.openzfs.systemd:ignore is "on"
241   if [ -n "${p_systemd_ignore}" ] ; then
242     if [ "${p_systemd_ignore}" = "on" ] ; then
243       return
244     elif [ "${p_systemd_ignore}" = "-" ] \
245       || [ "${p_systemd_ignore}" = "off" ] ; then
246       : # This is OK
247     else
248       do_fail "invalid org.openzfs.systemd:ignore for ${dataset}"
249     fi
250   fi
251
252   # Check for canmount=off .
253   if [ "${p_canmount}" = "off" ] ; then
254     return
255   elif [ "${p_canmount}" = "noauto" ] ; then
256     noauto="on"
257   elif [ "${p_canmount}" = "on" ] ; then
258     : # This is OK
259   else
260     do_fail "invalid canmount for ${dataset}"
261   fi
262
263   # Check for legacy and blank mountpoints.
264   if [ "${p_mountpoint}" = "legacy" ] ; then
265     return
266   elif [ "${p_mountpoint}" = "none" ] ; then
267     return
268   elif [ "${p_mountpoint%"${p_mountpoint#?}"}" != "/" ] ; then
269     do_fail "invalid mountpoint for ${dataset}"
270   fi
271
272   # Escape the mountpoint per systemd policy.
273   mountfile="$(systemd-escape --path --suffix=mount "${p_mountpoint}")"
274
275   # Parse options
276   # see lib/libzfs/libzfs_mount.c:zfs_add_options
277   opts=""
278
279   # atime
280   if [ "${p_atime}" = on ] ; then
281     # relatime
282     if [ "${p_relatime}" = on ] ; then
283       opts="${opts},atime,relatime"
284     elif [ "${p_relatime}" = off ] ; then
285       opts="${opts},atime,strictatime"
286     else
287       printf 'zfs-mount-generator: (%s) invalid relatime\n' \
288         "${dataset}" >/dev/kmsg
289     fi
290   elif [ "${p_atime}" = off ] ; then
291     opts="${opts},noatime"
292   else
293     printf 'zfs-mount-generator: (%s) invalid atime\n' \
294       "${dataset}" >/dev/kmsg
295   fi
296
297   # devices
298   if [ "${p_devices}" = on ] ; then
299     opts="${opts},dev"
300   elif [ "${p_devices}" = off ] ; then
301     opts="${opts},nodev"
302   else
303     printf 'zfs-mount-generator: (%s) invalid devices\n' \
304       "${dataset}" >/dev/kmsg
305   fi
306
307   # exec
308   if [ "${p_exec}" = on ] ; then
309     opts="${opts},exec"
310   elif [ "${p_exec}" = off ] ; then
311     opts="${opts},noexec"
312   else
313     printf 'zfs-mount-generator: (%s) invalid exec\n' \
314       "${dataset}" >/dev/kmsg
315   fi
316
317   # readonly
318   if [ "${p_readonly}" = on ] ; then
319     opts="${opts},ro"
320   elif [ "${p_readonly}" = off ] ; then
321     opts="${opts},rw"
322   else
323     printf 'zfs-mount-generator: (%s) invalid readonly\n' \
324       "${dataset}" >/dev/kmsg
325   fi
326
327   # setuid
328   if [ "${p_setuid}" = on ] ; then
329     opts="${opts},suid"
330   elif [ "${p_setuid}" = off ] ; then
331     opts="${opts},nosuid"
332   else
333     printf 'zfs-mount-generator: (%s) invalid setuid\n' \
334       "${dataset}" >/dev/kmsg
335   fi
336
337   # nbmand
338   if [ "${p_nbmand}" = on ]  ; then
339     opts="${opts},mand"
340   elif [ "${p_nbmand}" = off ] ; then
341     opts="${opts},nomand"
342   else
343     printf 'zfs-mount-generator: (%s) invalid nbmand\n' \
344       "${dataset}" >/dev/kmsg
345   fi
346
347   if [ -n "${p_systemd_wantedby}" ] && \
348       [ "${p_systemd_wantedby}" != "-" ] ; then
349     noauto="on"
350     if [ "${p_systemd_wantedby}" = "none" ] ; then
351       wantedby=""
352     else
353       wantedby="${p_systemd_wantedby}"
354       before="${before} ${wantedby}"
355     fi
356   fi
357
358   if [ -n "${p_systemd_requiredby}" ] && \
359       [ "${p_systemd_requiredby}" != "-" ] ; then
360     noauto="on"
361     if [ "${p_systemd_requiredby}" = "none" ] ; then
362       requiredby=""
363     else
364       requiredby="${p_systemd_requiredby}"
365       before="${before} ${requiredby}"
366     fi
367   fi
368
369   # For datasets with canmount=on, a dependency is created for
370   # local-fs.target by default. To avoid regressions, this dependency
371   # is reduced to "wants" rather than "requires" when nofail is not "off".
372   # **THIS MAY CHANGE**
373   # noauto=on disables this behavior completely.
374   if [ "${noauto}" != "on" ] ; then
375     if [ "${p_systemd_nofail}" = "off" ] ; then
376       requiredby="local-fs.target"
377       before="${before} local-fs.target"
378     else
379       wantedby="local-fs.target"
380       if [ "${p_systemd_nofail}" != "on" ] ; then
381         before="${before} local-fs.target"
382       fi
383     fi
384   fi
385
386   # Handle existing files:
387   # 1.  We never overwrite existing files, although we may delete
388   #     files if we're sure they were created by us. (see 5.)
389   # 2.  We handle files differently based on canmount. Units with canmount=on
390   #     always have precedence over noauto. This is enforced by the sort pipe
391   #     in the loop around this function.
392   #     It is important to use $p_canmount and not $noauto here, since we
393   #     sort by canmount while other properties also modify $noauto, e.g.
394   #     org.openzfs.systemd:wanted-by.
395   # 3.  If no unit file exists for a noauto dataset, we create one.
396   #     Additionally, we use $noauto_files to track the unit file names
397   #     (which are the systemd-escaped mountpoints) of all (exclusively)
398   #     noauto datasets that had a file created.
399   # 4.  If the file to be created is found in the tracking variable,
400   #     we do NOT create it.
401   # 5.  If a file exists for a noauto dataset, we check whether the file
402   #     name is in the variable. If it is, we have multiple noauto datasets
403   #     for the same mountpoint. In such cases, we remove the file for safety.
404   #     To avoid further noauto datasets creating a file for this path again,
405   #     we leave the file name in the tracking variable.
406   if [ -e "${dest_norm}/${mountfile}" ] ; then
407     if is_known "$mountfile" "$noauto_files" ; then
408       # if it's in $noauto_files, we must be noauto too. See 2.
409       printf 'zfs-mount-generator: removing duplicate noauto %s\n' \
410         "${mountfile}" >/dev/kmsg
411       # See 5.
412       rm "${dest_norm}/${mountfile}"
413     else
414       # don't log for canmount=noauto
415       if [  "${p_canmount}" = "on" ] ; then
416         printf 'zfs-mount-generator: %s already exists. Skipping.\n' \
417           "${mountfile}" >/dev/kmsg
418       fi
419     fi
420     # file exists; Skip current dataset.
421     return
422   else
423     if is_known "${mountfile}" "${noauto_files}" ; then
424       # See 4.
425       return
426     elif [ "${p_canmount}" = "noauto" ] ; then
427       noauto_files="${mountfile} ${noauto_files}"
428     fi
429   fi
430
431   # Create the .mount unit file.
432   #
433   # (Do not use `<<EOF`-style here-documents for this, see warning above)
434   #
435   echo \
436 "# Automatically generated by zfs-mount-generator
437
438 [Unit]
439 SourcePath=${cachefile}
440 Documentation=man:zfs-mount-generator(8)
441
442 Before=${before}
443 After=${after}
444 Wants=${wants}
445 ${bindsto}
446 ${requires}
447 ${requiredmounts}
448
449 [Mount]
450 Where=${p_mountpoint}
451 What=${dataset}
452 Type=zfs
453 Options=defaults${opts},zfsutil" > "${dest_norm}/${mountfile}"
454
455   # Finally, create the appropriate dependencies
456   create_dependencies "${mountfile}" "wants" "$wantedby"
457   create_dependencies "${mountfile}" "requires" "$requiredby"
458
459 }
460
461 for cachefile in "${FSLIST}/"* ; do
462   # Disable glob expansion to protect against special characters when parsing.
463   set -f
464   # Sort cachefile's lines by canmount, "on" before "noauto"
465   # and feed each line into process_line
466   sort -t "$(printf '\t')" -k 3 -r "${cachefile}" | \
467   ( # subshell is necessary for `sort|while read` and $noauto_files
468     noauto_files=""
469     while read -r fs ; do
470       process_line "${fs}"
471     done
472   )
473 done