#!/bin/sh

# zfs-mount-generator - generates systemd mount units for zfs
# Copyright (c) 2017 Antonio Russo <antonio.e.russo@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="@sysconfdir@/zfs/zfs-list.cache"

[ -d "${FSLIST}" ] || exit 0

do_fail() {
  printf 'zfs-mount-generator: %s\n' "$*" > /dev/kmsg
  exit 1
}

# 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

# 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 <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')"
  # protect against special characters in, e.g., mountpoints
  set -f
  set -- $1
  dataset="${1}"
  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}"

  # Minimal pre-requisites to mount a ZFS dataset
  wants="zfs-import.target"

  # Handle encryption
  if [ -n "${p_encroot}" ] &&
      [ "${p_encroot}" != "-" ] ; then
    keyloadunit="zfs-load-key-$(systemd-escape "${p_encroot}").service"
    if [ "${p_encroot}" = "${dataset}" ] ; then
        pathdep=""
      if [ "${p_keyloc%%://*}" = "file" ] ; then
        pathdep="RequiresMountsFor='${p_keyloc#file://}'"
        keyloadcmd="@sbindir@/zfs load-key '${dataset}'"
      elif [ "${p_keyloc}" = "prompt" ] ; then
        keyloadcmd="/bin/sh -c 'set -eu;"\
"keystatus=\"\$\$(@sbindir@/zfs get -H -o value keystatus \"${dataset}\")\";"\
"[ \"\$\$keystatus\" = \"unavailable\" ] || exit 0;"\
"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

      # 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=${wants}
${pathdep}

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=${keyloadcmd}
ExecStop=@sbindir@/zfs unload-key '${dataset}'"   > "${dest_norm}/${keyloadunit}"
    fi
    # Update the dependencies for the mount file to require the
    # key-loading unit.
    wants="${wants} ${keyloadunit}"
  fi

  # Prepare the .mount unit

  # 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
  elif [ "${p_canmount}" = "on" ] ; then
    : # This is OK
  else
    do_fail "invalid canmount"
  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 $*"
  fi

  # Escape the mountpoint per systemd policy.
  mountfile="$(systemd-escape "${p_mountpoint#?}").mount"

  # 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 the mountpoint has already been created, give it precedence.
  if [ -e "${dest_norm}/${mountfile}" ] ; then
    printf 'zfs-mount-generator: %s already exists\n' "${mountfile}" \
      >/dev/kmsg
    return
  fi

  # Create the .mount unit file.
  # By ordering before zfs-mount.service, we avoid race conditions.
  #
  # (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=local-fs.target zfs-mount.service
After=${wants}
Wants=${wants}

[Mount]
Where=${p_mountpoint}
What=${dataset}
Type=zfs
Options=defaults${opts},zfsutil" > "${dest_norm}/${mountfile}"

  # Finally, create the appropriate dependency
  ln -s "../${mountfile}" "${req_dir}"
}

# Feed each line into process_line
for cachefile in "${FSLIST}/"* ; do
  while read -r fs ; do
    process_line "${fs}"
  done < "${cachefile}"
done
