lots of fixes and tweaks,

generate partition tables in nix,
add open-system maintenance function
This commit is contained in:
Niklas Gollenstede
2022-06-04 20:54:09 +02:00
parent f56db19b5e
commit e46b671f8f
19 changed files with 233 additions and 112 deletions

View File

@ -20,7 +20,7 @@ function add-key-hostname {( set -eu # 1: usage
## Adds a key by copying it from a bootkey partition (see »add-bootkey-to-keydev«) to the keystore.
function add-key-usb-part {( set -eu # 1: usage
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
if [[ ! "$usage" =~ ^(luks/keystore/.*)$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' ; exit 1 ; fi
if [[ ! "$usage" =~ ^(luks/keystore-[^/]+/[1-8])$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' ; exit 1 ; fi
bootkeyPartlabel=bootkey-"@{config.networking.hostName!hashString.sha256:0:8}"
cat /dev/disk/by-partlabel/"$bootkeyPartlabel" | write-secret "$keystore"/"$usage".key
)}

View File

@ -33,7 +33,8 @@ function do-disk-setup { # 1: diskPaths
# * So alignment at the default »align=8MiB« actually seems a decent choice.
## Partitions all »config.wip.fs.disks.devices« to ensure that all (correctly) specified »config.wip.fs.disks.partitions« exist.
## Partitions the »diskPaths« instances of all »config.wip.fs.disks.devices« to ensure that all specified »config.wip.fs.disks.partitions« exist.
# Parses »diskPaths«, creates and loop-mounts images for non-/dev/ paths, and tries to abort if any partition already exists on the host.
function partition-disks { { # 1: diskPaths
beQuiet=/dev/null ; if [[ ${args[debug]:-} ]] ; then beQuiet=/dev/stdout ; fi
declare -g -A blockDevs=( ) # this ends up in the caller's scope
@ -45,11 +46,11 @@ function partition-disks { { # 1: diskPaths
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" ; exit 1 ; fi
if [[ ! ${blockDevs[$name]} =~ ^(/dev/.*)$ ]] ; then
if [[ ${blockDevs[$name]} != /dev/* ]] ; then
local outFile=${blockDevs[$name]} ; ( set -eu
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}"
install -o root -g root -m 640 -T /dev/null "$outFile" && fallocate -l "${disk[size]}" "$outFile"
) && blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d ${blockDevs[$name]}" EXIT # NOTE: this must not be inside a sub-shell!
install -o root -g root -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile"
) && blockDevs[$name]=$(losetup --show -f "$outFile") && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT # NOTE: this must not be inside a sub-shell!
else
if [[ ! "$(blockdev --getsize64 "${blockDevs[$name]}")" ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
@ -60,55 +61,17 @@ function partition-disks { { # 1: diskPaths
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'declare -A part='"$partDecl"
if [[ -e /dev/disk/by-partlabel/"${part[name]}" ]] && ! is-partition-on-disks /dev/disk/by-partlabel/"${part[name]}" "${blockDevs[@]}" ; then echo "Partition /dev/disk/by-partlabel/${part[name]} exists but does not reside on one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
if [[ -e /dev/disk/by-partlabel/"${part[name]}" ]] && ! is-partition-on-disks /dev/disk/by-partlabel/"${part[name]}" "${blockDevs[@]}" ; then echo "Partition /dev/disk/by-partlabel/${part[name]} already exists on this host and does not reside on one of the target disks ${blockDevs[@]}. Refusing to create another partition with the same partlabel!" ; exit 1 ; fi
done
for name in "@{!config.wip.fs.disks.devices[@]}" ; do (
for name in "@{!config.wip.fs.disks.devices[@]}" ; do
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}"
if [[ ${disk[serial]:-} ]] ; then
actual=$(udevadm info --query=property --name="${blockDevs[${disk[name]}]}" | @{native.gnugrep}/bin/grep -oP 'ID_SERIAL_SHORT=\K.*')
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device ${blockDevs[${disk[name]}]} does not match the serial declared for ${disk[name]}" ; exit 1 ; fi
actual=$(udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*')
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev does not match the serial declared for ${disk[name]}" ; exit 1 ; fi
fi
declare -a sgdisk=( --zap-all ) # delete existing part tables
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'declare -A part='"$partDecl"
if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi
if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then
devSize=$(blockdev --getsize64 "${blockDevs[${disk[name]}]}")
part[size]=$(( $devSize / 1024 * ${part[size]:0:(-1)} / 100 ))K
fi
sgdisk+=( -a "${part[alignment]:-${disk[alignment]}}" -n "${part[index]:-0}":"${part[position]}":+"${part[size]:-}" -t 0:"${part[type]}" -c 0:"${part[name]}" )
done
if [[ ${disk[mbrParts]:-} ]] ; then
sgdisk+=( --hybrid "${disk[mbrParts]}" ) # --hybrid: create MBR in addition to GPT; ${disk[mbrParts]}: make these GPT part 1 MBR parts 2[3[4]]
fi
( PATH=@{native.gptfdisk}/bin ; set -x ; sgdisk "${sgdisk[@]}" "${blockDevs[${disk[name]}]}" >$beQuiet ) # running all at once is much faster
if [[ ${disk[mbrParts]:-} ]] ; then
printf "
M # edit hybrid MBR
d;1 # delete parts 1 (GPT)
# move the selected »mbrParts« to slots 1[2[3]] instead of 2[3[4]] (by re-creating part1 in the last sector, then sorting)
n;p;1 # new ; primary ; part1
$(( $(blockSectorCount "${blockDevs[${disk[name]}]}") - 1)) # start (size 1sec)
x;f;r # expert mode ; fix order ; return
d;$(( (${#disk[mbrParts]} + 1) / 2 + 1 )) # delete ; part(last)
# create GPT part (spanning primary GPT area) as last part
n;p;4 # new ; primary ; part4
1;33 # start ; end
t;4;ee # type ; part4 ; GPT
${disk[extraFDiskCommands]}
p;w;q # print ; write ; quit
" | @{native.gnused}/bin/sed -E 's/^ *| *(#.*)?$//g' | @{native.gnused}/bin/sed -E 's/\n\n+| *; */\n/g' | tee >((echo -n '++ ' ; tr $'\n' '|' ; echo) 1>&2) | ( set -x ; fdisk "${blockDevs[${disk[name]}]}" &>$beQuiet )
fi
) ; done
partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}"
done
@{native.parted}/bin/partprobe "${blockDevs[@]}"
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
@ -116,6 +79,50 @@ function partition-disks { { # 1: diskPaths
wipefs --all "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >$beQuiet
)}
## Given a declared disk device's »name« and a path to an actual »blockDev« (or image) file, partitions the device as declared in the config.
function partition-disk {( set -eu # 1: name, 2: blockDev, 3?: devSize
name=$1 ; blockDev=$2
eval 'declare -A disk='"@{config.wip.fs.disks.devices[$name]}"
devSize=${3:-$(@{native.util-linux}/bin/blockdev --getsize64 "$blockDev")}
declare -a sgdisk=( --zap-all ) # delete existing part tables
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'declare -A part='"$partDecl"
if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi
if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then
part[size]=$(( $devSize / 1024 * ${part[size]:0:(-1)} / 100 ))K
fi
sgdisk+=( -a "${part[alignment]:-${disk[alignment]}}" -n "${part[index]:-0}":"${part[position]}":+"${part[size]:-}" -t 0:"${part[type]}" -c 0:"${part[name]}" )
done
if [[ ${disk[mbrParts]:-} ]] ; then
sgdisk+=( --hybrid "${disk[mbrParts]}" ) # --hybrid: create MBR in addition to GPT; ${disk[mbrParts]}: make these GPT part 1 MBR parts 2[3[4]]
fi
( PATH=@{native.gptfdisk}/bin ; set -x ; sgdisk "${sgdisk[@]}" "$blockDev" >$beQuiet ) # running all at once is much faster
if [[ ${disk[mbrParts]:-} ]] ; then
printf "
M # edit hybrid MBR
d;1 # delete parts 1 (GPT)
# move the selected »mbrParts« to slots 1[2[3]] instead of 2[3[4]] (by re-creating part1 in the last sector, then sorting)
n;p;1 # new ; primary ; part1
$(( ($devSize/512) - 1)) # start (size 1sec)
x;f;r # expert mode ; fix order ; return
d;$(( (${#disk[mbrParts]} + 1) / 2 + 1 )) # delete ; part(last)
# create GPT part (spanning primary GPT area) as last part
n;p;4 # new ; primary ; part4
1;33 # start ; end
t;4;ee # type ; part4 ; GPT
${disk[extraFDiskCommands]}
p;w;q # print ; write ; quit
" | @{native.gnused}/bin/sed -E 's/^ *| *(#.*)?$//g' | @{native.gnused}/bin/sed -E 's/\n\n+| *; */\n/g' | tee >((echo -n '++ ' ; tr $'\n' '|' ; echo) 1>&2) | ( PATH=@{native.util-linux}/bin ; set -x ; fdisk "$blockDev" &>$beQuiet )
fi
)}
## Checks whether a »partition« resides on one of the provided »blockDevs«.
function is-partition-on-disks {( set -eu # 1: partition, ...: blockDevs
partition=$1 ; shift ; declare -a blockDevs=( "$@" )
@ -152,31 +159,35 @@ function format-partitions {( set -eu
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt« (instead of »/«).
function mount-system {( set -eu # 1: mnt, 2?: fstabPath
# TODO: »config.system.build.fileSystems« is a dependency-sorted list. Could use that ...
# mount --all --fstab @{config.system.build.toplevel}/etc/fstab --target-prefix "$1" -o X-mount.mkdir # (»--target-prefix« is not supported on Ubuntu 20.04)
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"}
PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH
<$fstabPath @{native.gnugrep}/bin/grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target"
[[ $type == tmpfs ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
[[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
) || [[ $options == *,nofail,* ]] ; fi # (actually, nofail already makes mount fail silently)
done
# Since bind mounts may depend on other mounts not only for the target (which the sort takes care of) but also for the source, do all bind mounts last. This would break if there was a different bind mountpoint within a bind-mounted target.
<$fstabPath @{native.gnugrep}/bin/grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,}
if [[ $options =~ ,r?bind, ]] || [[ $type == overlay ]] ; then : ; else continue ; fi
if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target"
if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # work and upper dirs must be in target, lower dirs are probably store paths
workdir=$(<<<"$options" @{native.gnugrep}/bin/grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
upperdir=$(<<<"$options" @{native.gnugrep}/bin/grep -o -P ',upperdir=\K[^,]+' || true) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target.
workdir=$(<<<"$options" grep -o -P ',workdir=\K[^,]+' || true) ; if [[ $workdir ]] ; then mkdir -p "$workdir" ; fi
upperdir=$(<<<"$options" grep -o -P ',upperdir=\K[^,]+' || true) ; if [[ $upperdir ]] ; then mkdir -p "$upperdir" ; fi
lowerdir=$(<<<"$options" grep -o -P ',lowerdir=\K[^,]+' || true) # TODO: test the lowerdir stuff
options=${options//,lowerdir=$lowerdir,/,lowerdir=$mnt/${lowerdir//:/:$mnt\/},} ; source=overlay
else
if [[ $source == /nix/store/* ]] ; then options=,ro$options ; fi
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" ; fi
fi
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
@ -187,13 +198,10 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath
## Unmounts all file systems (that would be mounted during boot / by »mount-system«).
function unmount-system {( set -eu # 1: mnt, 2?: fstabPath
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"}
<$fstabPath @{native.gnugrep}/bin/grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
if [[ ! $target || $target == none ]] ; then continue ; fi
if mountpoint -q "$mnt"/"$target" ; then
umount "$mnt"/"$target"
fi
done
)}
## Given a block device path, returns the number of 512byte sectors it can hold.
function blockSectorCount { printf %s "$(( $(blockdev --getsize64 "$1") / 512 ))" ; }

View File

@ -33,9 +33,9 @@ function prepare-installer { # ...
if zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." ; exit 1 ; fi
done
if [[ ${SUDO_USER:-} ]] ; then function nix {( set +x ; declare -a args=("$@") ; PATH=/bin:/usr/bin su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
if [[ ${SUDO_USER:-} ]] ; then function nix {( set +x ; declare -a args=("$@") ; PATH=$hostPath su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
if [[ ${args[debug]:-} ]] ; then set +e ; set -E ; trap 'code= ; @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} || code=$? ; if [[ $code ]] ; then exit $code ; fi' ERR ; fi # On error, instead of exiting straight away, open a shell to allow diagnosing/fixing the issue. Only exit if that shell reports failure (e.g. CtrlC + CtrlD). Unfortunately, the exiting has to be repeated for level of each nested sub-shells.
if [[ ${args[debug]:-} ]] ; then set +e ; set -E ; trap 'code= ; cat 2>/dev/null || true ; @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} || code=$? ; if [[ $code ]] ; then exit $code ; fi' ERR ; fi # On error, instead of exiting straight away, open a shell to allow diagnosing/fixing the issue. Only exit if that shell reports failure (e.g. CtrlC + CtrlD). Unfortunately, the exiting has to be repeated for level of each nested sub-shells. The cat eats anything lined up on stdin, which would otherwise be run in the shell (TODO: but it blocks if there is nothing on stdin, requiring Ctrl+D to be pressed).
}
@ -44,11 +44,11 @@ function prepare-installer { # ...
# The restore commands are expected to pull in a backup of the systems secrets and state from somewhere, and need to acknowledge that something happened by running »restore-supported-callback«.
function init-or-restore-system {( set -eu # (void)
if [[ ! ${args[restore]:-} ]] ; then
run-hook-script 'System Initialization' @{config.wip.fs.disks.initSystemCommands!writeText.postPartitionCommands} # TODO: Do this later inside the chroot?
run-hook-script 'System Initialization' @{config.wip.fs.disks.initSystemCommands!writeText.initSystemCommands} # TODO: Do this later inside the chroot?
return # usually, this would be it ...
fi
requiresRestoration=$(mktemp) ; trap "rm -f '$requiresRestoration'" EXIT ; function restore-supported-callback {( rm -f "$requiresRestoration" )}
run-hook-script 'System Restoration' @{config.wip.fs.disks.restoreSystemCommands!writeText.postPartitionCommands}
run-hook-script 'System Restoration' @{config.wip.fs.disks.restoreSystemCommands!writeText.restoreSystemCommands}
if [[ -e $requiresRestoration ]] ; then echo 'The »restoreSystemCommands« did not call »restore-supported-callback« to mark backup restoration as supported for this system. Assuming incomplete configuration.' 1>&2 ; exit 1 ; fi
)}

View File

@ -7,7 +7,7 @@ function prompt-for-user-passwords { # (void)
userPasswords[$user]=@{config.users.users!catAttrSets.password[$user]}
done
for user in "@{!config.users.users!catAttrSets.passwordFile[@]}" ; do
if ! userPasswords[$user]=$(prompt-new-password "for the user account »$user«") ; then exit 1 ; fi
if ! userPasswords[$user]=$(prompt-new-password "for the user account »$user«") ; then return 1 ; fi
done
}
@ -35,15 +35,15 @@ function populate-keystore { { # (void)
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" == home-pw || "${methods[$usage]}" == copy ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != home-pw ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
done
for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != copy ]] ; then continue ; fi
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}" || return 1
done
)}
@ -70,6 +70,7 @@ function create-luks-layers {( set -eu # (void)
function open-luks-layers { # (void)
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}
for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
if [[ -e /dev/mapper/$luksName ]] ; then continue ; fi
rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]}
primaryKey="$keystore"/luks/"$luksName"/0.key

View File

@ -106,3 +106,62 @@ function add-bootkey-to-keydev {( set -eu # 1: blockDev, 2?: hostHash
@{native.parted}/bin/partprobe "$blockDev" ; @{native.systemd}/bin/udevadm settle -t 15 # wait for partitions to update
</dev/urandom tr -dc 0-9a-f | head -c 512 >/dev/disk/by-partlabel/"$bootkeyPartlabel"
)}
## Tries to open and mount the systems keystore from its LUKS partition. If successful, adds the traps to close it when the parent shell exits.
# See »open-system«'s implementation for some example calls to this function.
function mount-keystore-luks { # ...: cryptsetupOptions
# (For the traps to work, this can't run in a sub shell. The function therefore can't use »( set -eu ; ... )« internally and instead has to use »&&« after every command and in place of most »;«, and the function can't be called from a pipeline.)
keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8} &&
mkdir -p -- /run/$keystore &&
@{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore &&
mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore &&
prepend_trap "umount /run/$keystore ; @{native.cryptsetup}/bin/cryptsetup close $keystore ; rmdir /run/$keystore" EXIT
}
## Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
# For any steps taken, it also adds the reaps to undo them on exit from the calling shell, and it always adds the exit trap to do the unmounting itself.
# »diskImages« may be passed in the same format as to the installer. If so, any image files are ensured to be loop-mounted.
# Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
# $ source ${config_wip_fs_disks_initSystemCommands1writeText_initSystemCommands}
# $ source ${config_wip_fs_disks_restoreSystemCommands1writeText_restoreSystemCommands}
# $ install-system-to /tmp/nixos-install-${config_networking_hostName}
# $ nixos-enter --root /tmp/nixos-install-${config_networking_hostName}
function open-system { # 1?: diskImages
# (for the traps to work, this can't run in a sub shell, so also can't »set -eu«, so use »&&« after every command and in place of most »;«)
local diskImages=${1:-} # If »diskImages« were specified and they point at files that aren't loop-mounted yet, then loop-mount them now:
local images=$( losetup --list --all --raw --noheadings --output BACK-FILE )
local decl && for decl in ${diskImages//:/ } ; do
local image=${decl/*=/} && if [[ $image != /dev/* ]] && ! <<<$images grep -xF $image ; then
local blockDev=$( losetup --show -f "$image" ) && prepend_trap "losetup -d '$blockDev'" EXIT &&
@{native.parted}/bin/partprobe "$blockDev" &&
:;fi &&
:;done &&
( @{native.systemd}/bin/udevadm settle -t 15 || true ) && # sometimes partitions aren't quite made available yet
if [[ @{config.wip.fs.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
mount-keystore-luks --key-file=<(printf %s "@{config.networking.hostName}") ||
mount-keystore-luks --key-file=/dev/disk/by-partlabel/bootkey-@{config.networking.hostName!hashString.sha256:0:8} ||
mount-keystore-luks --key-file=<( read -s -p PIN: pin && echo ' touch!' >&2 && ykchalresp -2 "$pin" ) ||
# TODO: try static yubikey challenge
mount-keystore-luks
fi &&
local mnt=/tmp/nixos-install-@{config.networking.hostName} && if [[ ! -e $mnt ]] ; then mkdir -p "$mnt" && prepend_trap "rmdir '$mnt'" EXIT ; fi &&
open-luks-layers && # Load crypt layers and zfs pools:
if [[ $( LC_ALL=C type -t ensure-datasets ) == 'function' ]] ; then
local poolName && for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if ! zfs get -o value -H name "$poolName" &>/dev/null ; then
zpool import -f -N -R "$mnt" "$poolName" && prepend_trap "zpool export '$poolName'" EXIT &&
:;fi &&
: | zfs load-key -r "$poolName" || true &&
:;done &&
ensure-datasets "$mnt" &&
:;fi &&
prepend_trap "unmount-system '$mnt'" EXIT && mount-system "$mnt" &&
true # (success)
}

View File

@ -21,7 +21,7 @@ function generic-arg-parse { # ...
## Prepends a command to a trap. Especially useful fo define »finally« commands via »prepend_trap '<command>' EXIT«.
# NOTE: When calling this in a sub-shell whose parents already has traps installed, make sure to do »trap - trapName« first. On a new shell, this should be a no-op, but without it, the parent shell's traps will be added to the sub-shell as well (due to strange behavior of »trap -p« (in bash ~5.1.8)).
prepend_trap() { # 1: command, ...: trapNames
function prepend_trap { # 1: command, ...: trapNames
fatal() { printf "ERROR: $@\n" >&2 ; return 1 ; }
local cmd=$1 ; shift || fatal "${FUNCNAME} usage error"
local name ; for name in "$@" ; do
@ -30,7 +30,8 @@ prepend_trap() { # 1: command, ...: trapNames
p3() { printf '%s\n' "${3:-}" ; } ; eval "p3 $(trap -p "${name}")"
)" "${name}" || fatal "unable to add to trap ${name}"
done
} ; declare -f -t prepend_trap # required to modify DEBUG or RETURN traps
}
declare -f -t prepend_trap # required to modify DEBUG or RETURN traps
## Writes a »$name«d secret from stdin to »$targetDir«, ensuring proper file permissions.

View File

@ -31,8 +31,8 @@ function create-zpools { # 1: mnt
}
## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined).
# The pool(s) must exist, be imported with root prefix »$mnt«, and (if datasets are to be created or encryption roots to be inherited) the system's keystore must be open (see »mount-keystore-luks«).
# »keystatus« and »mounted« of existing datasets should remain unchained, newly crated datasets will not be mounted but have their keys loaded.
# The pool(s) must exist, be imported with root prefix »$mnt«, and (if datasets are to be created or encryption roots to be inherited) the system's keystore must be open (see »mount-keystore-luks«) or the keys be loaded.
# »keystatus« and »mounted« of existing datasets should remain unchanged, newly crated datasets will not be mounted but have their keys loaded.
function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then return ; fi
mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes)
@ -58,6 +58,7 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
if [[ ${props[keyformat]:-} == ephemeral ]] ; then
cryptRoot=${dataset[name]} ; unset props[keyformat] ; props[keylocation]=file:///dev/null
fi
if [[ $explicitKeylocation ]] ; then props[keylocation]=$explicitKeylocation ; fi
unset props[encryption] ; unset props[keyformat] # can't change these anyway
names=$(IFS=, ; echo "${!props[*]}") ; values=$(IFS=$'\n' ; echo "${props[*]}")
if [[ $values != "$(zfs get -o value -H "$names" "${dataset[name]}")" ]] ; then (
@ -66,27 +67,28 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
) ; fi
if [[ $cryptRoot && $(zfs get -o value -H encryptionroot "${dataset[name]}") != "$cryptRoot" ]] ; then ( # inherit key from parent (which the parent would also already have done if necessary)
parent=$(dirname "${dataset[name]}")
if [[ $(zfs get -o value -H keystatus "$parent") != available ]] ; then
zfs load-key -L file://"$cryptKey" "$parent" ; trap "zfs unload-key $parent || true" EXIT
if [[ $(zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "zfs unload-key $cryptRoot || true" EXIT
fi
if [[ $(zfs get -o value -H keystatus "${dataset[name]}") != available ]] ; then
zfs load-key -L file://"$cryptKey" "${dataset[name]}" # will unload with parent
zfs load-key -L file://"$cryptKey" "${dataset[name]}" # will unload with cryptRoot
fi
( set -x ; zfs change-key -i "${dataset[name]}" )
) ; fi
else # create dataset
else ( # create dataset
if [[ ${props[keyformat]:-} == ephemeral ]] ; then
props[encryption]=aes-256-gcm ; props[keyformat]=hex ; props[keylocation]=file:///dev/stdin ; explicitKeylocation=file:///dev/null
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
</dev/urandom tr -dc 0-9a-f | head -c 64 | ( set -x ; zfs create "${args[@]}" "${dataset[name]}" )
zfs unload-key "${dataset[name]}"
else (
# TODO: if [[ $cryptRoot && $(zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then ... load key while in this block ...
else
if [[ $cryptRoot && $cryptRoot != ${dataset[name]} && $(zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "zfs unload-key $cryptRoot || true" EXIT
fi
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
( set -x ; zfs create "${args[@]}" "${dataset[name]}" )
) ; fi
fi
if [[ ${props[canmount]} != off ]] ; then (
mount -t zfs -o zfsutil "${dataset[name]}" $tmpMnt ; trap "umount '${dataset[name]}'" EXIT
chmod 000 "$tmpMnt" ; ( chown "${dataset[uid]}:${dataset[gid]}" -- "$tmpMnt" ; chmod "${dataset[mode]}" -- "$tmpMnt" )
@ -95,7 +97,7 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
( set -x ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" )
fi
zfs snapshot -r "${dataset[name]}"@empty
fi
) ; fi
eval 'declare -A allows='"${dataset[permissions]}"
for who in "${!allows[@]}" ; do