ubuntu-22.04.3-desktop-amd64/casper/filesystem/usr/lib/systemd/system-generators/zfs-mount-generator

615 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/bin/sh
# zfs-mount-generator - generates systemd mount units for zfs
# Copyright (c) 2017 Antonio Russo <antonio.e.russo@gmail.com>
# Copyright (c) 2020 InsanePrawn <insane.prawny@gmail.com>
#
# 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="/etc/zfs/zfs-list.cache"
[ -d "${FSLIST}" ] || exit 0
[ "$(echo "${FSLIST}"/*)" = "${FSLIST}/*" ] && exit 0
OLD_IFS=$IFS
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 <properties>
# 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
# Escape the mountpoint per systemd policy.
mountfile="$(systemd-escape --path --suffix=mount "${p_mountpoint}")"
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
# Automount and unmount ZSys USERDATA datasets with keystore
zsys_automount=0
automount_loadkey_extra_args=""
automount_pool=""
automount_user=""
if echo "${dataset}" | grep -q '/USERDATA/'; then
automount_pool=${dataset%%/*}
automount_user=${dataset##*/}
# Only operate on user dataset mountpoint itself and not its children
if ! echo "${automount_user}" | grep -q '/'; then
automount_user=${automount_user%%_*}
# Ensure we have a keystore
if [ -f "/run/keystore/${automount_pool}/${automount_user}.enc" -a -x /usr/sbin/user_keystore ]; then
zsys_automount=1
fi
fi
fi
# Create automount unit and keystore tracker
if [ ${zsys_automount} -eq 1 ]; then
automountunit="$(systemd-escape --path --suffix=automount "${p_mountpoint}")"
echo \
"# Automatically generated by zfs-mount-generator
[Unit]
Description=Automount ZFS user home for ${dataset} on demand
[Automount]
Where=${p_mountpoint}
TimeoutIdleSec=10
[Install]
WantedBy=local-fs.target
" > "${dest_norm}/${automountunit}"
create_dependencies "${automountunit}" "wants" "local-fs.target"
keystoreunit="zfs-keystore-$(systemd-escape "${p_encroot}").service"
automount_loadkey_extra_args="BindsTo=${mountfile}
BindsTo=${keystoreunit}
After=${keystoreunit}"
echo \
"# Automatically generated by zfs-mount-generator
[Unit]
Description=Make available ZFS encryption key for ${dataset} from keystore
ConditionPathExists=/run/keystore/${automount_pool}/${automount_user}.enc
BindsTo=${keyloadunit}
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStop=/usr/sbin/user_keystore lock ${automount_pool} ${automount_user}
" > "${dest_norm}/${keystoreunit}"
fi
keymountdep=""
if [ "${p_keyloc%%://*}" = "file" ] ; then
if [ -n "${requiredmounts}" ] ; then
keymountdep="${requiredmounts} '${p_keyloc#file://}'"
else
keymountdep="RequiresMountsFor='${p_keyloc#file://}'"
fi
keyloadscript="/sbin/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}:\"|\
/sbin/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=\"\$\$(/sbin/zfs get -H -o value keystatus \"${dataset}\")\";\
[ \"\$\$keystatus\" = \"unavailable\" ] || exit 0;\
${keyloadscript}'"
keyunloadcmd="\
/bin/sh -c '\
set -eu;\
keystatus=\"\$\$(/sbin/zfs get -H -o value keystatus \"${dataset}\")\";\
[ \"\$\$keystatus\" = \"available\" ] || exit 0;\
/sbin/zfs unload-key \"${dataset}\"'"
# Generate the key-load .service unit
#
# Note: It is tempting to use a `<<EOF` style here-document for this, but
# bash requires a writable /tmp or $TMPDIR for that. This is not always
# available early during boot.
#
echo \
"# Automatically generated by zfs-mount-generator
[Unit]
Description=Load ZFS key for ${dataset}
SourcePath=${cachefile}
Documentation=man:zfs-mount-generator(8)
DefaultDependencies=no
Wants=${wants}
After=${after}
${requires}
${keymountdep}
${automount_loadkey_extra_args}
[Service]
Type=oneshot
RemainAfterExit=yes
# This avoids a dependency loop involving systemd-journald.socket if this
# dataset is a parent of the root filesystem.
StandardOutput=null
StandardError=null
ExecStart=${keyloadcmd}
ExecStop=${keyunloadcmd}" > "${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
# 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 `<<EOF`-style here-documents for this, see warning above)
#
echo \
"# Automatically generated by zfs-mount-generator
[Unit]
SourcePath=${cachefile}
Documentation=man:zfs-mount-generator(8)
Before=${before}
After=${after}
Wants=${wants}
${bindsto}
${requires}
${requiredmounts}
[Mount]
Where=${p_mountpoint}
What=${dataset}
Type=zfs
Options=defaults${opts},zfsutil" > "${dest_norm}/${mountfile}"
# Finally, create the appropriate dependencies
create_dependencies "${mountfile}" "wants" "$wantedby"
create_dependencies "${mountfile}" "requires" "$requiredby"
}
ZPOOL_CACHE="/etc/zfs/zpool.cache"
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"
zsys_revert_failed=0
errfile="/tmp/zsys-revert-out.log"
drop_emergency_on_failure() {
if [ ${zsys_revert_failed} -eq 0 ]; then
return
fi
# Drop to emergency target in case of failure after cleanup fstab mountpoints.
# This avoids booting and having a mix of old and new datasets, and creating directory in the wrong
# datasets, like /boot/grub in / which will prevent zfs to mount /boot dataset later on.
rm -f "${dest_norm}"/*.mount
ln -s /lib/systemd/system/emergency.target "${dest_norm}"/default.target
printf 'ERROR: zfs-mount-generator failed and you requested a revert:\n' > /dev/kmsg
cat "${errfile}" > /dev/kmsg
printf 'You can reboot on current master dataset to fix the issue\n' > /dev/kmsg
}
# Handle revert so that zsys prepares all datasets as expected.
initzsys() {
if [ ! -x /sbin/zsysd ]; then
return
fi
# Non ZFS system
if ! grep -q "root=ZFS=" /proc/cmdline; then
return
fi
# If we boot on the same dataset than last time, assume we dont need to do anything as the cache file will only
# import desired pools.
bootds="$(sed -e 's/.*root=ZFS=\([^ ]\+\).*/\1/' /proc/cmdline)"
if grep -Eq "${bootds}\s+/\s+on" "${FSLIST}/"*; then
return
fi
# If we get here: we are reverting. Let zsys handle it
trap drop_emergency_on_failure EXIT INT QUIT ABRT PIPE TERM
exec 3>&1 1>"${errfile}"
exec 4>&2 2>&1
zsys_revert_failed=1
# Import and list previously imported pools for zsys
if [ -f "${ZPOOL_CACHE}" ]; then
/sbin/zpool import -c "${ZPOOL_CACHE}" -aN
# As a best effort, import all available pools, hoping there is no conflict.
else
echo "We had to search for all available pools because ${ZPOOL_CACHE} doesn't exist. To avoid this, create a zpool cache file."
/sbin/zpool import -aN
fi
/sbin/zsysd boot-prepare >"${errfile}"
# If FSLIST is empty, populate with all imported pools
if [ -z "$(ls -A ${FSLIST})" ]; then
/sbin/zpool list -H -o name | xargs -I{} touch ${FSLIST}/{}
fi
# Refresh zfs list cache
for cachefile in "${FSLIST}/"* ; do
pool=`basename ${cachefile}`
/sbin/zfs list -H -t filesystem -o "${PROPS}" -r "${pool}" >"${cachefile}"
done
exec 1>&3 3>&-
exec 2>&4 4>&-
zsys_revert_failed=0
rm "${errfile}"
}
initzsys
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