Description: Support zsys systems
 Zsys is an enhanced and structured dataset layout for ZFS.
 .
 It enables advanced use cases by differentiating system,
 user data and persistent partitions to allow only partial
 permanent or temporary rollback without destroying intermediate
 snapshots.
Author: Jean-Baptiste Lallement <jean.baptiste@ubuntu.com>
        Didier Roche <didrocks@ubuntu.com>
        Heitor Alves de Siqueira <halves@ubuntu.com>
Last-Update: 2025-01-20
--- a/contrib/initramfs/scripts/zfs
+++ b/contrib/initramfs/scripts/zfs
@@ -75,6 +75,20 @@
 	"${ZFS}" get -H -ovalue "${_get_value}" "${_get_fs}" 2> /dev/null
 }
 
+# Get a ZFS filesystem property value with the source stripped from the value
+get_fs_value_without_source()
+{
+	value="$(get_fs_value $@)"
+	echo "${value%%:*}"
+}
+
+# Get a ZFS filesystem property source for a given key
+get_fs_source()
+{
+	value="$(get_fs_value $@)"
+	echo "${value#*:}"
+}
+
 # Find the 'bootfs' property on pool $1.
 # If the property does not contain '/', then ignore this
 # pool by exporting it again.
@@ -551,17 +565,18 @@
 	_clone_snap="${1}"
 	_clone_destfs="${2}"
 	_clone_mountpoint="${3}"
+	local additional_parameters="$4"
 
 	[ "${quiet}" != "y" ] && \
 		zfs_log_begin_msg "Cloning '${_clone_snap}' to '${_clone_destfs}'"
+ 
+	if [ -n "${_clone_mountpoint}" ]; then
+		additional_parameters="${additional_parameters} -o mountpoint=${_clone_mountpoint}"
+	fi
 
 	# Clone the snapshot into a dataset we can boot from
-	# + We don't want this filesystem to be automatically mounted, we
-	#   want control over this here and nowhere else.
-	# + We don't need any mountpoint set for the same reason.
-	# We use the 'org.zol:mountpoint' property to remember the mountpoint.
-	ZFS_CMD="${ZFS} clone -o canmount=noauto -o mountpoint=none"
-	ZFS_CMD="${ZFS_CMD} -o org.zol:mountpoint=${_clone_mountpoint}"
+	ZFS_CMD="${ZFS} clone"
+	ZFS_CMD="${ZFS_CMD} -o canmount=noauto ${additional_parameters}"
 	ZFS_CMD="${ZFS_CMD} ${_clone_snap} ${_clone_destfs}"
 	ZFS_STDERR="$(${ZFS_CMD} 2>&1)"
 	ZFS_ERROR="${?}"
@@ -683,6 +698,15 @@
 	_snapname="${_boot_snap##*@}"
 	ZFS_BOOTFS="${_rootfs}_${_snapname}"
 
+	# Detect if we are on a zsys system, which will generates an unique UUID
+	# and override ZFS_BOOTFS
+	use_zsys=$(get_fs_value_without_source "${_rootfs}" com.ubuntu.zsys:bootfs)
+	if [ "$use_zsys" = "yes" ]; then
+		zsys_uid=`uid`
+		ZFS_BOOTFS="${_rootfs%_*}_${zsys_uid}" # we strip old uid and add new one
+	fi
+
+	# Rollback won't have effect on zsys system
 	if ! get_cmdline 'rollback'
 	then
 		# If the destination dataset for the clone
@@ -715,10 +739,19 @@
 			#       rpool/ROOT/debian/boot@snap2 => rpool/ROOT/debian_snap2/boot
 			#       rpool/ROOT/debian/usr@snap2  => rpool/ROOT/debian_snap2/usr
 			#       rpool/ROOT/debian/var@snap2  => rpool/ROOT/debian_snap2/var
+			#
+			# For zsys, we have stable root dataset names with uid, so:
+			#       rpool/ROOT/debian_uid1@snap2		=> rpool/ROOT/debian_uid2
+			#       rpool/ROOT/debian_uid1/boot@snap2	=> rpool/ROOT/debian_uid2/boot
+
 			_subfs="${s##"${_rootfs}"}"
 			_subfs="${_subfs%%@"${_snapname}"}"
 
 			_destfs="${_rootfs}_${_snapname}" # base fs.
+			if [ "${use_zsys}" = "yes" ]; then
+				_destfs="${_rootfs%_*}_${zsys_uid}" # we strip old uid and add new one
+			fi
+
 			[ -n "${_subfs}" ] && _destfs="${_destfs}${_subfs}" # + sub fs.
 
 			# Get the mountpoint of the filesystem, to be used
@@ -736,9 +769,38 @@
 				fi
 			fi
 
+			# On non zsys:
+			# + We don't want this filesystem to be automatically mounted, we
+			#   want control over this here and nowhere else.
+			# + We don't need any mountpoint set for the same reason.
+			# + We use the 'org.zol:mountpoint' property to remember the mountpoint.
+			# On zsys:
+			# + We don't want this filesystem to be automatically mounted, when cloned
+			#   so, we set canmount=noauto. Zsys early boot will set the current datasets
+			#   to on, alongside other system datasets switch. This enables
+			#   zpool import -a -R /altroot to mount the whole system.
+			#   The initrd script is doing zpool import -N, so we are not impacted by setting
+			#   canmount=on on secondary boot.
+			# + We thus need the real mountpoint set for this reason (as we can't set it
+			#   once the system booted, even if the mountpoint didn't change)
+			# + We set additional parameters to zsys to mark datasets we want mount manually
+			#   at boot.
+			if [ "${use_zsys}" != "yes" ]; then
+				clone_additional_parameters="-o org.zol:mountpoint=${_mountpoint}"
+				_mountpoint=none
+			else
+				[ "$(get_fs_value_without_source "$s" com.ubuntu.zsys:bootfs)" != "yes" ] && continue
+				clone_additional_parameters="-o com.ubuntu.zsys:bootfs=yes"
+				# Only set mountpoint explicitely if it was locally set
+				# Keep the possibility to have mountpoint inherited for manual zfs snapshots without zsys involved, which
+				# will have an empty user propertie
+				local mountpoint_source="$(get_fs_source "$s" com.ubuntu.zsys:mountpoint)"
+				[ -n "${mountpoint_source}" -a "${mountpoint_source}" != "local" ] && _mountpoint=""
+			fi
+
 			# Clone the snapshot into its own
 			# filesystem
-			clone_snap "${s}" "${_destfs}" "${_mountpoint}" || \
+			clone_snap "${s}" "${_destfs}" "${_mountpoint}" "${clone_additional_parameters}" || \
 			    _retval="$((_retval + 1))"
 		fi
 	done
@@ -960,6 +1022,38 @@
 		shell
 	fi
 
+	# ------------
+	# Open and mount luks keystore for any pools using one
+	CRYPTROOT=/scripts/local-top/cryptroot
+	# We already processed original crypttab by calling local-top. Only add zvol related ones now.
+	if [ -x "${CRYPTROOT}" ]; then
+	        TABFILE=/cryptroot/crypttab
+	        :>"${TABFILE}"
+		# Wait for all keystore devices in /dev/zvol to appear with a 5s timeout
+		timeout=50
+		NUMKS=$(zfs list -H -o name | grep '/keystore$' | wc -l)
+		while [ ${NUMKS} -ne $(find /dev/zvol/ -name 'keystore' | wc -l) ]; do
+			if [ $timeout -le 0 ]; then
+				break
+			fi
+			sleep .1
+			timeout=$((timeout - 1))
+		done
+		# Locate, then mount the keystore volumes
+		for ks in $(find /dev/zvol/ -name 'keystore'); do
+			[ ! -e "${ks}" ] && continue
+			pool="$(basename $(dirname ${ks}))"
+			echo "keystore-${pool} ${ks} none luks,discard" >> "${TABFILE}"
+		done
+		${CRYPTROOT}
+		for dev in $(find /dev/mapper -name 'keystore-*'); do
+			# Translate filename to path
+			storepath="/run/$(echo $(basename ${dev})|sed -e 's,-,/,')"
+			mkdir -p "${storepath}"
+			mount "${dev}" "${storepath}"
+		done
+	fi
+
 	# In case the pool was specified as guid, resolve guid to name
 	pool="$("${ZPOOL}" get -H -o name,value name,guid | \
 	    awk -v pool="${ZFS_RPOOL}" '$2 == pool { print $1 }')"
@@ -980,6 +1074,8 @@
 		# Booting from a snapshot?
 		# Will overwrite the ZFS_BOOTFS variable like so:
 		#   rpool/ROOT/debian@snap2 => rpool/ROOT/debian_snap2
+		#   or
+		#   rpool/ROOT/debian@snap2 => rpool/ROOT/debian_<uid> if selected system is a zsys one
 		echo "${ZFS_BOOTFS}" | grep -q '@' && \
 		    setup_snapshot_booting "${ZFS_BOOTFS}"
 	fi
@@ -1023,10 +1119,19 @@
 	#    pool/root/var
 	#    [etc]
 	filesystems="$("${ZFS}" list -oname -tfilesystem -H -r "${ZFS_BOOTFS}")"
+
+	# If the root filesystem is a zsys one, we select the datasets to mount
+	# at boot.
+	# Some datasets under ROOT/ can be mounted on top of persistent datasets
+	# that are hosted elsewhere in the pool. Those are thus only mounted at
+	# early boot.
+	use_zsys=$(get_fs_value_without_source "${ZFS_BOOTFS}" com.ubuntu.zsys:bootfs)
+
 	OLD_IFS="${IFS}" ; IFS="
 "
 	for fs in ${filesystems}
 	do
+		[ "$use_zsys" = "yes" -a "$(get_fs_value_without_source "$fs" com.ubuntu.zsys:bootfs)" != "yes" ] && continue
 		IFS="${OLD_IFS}" mount_fs "${fs}"
 	done
 	IFS="${OLD_IFS}"
@@ -1038,6 +1143,7 @@
 	# later in the boot process.
 	for fs in ${ZFS_INITRD_ADDITIONAL_DATASETS}
 	do
+		[ "$use_zsys" = "yes" -a "$(get_fs_value_without_source "$fs" com.ubuntu.zsys:bootfs)" != "yes" ] && continue
 		mount_fs "${fs}"
 	done
 
@@ -1085,3 +1191,8 @@
 		fi
 	fi
 }
+
+uid()
+{
+	grep -a -m10 -E "\*" /dev/urandom 2>/dev/null | tr -dc 'a-z0-9' | cut -c-6
+}
--- a/etc/systemd/system-generators/zfs-mount-generator.in
+++ b/etc/systemd/system-generators/zfs-mount-generator.in
@@ -30,6 +30,8 @@
 [ -d "${FSLIST}" ] || exit 0
 [ "$(echo "${FSLIST}"/*)" = "${FSLIST}/*" ] && exit 0
 
+OLD_IFS=$IFS
+
 do_fail() {
   printf 'zfs-mount-generator: %s\n' "$*" > /dev/kmsg
   exit 1
@@ -138,6 +140,9 @@
     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}"
@@ -163,6 +168,62 @@
       [ "${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
@@ -218,6 +279,7 @@
 After=${after}
 ${requires}
 ${keymountdep}
+${automount_loadkey_extra_args}
 
 [Service]
 Type=oneshot
@@ -270,9 +332,6 @@
     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=""
@@ -459,6 +518,87 @@
 
 }
 
+ZPOOL_CACHE="@sysconfdir@/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 @sbindir@/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 don’t 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
+    @sbindir@/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."
+    @sbindir@/zpool import -aN
+  fi
+
+  @sbindir@/zsysd boot-prepare >"${errfile}"
+
+  # If FSLIST is empty, populate with all imported pools
+  if [ -z "$(ls -A ${FSLIST})" ]; then
+    @sbindir@/zpool list -H -o name | xargs -I{} touch ${FSLIST}/{}
+  fi
+
+  # Refresh zfs list cache
+  for cachefile in "${FSLIST}/"* ; do
+    pool=`basename ${cachefile}`
+    @sbindir@/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
