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>
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:
15 # The above copyright notice and this permission notice shall be
16 # included in all copies or substantial portions of the Software.
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.
28 FSLIST="@sysconfdir@/zfs/zfs-list.cache"
30 [ -d "${FSLIST}" ] || exit 0
33 printf 'zfs-mount-generator: %s\n' "$*" > /dev/kmsg
37 # test if $1 is in space-separated list $2
41 for element in $2 ; do
42 if [ "$query" = "$element" ] ; then
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() {
57 target_dir="${dest_norm}/${target}.${suffix}/"
58 mkdir -p "${target_dir}"
59 ln -s "../${unitfile}" "${target_dir}"
63 # see systemd.generator
64 if [ $# -eq 0 ] ; then
66 elif [ $# -eq 3 ] ; then
69 do_fail "zero or three arguments required"
72 pools=$(zpool list -H -o name || true)
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
81 # zfs list -H -o name,...
82 # fields are tab separated
84 # shellcheck disable=SC2086
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}"
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"
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.
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.
133 if [ "$p" = "$pool" ] ; then
140 if [ -n "${p_systemd_after}" ] && \
141 [ "${p_systemd_after}" != "-" ] ; then
142 after="${p_systemd_after} ${after}"
145 if [ -n "${p_systemd_before}" ] && \
146 [ "${p_systemd_before}" != "-" ] ; then
147 before="${p_systemd_before} ${before}"
150 if [ -n "${p_systemd_requires}" ] && \
151 [ "${p_systemd_requires}" != "-" ] ; then
152 requires="Requires=${p_systemd_requires}"
155 if [ -n "${p_systemd_requiresmountsfor}" ] && \
156 [ "${p_systemd_requiresmountsfor}" != "-" ] ; then
157 requiredmounts="RequiresMountsFor=${p_systemd_requiresmountsfor}"
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
166 if [ "${p_keyloc%%://*}" = "file" ] ; then
167 if [ -n "${requiredmounts}" ] ; then
168 keymountdep="${requiredmounts} '${p_keyloc#file://}'"
170 keymountdep="RequiresMountsFor='${p_keyloc#file://}'"
172 keyloadscript="@sbindir@/zfs load-key \"${dataset}\""
173 elif [ "${p_keyloc}" = "prompt" ] ; then
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));\
184 printf 'zfs-mount-generator: (%s) invalid keylocation\n' \
185 "${dataset}" >/dev/kmsg
190 keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\
191 [ \"\$\$keystatus\" = \"unavailable\" ] || exit 0;\
196 keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\
197 [ \"\$\$keystatus\" = \"available\" ] || exit 0;\
198 @sbindir@/zfs unload-key \"${dataset}\"'"
202 # Generate the key-load .service unit
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.
209 "# Automatically generated by zfs-mount-generator
212 Description=Load ZFS key for ${dataset}
213 SourcePath=${cachefile}
214 Documentation=man:zfs-mount-generator(8)
215 DefaultDependencies=no
224 # This avoids a dependency loop involving systemd-journald.socket if this
225 # dataset is a parent of the root filesystem.
228 ExecStart=${keyloadcmd}
229 ExecStop=${keyunloadcmd}" > "${dest_norm}/${keyloadunit}"
231 # Update the dependencies for the mount file to want the
234 bindsto="BindsTo=${keyloadunit}"
235 after="${after} ${keyloadunit}"
238 # Prepare the .mount unit
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
244 elif [ "${p_systemd_ignore}" = "-" ] \
245 || [ "${p_systemd_ignore}" = "off" ] ; then
248 do_fail "invalid org.openzfs.systemd:ignore for ${dataset}"
252 # Check for canmount=off .
253 if [ "${p_canmount}" = "off" ] ; then
255 elif [ "${p_canmount}" = "noauto" ] ; then
257 elif [ "${p_canmount}" = "on" ] ; then
260 do_fail "invalid canmount for ${dataset}"
263 # Check for legacy and blank mountpoints.
264 if [ "${p_mountpoint}" = "legacy" ] ; then
266 elif [ "${p_mountpoint}" = "none" ] ; then
268 elif [ "${p_mountpoint%"${p_mountpoint#?}"}" != "/" ] ; then
269 do_fail "invalid mountpoint for ${dataset}"
272 # Escape the mountpoint per systemd policy.
273 mountfile="$(systemd-escape --path --suffix=mount "${p_mountpoint}")"
276 # see lib/libzfs/libzfs_mount.c:zfs_add_options
280 if [ "${p_atime}" = on ] ; then
282 if [ "${p_relatime}" = on ] ; then
283 opts="${opts},atime,relatime"
284 elif [ "${p_relatime}" = off ] ; then
285 opts="${opts},atime,strictatime"
287 printf 'zfs-mount-generator: (%s) invalid relatime\n' \
288 "${dataset}" >/dev/kmsg
290 elif [ "${p_atime}" = off ] ; then
291 opts="${opts},noatime"
293 printf 'zfs-mount-generator: (%s) invalid atime\n' \
294 "${dataset}" >/dev/kmsg
298 if [ "${p_devices}" = on ] ; then
300 elif [ "${p_devices}" = off ] ; then
303 printf 'zfs-mount-generator: (%s) invalid devices\n' \
304 "${dataset}" >/dev/kmsg
308 if [ "${p_exec}" = on ] ; then
310 elif [ "${p_exec}" = off ] ; then
311 opts="${opts},noexec"
313 printf 'zfs-mount-generator: (%s) invalid exec\n' \
314 "${dataset}" >/dev/kmsg
318 if [ "${p_readonly}" = on ] ; then
320 elif [ "${p_readonly}" = off ] ; then
323 printf 'zfs-mount-generator: (%s) invalid readonly\n' \
324 "${dataset}" >/dev/kmsg
328 if [ "${p_setuid}" = on ] ; then
330 elif [ "${p_setuid}" = off ] ; then
331 opts="${opts},nosuid"
333 printf 'zfs-mount-generator: (%s) invalid setuid\n' \
334 "${dataset}" >/dev/kmsg
338 if [ "${p_nbmand}" = on ] ; then
340 elif [ "${p_nbmand}" = off ] ; then
341 opts="${opts},nomand"
343 printf 'zfs-mount-generator: (%s) invalid nbmand\n' \
344 "${dataset}" >/dev/kmsg
347 if [ -n "${p_systemd_wantedby}" ] && \
348 [ "${p_systemd_wantedby}" != "-" ] ; then
350 if [ "${p_systemd_wantedby}" = "none" ] ; then
353 wantedby="${p_systemd_wantedby}"
354 before="${before} ${wantedby}"
358 if [ -n "${p_systemd_requiredby}" ] && \
359 [ "${p_systemd_requiredby}" != "-" ] ; then
361 if [ "${p_systemd_requiredby}" = "none" ] ; then
364 requiredby="${p_systemd_requiredby}"
365 before="${before} ${requiredby}"
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"
379 wantedby="local-fs.target"
380 if [ "${p_systemd_nofail}" != "on" ] ; then
381 before="${before} local-fs.target"
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
412 rm "${dest_norm}/${mountfile}"
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
420 # file exists; Skip current dataset.
423 if is_known "${mountfile}" "${noauto_files}" ; then
426 elif [ "${p_canmount}" = "noauto" ] ; then
427 noauto_files="${mountfile} ${noauto_files}"
431 # Create the .mount unit file.
433 # (Do not use `<<EOF`-style here-documents for this, see warning above)
436 "# Automatically generated by zfs-mount-generator
439 SourcePath=${cachefile}
440 Documentation=man:zfs-mount-generator(8)
450 Where=${p_mountpoint}
453 Options=defaults${opts},zfsutil" > "${dest_norm}/${mountfile}"
455 # Finally, create the appropriate dependencies
456 create_dependencies "${mountfile}" "wants" "$wantedby"
457 create_dependencies "${mountfile}" "requires" "$requiredby"
461 for cachefile in "${FSLIST}/"* ; do
462 # Disable glob expansion to protect against special characters when parsing.
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
469 while read -r fs ; do