From ecbbdac799e0fd33f9d8b5fd6315008e3b4c9a50 Mon Sep 17 00:00:00 2001 From: InsanePrawn Date: Wed, 12 Feb 2020 18:01:15 +0100 Subject: [PATCH] Systemd mount generator: Generate noauto units; add control properties This commit refactors the systemd mount generators and makes the following major changes: - The generator now generates units for datasets marked canmount=noauto, too. These units are NOT WantedBy local-fs.target. If there are multiple noauto datasets for a path, no noauto unit will be created. Datasets with canmount=on are prioritized. - Introduces handling of new user properties which are now included in the zfs-list.cache files: - org.openzfs.systemd:requires: List of units to require for this mount unit - org.openzfs.systemd:requires-mounts-for: List of mounts to require by this mount unit - org.openzfs.systemd:before: List of units to order after this mount unit - org.openzfs.systemd:after: List of units to order before this mount unit - org.openzfs.systemd:wanted-by: List of units to add a Wants dependency on this mount unit to - org.openzfs.systemd:required-by: List of units to add a Requires dependency on this mount unit to - org.openzfs.systemd:nofail: Toggles between a wants and a requires dependency. - org.openzfs.systemd:ignore: Do not generate a mount unit for this dataset. Consult the updated man page for detailed documentation. - Restructures and extends the zfs-mount-generator(8) man page with the above properties, information on unit ordering and a license header. Reviewed-by: Richard Laager Reviewed-by: Antonio Russo Reviewed-by: Brian Behlendorf Signed-off-by: InsanePrawn Closes #9649 --- .../zed.d/history_event-zfs-list-cacher.sh.in | 17 +- .../system-generators/zfs-mount-generator.in | 217 +++++++++++++++--- man/man8/Makefile.am | 1 + man/man8/zfs-mount-generator.8.in | 197 ++++++++++++++-- 4 files changed, 386 insertions(+), 46 deletions(-) diff --git a/cmd/zed/zed.d/history_event-zfs-list-cacher.sh.in b/cmd/zed/zed.d/history_event-zfs-list-cacher.sh.in index 6d0f44ab32..053b4414a7 100755 --- a/cmd/zed/zed.d/history_event-zfs-list-cacher.sh.in +++ b/cmd/zed/zed.d/history_event-zfs-list-cacher.sh.in @@ -46,8 +46,13 @@ case "${ZEVENT_HISTORY_INTERNAL_NAME}" in set|inherit) # Only act if one of the tracked properties is altered. case "${ZEVENT_HISTORY_INTERNAL_STR%%=*}" in - canmount|mountpoint|atime|relatime|devices|exec| \ - readonly|setuid|nbmand|encroot|keylocation) ;; + canmount|mountpoint|atime|relatime|devices|exec|readonly| \ + setuid|nbmand|encroot|keylocation|org.openzfs.systemd:requires| \ + org.openzfs.systemd:requires-mounts-for| \ + org.openzfs.systemd:before|org.openzfs.systemd:after| \ + org.openzfs.systemd:wanted-by|org.openzfs.systemd:required-by| \ + org.openzfs.systemd:nofail|org.openzfs.systemd:ignore \ + ) ;; *) exit 0 ;; esac ;; @@ -61,8 +66,12 @@ esac zed_lock zfs-list trap abort_alter EXIT -PROPS="name,mountpoint,canmount,atime,relatime,devices,exec,readonly" -PROPS="${PROPS},setuid,nbmand,encroot,keylocation" +PROPS="name,mountpoint,canmount,atime,relatime,devices,exec\ +,readonly,setuid,nbmand,encroot,keylocation\ +,org.openzfs.systemd:requires,org.openzfs.systemd:requires-mounts-for\ +,org.openzfs.systemd:before,org.openzfs.systemd:after\ +,org.openzfs.systemd:wanted-by,org.openzfs.systemd:required-by\ +,org.openzfs.systemd:nofail,org.openzfs.systemd:ignore" "${ZFS}" list -H -t filesystem -o $PROPS -r "${ZEVENT_POOL}" > "${FSLIST_TMP}" diff --git a/etc/systemd/system-generators/zfs-mount-generator.in b/etc/systemd/system-generators/zfs-mount-generator.in index 411b3f9553..bb735112da 100755 --- a/etc/systemd/system-generators/zfs-mount-generator.in +++ b/etc/systemd/system-generators/zfs-mount-generator.in @@ -2,6 +2,7 @@ # zfs-mount-generator - generates systemd mount units for zfs # Copyright (c) 2017 Antonio Russo +# Copyright (c) 2020 InsanePrawn # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -33,6 +34,35 @@ do_fail() { exit 1 } +# test if $1 is in space-separated list $2 +is_known() { + query="$1" + IFS=' ' + # protect against special characters + set -f + for element in $2 ; do + if [ "$query" = "$element" ] ; then + return 0 + fi + done + return 1 +} + +# create dependency on unit file $1 +# of type $2, i.e. "wants" or "requires" +# in the target units from space-separated list $3 +create_dependencies() { + unitfile="$1" + suffix="$2" + # protect against special characters + set -f + for target in $3 ; do + target_dir="${dest_norm}/${target}.${suffix}/" + mkdir -p "${target_dir}" + ln -s "../${unitfile}" "${target_dir}" + done +} + # see systemd.generator if [ $# -eq 0 ] ; then dest_norm="/tmp" @@ -42,11 +72,6 @@ else do_fail "zero or three arguments required" fi -# For ZFSs marked "auto", a dependency is created for local-fs.target. To -# avoid regressions, this dependency is reduced to "wants" rather than -# "requires". **THIS MAY CHANGE** -req_dir="${dest_norm}/local-fs.target.wants/" -mkdir -p "${req_dir}" # All needed information about each ZFS is available from # zfs list -H -t filesystem -o @@ -74,18 +99,58 @@ process_line() { p_nbmand="${10}" p_encroot="${11}" p_keyloc="${12}" + p_systemd_requires="${13}" + p_systemd_requiresmountsfor="${14}" + p_systemd_before="${15}" + p_systemd_after="${16}" + p_systemd_wantedby="${17}" + p_systemd_requiredby="${18}" + p_systemd_nofail="${19}" + p_systemd_ignore="${20}" # Minimal pre-requisites to mount a ZFS dataset + # By ordering before zfs-mount.service, we avoid race conditions. + after="zfs-import.target" + before="zfs-mount.service" wants="zfs-import.target" + requires="" + requiredmounts="" + wantedby="" + requiredby="" + noauto="off" + + if [ -n "${p_systemd_after}" ] && \ + [ "${p_systemd_after}" != "-" ] ; then + after="${p_systemd_after} ${after}" + fi + + if [ -n "${p_systemd_before}" ] && \ + [ "${p_systemd_before}" != "-" ] ; then + before="${p_systemd_before} ${before}" + fi + + if [ -n "${p_systemd_requires}" ] && \ + [ "${p_systemd_requires}" != "-" ] ; then + requires="Requires=${p_systemd_requires}" + fi + + if [ -n "${p_systemd_requiresmountsfor}" ] && \ + [ "${p_systemd_requiresmountsfor}" != "-" ] ; then + requiredmounts="RequiresMountsFor=${p_systemd_requiresmountsfor}" + fi # Handle encryption if [ -n "${p_encroot}" ] && [ "${p_encroot}" != "-" ] ; then keyloadunit="zfs-load-key-$(systemd-escape "${p_encroot}").service" if [ "${p_encroot}" = "${dataset}" ] ; then - pathdep="" + keymountdep="" if [ "${p_keyloc%%://*}" = "file" ] ; then - pathdep="RequiresMountsFor='${p_keyloc#file://}'" + if [ -n "${requiredmounts}" ] ; then + keymountdep="${requiredmounts} '${p_keyloc#file://}'" + else + keymountdep="RequiresMountsFor='${p_keyloc#file://}'" + fi keyloadcmd="@sbindir@/zfs load-key '${dataset}'" elif [ "${p_keyloc}" = "prompt" ] ; then keyloadcmd="\ @@ -121,8 +186,10 @@ SourcePath=${cachefile} Documentation=man:zfs-mount-generator(8) DefaultDependencies=no Wants=${wants} -After=${wants} -${pathdep} +After=${after} +Before=${before} +${requires} +${keymountdep} [Service] Type=oneshot @@ -130,23 +197,35 @@ RemainAfterExit=yes ExecStart=${keyloadcmd} ExecStop=@sbindir@/zfs unload-key '${dataset}'" > "${dest_norm}/${keyloadunit}" fi - # Update the dependencies for the mount file to require the + # Update the dependencies for the mount file to want the # key-loading unit. wants="${wants} ${keyloadunit}" + after="${after} ${keyloadunit}" fi # Prepare the .mount unit + # skip generation of the mount unit if org.openzfs.systemd:ignore is "on" + if [ -n "${p_systemd_ignore}" ] ; then + if [ "${p_systemd_ignore}" = "on" ] ; then + return + elif [ "${p_systemd_ignore}" = "-" ] \ + || [ "${p_systemd_ignore}" = "off" ] ; then + : # This is OK + else + do_fail "invalid org.openzfs.systemd:ignore for ${dataset}" + fi + fi + # Check for canmount=off . if [ "${p_canmount}" = "off" ] ; then return elif [ "${p_canmount}" = "noauto" ] ; then - # Don't let a noauto marked mountpoint block an "auto" marked mountpoint - return + noauto="on" elif [ "${p_canmount}" = "on" ] ; then : # This is OK else - do_fail "invalid canmount" + do_fail "invalid canmount for ${dataset}" fi # Check for legacy and blank mountpoints. @@ -155,7 +234,7 @@ ExecStop=@sbindir@/zfs unload-key '${dataset}'" > "${dest_norm}/${keyloadunit} elif [ "${p_mountpoint}" = "none" ] ; then return elif [ "${p_mountpoint%"${p_mountpoint#?}"}" != "/" ] ; then - do_fail "invalid mountpoint $*" + do_fail "invalid mountpoint for ${dataset}" fi # Escape the mountpoint per systemd policy. @@ -233,15 +312,91 @@ ExecStop=@sbindir@/zfs unload-key '${dataset}'" > "${dest_norm}/${keyloadunit} "${dataset}" >/dev/kmsg fi - # If the mountpoint has already been created, give it precedence. + if [ -n "${p_systemd_wantedby}" ] && \ + [ "${p_systemd_wantedby}" != "-" ] ; then + noauto="on" + if [ "${p_systemd_wantedby}" = "none" ] ; then + wantedby="" + else + wantedby="${p_systemd_wantedby}" + before="${before} ${wantedby}" + fi + fi + + if [ -n "${p_systemd_requiredby}" ] && \ + [ "${p_systemd_requiredby}" != "-" ] ; then + noauto="on" + if [ "${p_systemd_requiredby}" = "none" ] ; then + requiredby="" + else + requiredby="${p_systemd_requiredby}" + before="${before} ${requiredby}" + fi + fi + + # For datasets with canmount=on, a dependency is created for + # local-fs.target by default. To avoid regressions, this dependency + # is reduced to "wants" rather than "requires" when nofail is not "off". + # **THIS MAY CHANGE** + # noauto=on disables this behavior completely. + if [ "${noauto}" != "on" ] ; then + if [ "${p_systemd_nofail}" = "off" ] ; then + requiredby="local-fs.target" + before="${before} local-fs.target" + else + wantedby="local-fs.target" + if [ "${p_systemd_nofail}" != "on" ] ; then + before="${before} local-fs.target" + fi + fi + fi + + # Handle existing files: + # 1. We never overwrite existing files, although we may delete + # files if we're sure they were created by us. (see 5.) + # 2. We handle files differently based on canmount. Units with canmount=on + # always have precedence over noauto. This is enforced by the sort pipe + # in the loop around this function. + # It is important to use $p_canmount and not $noauto here, since we + # sort by canmount while other properties also modify $noauto, e.g. + # org.openzfs.systemd:wanted-by. + # 3. If no unit file exists for a noauto dataset, we create one. + # Additionally, we use $noauto_files to track the unit file names + # (which are the systemd-escaped mountpoints) of all (exclusively) + # noauto datasets that had a file created. + # 4. If the file to be created is found in the tracking variable, + # we do NOT create it. + # 5. If a file exists for a noauto dataset, we check whether the file + # name is in the variable. If it is, we have multiple noauto datasets + # for the same mountpoint. In such cases, we remove the file for safety. + # To avoid further noauto datasets creating a file for this path again, + # we leave the file name in the tracking variable. if [ -e "${dest_norm}/${mountfile}" ] ; then - printf 'zfs-mount-generator: %s already exists\n' "${mountfile}" \ - >/dev/kmsg + if is_known "$mountfile" "$noauto_files" ; then + # if it's in $noauto_files, we must be noauto too. See 2. + printf 'zfs-mount-generator: removing duplicate noauto %s\n' \ + "${mountfile}" >/dev/kmsg + # See 5. + rm "${dest_norm}/${mountfile}" + else + # don't log for canmount=noauto + if [ "${p_canmount}" = "on" ] ; then + printf 'zfs-mount-generator: %s already exists. Skipping.\n' \ + "${mountfile}" >/dev/kmsg + fi + fi + # file exists; Skip current dataset. return + else + if is_known "${mountfile}" "${noauto_files}" ; then + # See 4. + return + elif [ "${p_canmount}" = "noauto" ] ; then + noauto_files="${mountfile} ${noauto_files}" + fi fi # Create the .mount unit file. - # By ordering before zfs-mount.service, we avoid race conditions. # # (Do not use `< "${dest_norm}/${keyloadunit} [Unit] SourcePath=${cachefile} Documentation=man:zfs-mount-generator(8) -Before=local-fs.target zfs-mount.service -After=${wants} + +Before=${before} +After=${after} Wants=${wants} +${requires} +${requiredmounts} [Mount] Where=${p_mountpoint} @@ -261,13 +419,20 @@ What=${dataset} Type=zfs Options=defaults${opts},zfsutil" > "${dest_norm}/${mountfile}" - # Finally, create the appropriate dependency - ln -s "../${mountfile}" "${req_dir}" + # Finally, create the appropriate dependencies + create_dependencies "${mountfile}" "wants" "$wantedby" + create_dependencies "${mountfile}" "requires" "$requiredby" + } -# Feed each line into process_line for cachefile in "${FSLIST}/"* ; do - while read -r fs ; do - process_line "${fs}" - done < "${cachefile}" + # Sort cachefile's lines by canmount, "on" before "noauto" + # and feed each line into process_line + sort -t "$(printf '\t')" -k 3 -r "${cachefile}" | \ + ( # subshell is necessary for `sort|while read` and $noauto_files + noauto_files="" + while read -r fs ; do + process_line "${fs}" + done + ) done diff --git a/man/man8/Makefile.am b/man/man8/Makefile.am index a4cd9f5675..f81a1f6720 100644 --- a/man/man8/Makefile.am +++ b/man/man8/Makefile.am @@ -89,6 +89,7 @@ EXTRA_DIST = \ $(nodist_man_MANS): %: %.in -$(SED) -e 's,@zfsexecdir\@,$(zfsexecdir),g' \ + -e 's,@systemdgeneratordir\@,$(systemdgeneratordir),g' \ -e 's,@runstatedir\@,$(runstatedir),g' \ -e 's,@sysconfdir\@,$(sysconfdir),g' \ $< >'$@' diff --git a/man/man8/zfs-mount-generator.8.in b/man/man8/zfs-mount-generator.8.in index a696eb4617..41a2999f0f 100644 --- a/man/man8/zfs-mount-generator.8.in +++ b/man/man8/zfs-mount-generator.8.in @@ -1,8 +1,33 @@ -.TH "ZFS\-MOUNT\-GENERATOR" "8" "ZFS" "zfs-mount-generator" "\"" +.\" +.\" Copyright 2018 Antonio Russo +.\" Copyright 2019 Kjeld Schouten-Lebbing +.\" Copyright 2020 InsanePrawn +.\" +.\" Permission is hereby granted, free of charge, to any person obtaining +.\" a copy of this software and associated documentation files (the +.\" "Software"), to deal in the Software without restriction, including +.\" without limitation the rights to use, copy, modify, merge, publish, +.\" distribute, sublicense, and/or sell copies of the Software, and to +.\" permit persons to whom the Software is furnished to do so, subject to +.\" the following conditions: +.\" +.\" The above copyright notice and this permission notice shall be +.\" included in all copies or substantial portions of the Software. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +.\" EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +.\" MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +.\" NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +.\" LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +.\" OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +.\" WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +.TH "ZFS\-MOUNT\-GENERATOR" "8" "2020-01-19" "ZFS" "zfs-mount-generator" "\"" + .SH "NAME" zfs\-mount\-generator \- generates systemd mount units for ZFS .SH SYNOPSIS -.B /lib/systemd/system-generators/zfs\-mount\-generator +.B @systemdgeneratordir@/zfs\-mount\-generator .sp .SH DESCRIPTION zfs\-mount\-generator implements the \fBGenerators Specification\fP @@ -11,22 +36,50 @@ of and is called during early boot to generate .BR systemd.mount (5) units for automatically mounted datasets. Mount ordering and dependencies -are created for all tracked pools (see below). If a dataset has -.BR canmount=on -and -.BR mountpoint -set, the -.BR auto -mount option will be set, and a dependency for -.BR local-fs.target -on the mount will be created. +are created for all tracked pools (see below). -Because zfs pools may not be available very early in the boot process, -information on ZFS mountpoints must be stored separately. The output -of the command +.SS ENCRYPTION KEYS +If the dataset is an encryption root, a service that loads the associated key (either from file or through a +.BR systemd\-ask\-password (1) +prompt) will be created. This service +. BR RequiresMountsFor +the path of the key (if file-based) and also copies the mount unit's +.BR After , +.BR Before +and +.BR Requires . +All mount units of encrypted datasets add the key\-load service for their encryption root to their +.BR Wants +and +.BR After . +The service will not be +.BR Want ed +or +.BR Require d +by +.BR local-fs.target +directly, and so will only be started manually or as a dependency of a started mount unit. + +.SS UNIT ORDERING AND DEPENDENCIES +mount unit's +.BR Before +\-> +key\-load service (if any) +\-> +mount unit +\-> +mount unit's +.BR After + +It is worth nothing that when a mount unit is activated, it activates all available mount units for parent paths to its mountpoint, i.e. activating the mount unit for /tmp/foo/1/2/3 automatically activates all available mount units for /tmp, /tmp/foo, /tmp/foo/1, and /tmp/foo/1/2. This is true for any combination of mount units from any sources, not just ZFS. + +.SS CACHE FILE +Because ZFS pools may not be available very early in the boot process, +information on ZFS mountpoints must be stored separately. The output of the command .PP .RS 4 -zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand,encroot,keylocation +zfs list -H -o name,mountpoint,canmount,atime,relatime,devices,exec,readonly,setuid,nbmand,encroot,keylocation,org.openzfs.systemd:requires,org.openzfs.systemd:requires-mounts-for,org.openzfs.systemd:before,org.openzfs.systemd:after,org.openzfs.systemd:wanted-by,org.openzfs.systemd:required-by,org.openzfs.systemd:nofail,org.openzfs.systemd:ignore + .RE .PP for datasets that should be mounted by systemd, should be kept @@ -45,6 +98,98 @@ history_event-zfs-list-cacher.sh . .RE .PP .sp +.SS PROPERTIES +The behavior of the generator script can be influenced by the following dataset properties: +.sp +.TP 4 +.BR canmount = on | off | noauto +If a dataset has +.BR mountpoint +set and +.BR canmount +is not +.BR off , +a mount unit will be generated. +Additionally, if +.BR canmount +is +.BR on , +.BR local-fs.target +will gain a dependency on the mount unit. + +This behavior is equal to the +.BR auto +and +.BR noauto +legacy mount options, see +.BR systemd.mount (5). + +Encryption roots always generate a key-load service, even for +.BR canmount=off . +.TP 4 +.BR org.openzfs.systemd:requires\-mounts\-for = \fIpath\fR... +Space\-separated list of mountpoints to require to be mounted for this mount unit +.TP 4 +.BR org.openzfs.systemd:before = \fIunit\fR... +The mount unit and associated key\-load service will be ordered before this space\-separated list of units. +.TP 4 +.BR org.openzfs.systemd:after = \fIunit\fR... +The mount unit and associated key\-load service will be ordered after this space\-separated list of units. +.TP 4 +.BR org.openzfs.systemd:wanted\-by = \fIunit\fR... +Space-separated list of units that will gain a +.BR Wants +dependency on this mount unit. +Setting this property implies +.BR noauto . +.TP 4 +.BR org.openzfs.systemd:required\-by = \fIunit\fR... +Space-separated list of units that will gain a +.BR Requires +dependency on this mount unit. +Setting this property implies +.BR noauto . +.TP 4 +.BR org.openzfs.systemd:nofail = unset | on | off +Toggles between a +.BR Wants +and +.BR Requires +type of dependency between the mount unit and +.BR local-fs.target , +if +.BR noauto +isn't set or implied. + +.BR on : +Mount will be +.BR WantedBy +local-fs.target + +.BR off : +Mount will be +.BR Before +and +.BR RequiredBy +local-fs.target + +.BR unset : +Mount will be +.BR Before +and +.BR WantedBy +local-fs.target +.TP 4 +.BR org.openzfs.systemd:ignore = on | off +If set to +.BR on , +do not generate a mount unit for this dataset. + +.RE +See also +.BR systemd.mount (5) + +.PP .SH EXAMPLE To begin, enable tracking for the pool: .PP @@ -63,7 +208,9 @@ systemctl enable zfs-zed.service systemctl restart zfs-zed.service .RE .PP -Force the running of the ZEDLET by setting canmount=on for at least one dataset in the pool: +Force the running of the ZEDLET by setting a monitored property, e.g. +.BR canmount , +for at least one dataset in the pool: .PP .RS 4 zfs set canmount=on @@ -71,6 +218,24 @@ zfs set canmount=on .RE .PP This forces an update to the stale cache file. + +To test the generator output, run +.PP +.RS 4 +@systemdgeneratordir@/zfs-mount-generator /tmp/zfs-mount-generator . . +.RE +.PP +This will generate units and dependencies in +.I /tmp/zfs-mount-generator +for you to inspect them. The second and third argument are ignored. + +If you're satisfied with the generated units, instruct systemd to re-run all generators: +.PP +.RS 4 +systemctl daemon-reload +.RE +.PP + .sp .SH SEE ALSO .BR zfs (5)