#!/bin/sh # 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 # "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. set -e FSLIST="@sysconfdir@/zfs/zfs-list.cache" [ -d "${FSLIST}" ] || exit 0 do_fail() { printf 'zfs-mount-generator: %s\n' "$*" > /dev/kmsg exit 1 } # test if $1 is in space-separated list $2 is_known() { query="$1" IFS=' ' 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" IFS=' ' 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" elif [ $# -eq 3 ] ; then dest_norm="${1}" else do_fail "zero or three arguments required" fi pools=$(zpool list -H -o name || true) # All needed information about each ZFS is available from # zfs list -H -t filesystem -o # cached in $FSLIST, and each line is processed by the following function: # See the list below for the properties and their order process_line() { # zfs list -H -o name,... # fields are tab separated IFS="$(printf '\t')" # shellcheck disable=SC2086 set -- $1 dataset="${1}" pool="${dataset%%/*}" p_mountpoint="${2}" p_canmount="${3}" p_atime="${4}" p_relatime="${5}" p_devices="${6}" p_exec="${7}" p_readonly="${8}" p_setuid="${9}" 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="" bindsto="" wantedby="" requiredby="" noauto="off" # If the pool is already imported, zfs-import.target is not needed. This # avoids a dependency loop on root-on-ZFS systems: # systemd-random-seed.service After (via RequiresMountsFor) var-lib.mount # After zfs-import.target After zfs-import-{cache,scan}.service After # cryptsetup.service After systemd-random-seed.service. # # Pools are newline-separated and may contain spaces in their names. # There is no better portable way to set IFS to just a newline. Using # $(printf '\n') doesn't work because $(...) strips trailing newlines. IFS=" " for p in $pools ; do if [ "$p" = "$pool" ] ; then after="" wants="" break fi done 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 keymountdep="" if [ "${p_keyloc%%://*}" = "file" ] ; then if [ -n "${requiredmounts}" ] ; then keymountdep="${requiredmounts} '${p_keyloc#file://}'" else keymountdep="RequiresMountsFor='${p_keyloc#file://}'" fi keyloadscript="@sbindir@/zfs load-key \"${dataset}\"" elif [ "${p_keyloc}" = "prompt" ] ; then keyloadscript="\ count=0;\ while [ \$\$count -lt 3 ];do\ systemd-ask-password --id=\"zfs:${dataset}\"\ \"Enter passphrase for ${dataset}:\"|\ @sbindir@/zfs load-key \"${dataset}\" && exit 0;\ count=\$\$((count + 1));\ done;\ exit 1" else printf 'zfs-mount-generator: (%s) invalid keylocation\n' \ "${dataset}" >/dev/kmsg fi keyloadcmd="\ /bin/sh -c '\ set -eu;\ keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\ [ \"\$\$keystatus\" = \"unavailable\" ] || exit 0;\ ${keyloadscript}'" keyunloadcmd="\ /bin/sh -c '\ set -eu;\ keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";\ [ \"\$\$keystatus\" = \"available\" ] || exit 0;\ @sbindir@/zfs unload-key \"${dataset}\"'" # Generate the key-load .service unit # # Note: It is tempting to use a `< "${dest_norm}/${keyloadunit}" fi # Update the dependencies for the mount file to want the # key-loading unit. wants="${wants}" bindsto="BindsTo=${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 noauto="on" elif [ "${p_canmount}" = "on" ] ; then : # This is OK else do_fail "invalid canmount for ${dataset}" fi # Check for legacy and blank mountpoints. if [ "${p_mountpoint}" = "legacy" ] ; then return elif [ "${p_mountpoint}" = "none" ] ; then return elif [ "${p_mountpoint%"${p_mountpoint#?}"}" != "/" ] ; then do_fail "invalid mountpoint for ${dataset}" fi # Escape the mountpoint per systemd policy. mountfile="$(systemd-escape --path --suffix=mount "${p_mountpoint}")" # Parse options # see lib/libzfs/libzfs_mount.c:zfs_add_options opts="" # atime if [ "${p_atime}" = on ] ; then # relatime if [ "${p_relatime}" = on ] ; then opts="${opts},atime,relatime" elif [ "${p_relatime}" = off ] ; then opts="${opts},atime,strictatime" else printf 'zfs-mount-generator: (%s) invalid relatime\n' \ "${dataset}" >/dev/kmsg fi elif [ "${p_atime}" = off ] ; then opts="${opts},noatime" else printf 'zfs-mount-generator: (%s) invalid atime\n' \ "${dataset}" >/dev/kmsg fi # devices if [ "${p_devices}" = on ] ; then opts="${opts},dev" elif [ "${p_devices}" = off ] ; then opts="${opts},nodev" else printf 'zfs-mount-generator: (%s) invalid devices\n' \ "${dataset}" >/dev/kmsg fi # exec if [ "${p_exec}" = on ] ; then opts="${opts},exec" elif [ "${p_exec}" = off ] ; then opts="${opts},noexec" else printf 'zfs-mount-generator: (%s) invalid exec\n' \ "${dataset}" >/dev/kmsg fi # readonly if [ "${p_readonly}" = on ] ; then opts="${opts},ro" elif [ "${p_readonly}" = off ] ; then opts="${opts},rw" else printf 'zfs-mount-generator: (%s) invalid readonly\n' \ "${dataset}" >/dev/kmsg fi # setuid if [ "${p_setuid}" = on ] ; then opts="${opts},suid" elif [ "${p_setuid}" = off ] ; then opts="${opts},nosuid" else printf 'zfs-mount-generator: (%s) invalid setuid\n' \ "${dataset}" >/dev/kmsg fi # nbmand if [ "${p_nbmand}" = on ] ; then opts="${opts},mand" elif [ "${p_nbmand}" = off ] ; then opts="${opts},nomand" else printf 'zfs-mount-generator: (%s) invalid nbmand\n' \ "${dataset}" >/dev/kmsg fi 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 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. # # (Do not use `< "${dest_norm}/${mountfile}" # Finally, create the appropriate dependencies create_dependencies "${mountfile}" "wants" "$wantedby" create_dependencies "${mountfile}" "requires" "$requiredby" } for cachefile in "${FSLIST}/"* ; do # Disable glob expansion to protect against special characters when parsing. set -f # 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