mirror of
https://github.com/NiklasGollenstede/nixos-installer.git
synced 2025-08-08 23:21:28 +02:00
improve installation, add support for:
ZFS, encryption (keys, keystore, LUKS), bootFS, ephemeral root (tmpfs, ZFS, F2FS, ...), testing in qemu, options & debugging, ... and many small things
This commit is contained in:
@ -3,15 +3,17 @@
|
||||
|
||||
This is a library of bash functions, mostly for NixOS system installation.
|
||||
|
||||
The (paths to these) scripts are meant to me passed in the `scripts` argument to [`mkSystemsFlake`](../flakes.nix#mkSystemsFlake), which makes their functions available in the per-host `devShells`/`apps`.
|
||||
The (paths to these) scripts are meant to me passed in the `scripts` argument to [`mkSystemsFlake`](../flakes.nix#mkSystemsFlake) (see [`flake.nix`](../../flake.nix) for an example), which makes their functions available in the per-host `devShells`/`apps`.
|
||||
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
|
||||
Any script passed later in `scripts` can overwrite the functions of these (earlier) default scripts.
|
||||
|
||||
With the functions from here, adding a simple three-liner can be enough to do a completely automated NixOS installation:
|
||||
With the functions from here, [a simple four-liner](../install.sh) is enough to do a completely automated NixOS installation:
|
||||
```bash
|
||||
function install-system {( set -eu # 1: blockDev
|
||||
function install-system {( set -eu # 1: diskPaths
|
||||
prepare-installer "$@"
|
||||
do-disk-setup "$1"
|
||||
install-system-to $mnt prompt=true
|
||||
do-disk-setup "${argv[0]}"
|
||||
init-or-restore-system
|
||||
install-system-to $mnt
|
||||
)}
|
||||
```
|
||||
|
||||
|
110
lib/setup-scripts/add-key.sh
Normal file
110
lib/setup-scripts/add-key.sh
Normal file
@ -0,0 +1,110 @@
|
||||
|
||||
##
|
||||
# Key Generation Methods
|
||||
# See »../../modules/fs/keystore.nix.md« for more documentation.
|
||||
##
|
||||
|
||||
## Puts an empty key in the keystore, causing that ZFS dataset to be unencrypted, even if it's parent is encrypted.
|
||||
function add-key-unencrypted {( set -eu # 1: usage
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
|
||||
: | write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Adds a key by copying the hostname to the keystore.
|
||||
function add-key-hostname {( set -eu # 1: usage
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
|
||||
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf '»trivial« key mode is only available for the keystore itself.\n' ; exit 1 ; fi
|
||||
printf %s "@{config.networking.hostName}" | write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## 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
|
||||
bootkeyPartlabel=bootkey-"@{config.networking.hostName!hashString.sha256:0:8}"
|
||||
cat /dev/disk/by-partlabel/"$bootkeyPartlabel" | write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Adds a key by copying a different key from the keystore to the keystore.
|
||||
function add-key-copy {( set -eu # 1: usage, 2: source
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; source=$2
|
||||
cat "$keystore"/"$source".key | write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Adds a key by writing a constant value to the keystore.
|
||||
function add-key-constant {( set -eu # 1: usage, 2: value
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; value=$2
|
||||
printf %s "$value" | write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Adds a key by prompting for a password and saving it to the keystore.
|
||||
function add-key-password {( set -eu # 1: usage
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
|
||||
(prompt-new-password "as key for @{config.networking.hostName}/$usage" || exit 1) \
|
||||
| write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Generates a key by prompting for a password, combining it with »$keystore/home/$user.key«, and saving it to the keystore.
|
||||
function add-key-home-pw {( set -eu # 1: usage, 2: user
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; user=$2
|
||||
if [[ ${!userPasswords[@]} && ${userPasswords[$user]:-} ]] ; then
|
||||
password=${userPasswords[$user]}
|
||||
else
|
||||
password=$(prompt-new-password "that will be used as component of the key for @{config.networking.hostName}/$usage")
|
||||
fi
|
||||
( cat "$keystore"/home/"$user".key && cat <<<"$password" ) | sha256sum | head -c 64 \
|
||||
| write-secret "$keystore"/"$usage".key
|
||||
)}
|
||||
|
||||
## Generates a reproducible secret for a certain »$use«case by prompting for a pin/password and then challenging slot »$slot« of YubiKey »$serial«, and saves it to the »$keystore«.
|
||||
function add-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
|
||||
usage=$1 ; serialAndSlot=$2
|
||||
password=$(prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for @{config.networking.hostName}/$usage")
|
||||
add-key-yubikey-challenge "$usage" "$serialAndSlot:$password" true "pin for ${usage}"
|
||||
)}
|
||||
|
||||
## Generates a reproducible secret for a certain »$use«case on a »$host« by challenging slot »$slot« of YubiKey »$serial«, and saves it to the »$keystore«.
|
||||
function add-key-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndSalt(as »serial:slot:salt«)
|
||||
usage=$1 ; IFS=':' read -ra serialAndSlotAndSalt <<< "$2"
|
||||
usage_="$usage" ; if [[ "$usage" =~ ^(luks/.*/[0-8])$ ]] ; then usage_="${usage:0:(-2)}" ; fi # produce the same secret, regardless of the target luks slot
|
||||
challenge="@{config.networking.hostName}:$usage_${serialAndSlotAndSalt[2]:+:${serialAndSlotAndSalt[2]:-}}"
|
||||
add-key-yubikey-challenge "$usage" "${serialAndSlotAndSalt[0]}:${serialAndSlotAndSalt[1]}:$challenge"
|
||||
)}
|
||||
|
||||
## Generates a reproducible secret for a certain »$use«case by challenging slot »$slot« of YubiKey »$serial« with the fixed »$challenge«, and saves it to the »$keystore«.
|
||||
# If »$sshArgs« is set as (env) var, generate the secret locally, then use »ssh $sshArgs« to write the secret on the other end.
|
||||
# E.g.: # sshArgs='installerIP' add-key-yubikey /run/keystore/ zfs/rpool/remote 1234567:2:customChallenge
|
||||
function add-key-yubikey-challenge {( set -eu # 1: usage, 2: serialAndSlotAndChallenge(as »$serial:$slot:$challenge«), 3?: onlyOnce, 4?: message
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1 ; args=$2 ; message=${4:-}
|
||||
serial=$(<<<"$args" cut -d: -f1)
|
||||
slot=$(<<<"$args" cut -d: -f2)
|
||||
challenge=${args/$serial:$slot:/}
|
||||
|
||||
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'Please insert / change to YubiKey with serial %s!\n' "$serial" ; fi
|
||||
if [[ ! "${3:-}" ]] ; then
|
||||
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' twice with '"${message:-challenge »"$challenge":1/2«}"'. Enter to continue, or Ctrl+C to abort:'
|
||||
else
|
||||
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' once with '"${message:-challenge »"$challenge"«}"'. Enter to continue, or Ctrl+C to abort:'
|
||||
fi
|
||||
if [[ "$serial" != "$(@{native.yubikey-personalization}/bin/ykinfo -sq)" ]] ; then printf 'YubiKey with serial %s not present, aborting.\n' "$serial" ; exit 1 ; fi
|
||||
|
||||
if [[ ! "${3:-}" ]] ; then
|
||||
secret="$(@{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge":1)""$(@{native.yubikey-personalization}/bin/ykchalresp -2 "$challenge":2)"
|
||||
if [[ ${#secret} != 80 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" ; exit 1 ; fi
|
||||
else
|
||||
secret="$(@{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge")"
|
||||
if [[ ${#secret} != 40 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" ; exit 1 ; fi
|
||||
fi
|
||||
if [[ ! "${sshArgs:-}" ]] ; then
|
||||
printf %s "$secret" | ( head -c 64 | write-secret "$keystore"/"$usage".key )
|
||||
else
|
||||
read -p 'Uploading secret with »ssh '"$sshArgs"'«. Enter to continue, or Ctrl+C to abort:'
|
||||
printf %s "$secret" | ( head -c 64 | ssh $sshArgs /etc/nixos/utils/functions.sh write-secret "$keystore"/"$usage".key )
|
||||
fi
|
||||
)}
|
||||
|
||||
## Generates a random secret key and saves it to the keystore.
|
||||
function add-key-random {( set -eu # 1: usage
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8} ; usage=$1
|
||||
</dev/urandom tr -dc 0-9a-f | head -c 64 | write-secret "$keystore"/"$usage".key
|
||||
)}
|
@ -6,34 +6,49 @@
|
||||
## Prepares the disks of the target system for the copying of files.
|
||||
function do-disk-setup { # 1: diskPaths
|
||||
|
||||
mnt=/tmp/nixos-install-@{config.networking.hostName} ; mkdir -p "$mnt" ; prepend_trap "rmdir $mnt" EXIT # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0«
|
||||
prompt-for-user-passwords &&
|
||||
populate-keystore &&
|
||||
|
||||
partition-disks "$1"
|
||||
# ... block layers would go here ...
|
||||
source @{config.wip.installer.postPartitionCommands!writeText.postPartitionCommands}
|
||||
format-partitions
|
||||
source @{config.wip.installer.postFormatCommands!writeText.postFormatCommands}
|
||||
prepend_trap "unmount-system $mnt" EXIT ; mount-system $mnt
|
||||
source @{config.wip.installer.postMountCommands!writeText.postMountCommands}
|
||||
mnt=/tmp/nixos-install-@{config.networking.hostName} && mkdir -p "$mnt" && prepend_trap "rmdir $mnt" EXIT && # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0«
|
||||
|
||||
partition-disks "$1" &&
|
||||
create-luks-layers && open-luks-layers && # other block layers would go here too (but figuring out their dependencies would be difficult)
|
||||
run-hook-script 'Post Partitioning' @{config.wip.fs.disks.postPartitionCommands!writeText.postPartitionCommands} &&
|
||||
|
||||
format-partitions &&
|
||||
{ [[ $(LC_ALL=C type -t create-zpools) != function ]] || create-zpools $mnt ; } &&
|
||||
run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} &&
|
||||
|
||||
prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt &&
|
||||
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} &&
|
||||
:
|
||||
}
|
||||
|
||||
## Partitions all »config.installer.disks« to ensure that all (correctly) specified »{config.installer.partitions« exist.
|
||||
# Notes segmentation and alignment:
|
||||
# * Both fdisk and gdisk report start and end in 0-indexed sectors from the start of the block device.
|
||||
# * (fdisk and gdisk have slightly different interfaces, but seem to otherwise be mostly equivalent, (fdisk used to not understand GPT).)
|
||||
# * The MBR sits only in the first sector, a GPT additionally requires next 33 (34 total) and the (absolute) last 33 sectors. At least fdisk won't put partitions in the first 2048 sectors on MBRs.
|
||||
# * Crappy flash storage (esp. micro SD cards) requires alignment to pretty big sectors for optimal (esp. write) performance. For reasons of inconvenience, vendors don't document the size of those. Not too extensive test with 4 (in 2022 considered to be among the more decent) micro SD cards indicates the magic number to be somewhere between 1 and 4MiB, but it may very well be higher for others.
|
||||
# * (source: https://lwn.net/Articles/428584/)
|
||||
# * 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.
|
||||
function partition-disks { { # 1: diskPaths
|
||||
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
|
||||
beQuiet=/dev/null ; if [[ ${args[debug]:-} ]] ; then beQuiet=/dev/stdout ; fi
|
||||
declare -g -A blockDevs=( ) # this ends up in the caller's scope
|
||||
local path ; for path in ${1/:/ } ; do
|
||||
name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
|
||||
local path ; for path in ${1//:/ } ; do
|
||||
local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
|
||||
if [[ ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name specified more than once. Duplicate definition: $path" ; exit 1 ; fi
|
||||
blockDevs[$name]=$path
|
||||
done
|
||||
|
||||
local name ; for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do
|
||||
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
|
||||
local outFile=${blockDevs[$name]} ; ( set -eu
|
||||
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _size
|
||||
install -o root -g root -m 640 -T /dev/null "$outFile" && fallocate -l "$_size" "$outFile"
|
||||
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!
|
||||
else
|
||||
if [[ ! "$(blockdev --getsize64 "${blockDevs[$name]}")" ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" ; exit 1 ; fi
|
||||
@ -41,108 +56,138 @@ function partition-disks { { # 1: diskPaths
|
||||
fi
|
||||
done
|
||||
|
||||
} ; ( set -eu
|
||||
} && ( set -eu
|
||||
|
||||
for name in "@{!config.wip.installer.disks!attrsAsBashEvalSets[@]}" ; do (
|
||||
eval "@{config.wip.installer.disks!attrsAsBashEvalSets[$name]}" # _name ; _size ; _serial ; _alignment ; _mbrParts ; _extraFDiskCommands
|
||||
if [[ $_serial ]] ; then
|
||||
actual=$(udevadm info --query=property --name="${blockDevs[$name]}" | grep -oP 'ID_SERIAL_SHORT=\K.*')
|
||||
if [[ $_serial != "$actual" ]] ; then echo "Block device ${blockDevs[$name]} does not match the serial declared for $name" ; exit 1 ; fi
|
||||
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
|
||||
done
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
sgdisk=( --zap-all ) # delete existing part tables
|
||||
for partDecl in "@{config.wip.installer.partitionList!listAsBashEvalSets[@]}" ; do
|
||||
eval "$partDecl" # _name ; _disk ; _type ; _size ; _index
|
||||
if [[ $_disk != "$name" ]] ; then exit ; fi # i.e. continue
|
||||
if [[ $_position =~ ^[0-9]+$ ]] ; then alignment=1 ; else alignment=$_alignment ; fi # if position is an absolute number, start precisely there
|
||||
sgdisk+=( -a "$alignment" -n "${_index:-0}":"$_position":+"$_size" -t 0:"$_type" -c 0:"$_name" )
|
||||
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 [[ $_mbrParts ]] ; then
|
||||
sgdisk+=( --hybrid "$_mbrParts" ) # --hybrid: create MBR in addition to GPT; $_mbrParts: make these GPT part 1 MBR parts 2[3[4]]
|
||||
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
|
||||
|
||||
sgdisk "${sgdisk[@]}" "${blockDevs[$name]}" >$beQuiet # running all at once is much faster
|
||||
( PATH=@{native.gptfdisk}/bin ; set -x ; sgdisk "${sgdisk[@]}" "${blockDevs[${disk[name]}]}" >$beQuiet ) # running all at once is much faster
|
||||
|
||||
if [[ $_mbrParts ]] ; then
|
||||
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[$name]}") - 1)) # start (size 1sec)
|
||||
$(( $(blockSectorCount "${blockDevs[${disk[name]}]}") - 1)) # start (size 1sec)
|
||||
x;f;r # expert mode ; fix order ; return
|
||||
d;$(( (${#_mbrParts} + 1) / 2 + 1 )) # delete ; part(last)
|
||||
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
|
||||
|
||||
${_extraFDiskCommands}
|
||||
${disk[extraFDiskCommands]}
|
||||
p;w;q # print ; write ; quit
|
||||
" | perl -pe 's/^ *| *(#.*)?$//g' | perl -pe 's/\n\n+| *; */\n/g' | fdisk "${blockDevs[$name]}" &>$beQuiet
|
||||
" | @{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
|
||||
|
||||
partprobe "${blockDevs[$name]}"
|
||||
) ; done
|
||||
sleep 1 # sometimes partitions aren't quite made available yet (TODO: wait "for udev to settle" instead?)
|
||||
@{native.parted}/bin/partprobe "${blockDevs[@]}"
|
||||
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
|
||||
|
||||
# ensure that filesystem creation does not complain about the devices already being occupied by a previous filesystem
|
||||
wipefs --all "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >$beQuiet
|
||||
)}
|
||||
|
||||
## 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=( "$@" )
|
||||
blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
|
||||
else
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
|
||||
fi
|
||||
[[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]]
|
||||
)}
|
||||
|
||||
## For each filesystem in »config.fileSystems« whose ».device« is in »/dev/disk/by-partlabel/«, this creates the specified file system on that partition.
|
||||
function format-partitions {( set -eu
|
||||
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then beQuiet=/dev/stdout ; fi
|
||||
for fsDecl in "@{config.fileSystems!attrsAsBashEvalSets[@]}" ; do (
|
||||
eval "$fsDecl" # _name ; _device ; _fsType ; _formatOptions ; ...
|
||||
if [[ $_device != /dev/disk/by-partlabel/* ]] ; then exit ; fi # i.e. continue
|
||||
blockDev=$(realpath "$_device") ; if [[ $blockDev == /dev/sd* ]] ; then
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" )
|
||||
else
|
||||
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
|
||||
fi
|
||||
if [[ ' '"${blockDevs[@]}"' ' != *' '"$blockDev"' '* ]] ; then echo "Partition alias $_device does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
|
||||
mkfs.${_fsType} ${_formatOptions} "${_device}" >$beQuiet
|
||||
partprobe "${_device}"
|
||||
) ; done
|
||||
beQuiet=/dev/null ; if [[ ${args[debug]:-} ]] ; then beQuiet=/dev/stdout ; fi
|
||||
for fsDecl in "@{config.fileSystems[@]}" ; do
|
||||
eval 'declare -A fs='"$fsDecl"
|
||||
if [[ ${fs[device]} == /dev/disk/by-partlabel/* ]] ; then
|
||||
if ! is-partition-on-disks "${fs[device]}" "${blockDevs[@]}" ; then echo "Partition alias ${fs[device]} used by mount ${fs[mountPoint]} does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
|
||||
elif [[ ${fs[device]} == /dev/mapper/* ]] ; then
|
||||
if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${fs[device]/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device ${fs[device]} used by mount ${fs[mountPoint]} does not point at one of the device mappings ${!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi
|
||||
else continue ; fi
|
||||
( PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH ; set -x ; mkfs.${fs[fsType]} ${fs[formatOptions]} "${fs[device]}" >$beQuiet )
|
||||
@{native.parted}/bin/partprobe "${fs[device]}" || true
|
||||
done
|
||||
for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do
|
||||
if [[ $swapDev == /dev/disk/by-partlabel/* ]] ; then
|
||||
if ! is-partition-on-disks "$swapDev" "${blockDevs[@]}" ; then echo "Partition alias $swapDev used for SWAP does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
|
||||
elif [[ $swapDev == /dev/mapper/* ]] ; then
|
||||
if [[ ! @{config.boot.initrd.luks.devices!catAttrSets.device[${swapDev/'/dev/mapper/'/}]:-} ]] ; then echo "LUKS device $swapDev used for SWAP does not point at one of the device mappings @{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; exit 1 ; fi
|
||||
else continue ; fi
|
||||
( set -x ; mkswap "$swapDev" >$beQuiet )
|
||||
done
|
||||
)}
|
||||
|
||||
## Mounts all file systems as it would happen during boot, but at path prefix »$mnt«.
|
||||
## 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
|
||||
# mount --all --fstab @{config.system.build.toplevel.outPath}/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.outPath}/etc/fstab"}
|
||||
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
|
||||
# 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
|
||||
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
|
||||
if ! mountpoint -q "$mnt"/"$target" ; then (
|
||||
mkdir -p "$mnt"/"$target"
|
||||
[[ $type == tmpfs ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
|
||||
fi
|
||||
) || [[ $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 grep -v '^#' | LC_ALL=C sort -k2 | while read source target type options numbers ; do
|
||||
<$fstabPath @{native.gnugrep}/bin/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
|
||||
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" 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
|
||||
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
|
||||
else
|
||||
source=$mnt/$source ; if [[ ! -e $source ]] ; then mkdir -p "$source" ; fi
|
||||
fi
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target"
|
||||
fi
|
||||
) || [[ $options == *,nofail,* ]] ; fi
|
||||
done
|
||||
)}
|
||||
|
||||
## 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.outPath}/etc/fstab"}
|
||||
<$fstabPath grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
|
||||
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
|
||||
if [[ ! $target || $target == none ]] ; then continue ; fi
|
||||
if mountpoint -q "$mnt"/"$target" ; then
|
||||
umount "$mnt"/"$target"
|
||||
|
@ -3,55 +3,108 @@
|
||||
# NixOS Installation
|
||||
##
|
||||
|
||||
## Ensures that the installer gets called by root and with an argument, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
|
||||
## Entry point to the installation, see »./README.md«.
|
||||
function install-system {( set -eu # 1: blockDev
|
||||
prepare-installer "$@"
|
||||
do-disk-setup "${argv[0]}"
|
||||
init-or-restore-system
|
||||
install-system-to $mnt
|
||||
)}
|
||||
|
||||
## Does very simple argument passing and validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and enables debugging (if requested).
|
||||
function prepare-installer { # ...
|
||||
|
||||
beQuiet=/dev/null ; if [[ ${debug:=} ]] ; then set -x ; beQuiet=/dev/stdout ; fi
|
||||
generic-arg-parse "$@"
|
||||
|
||||
if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run in a root (e.g. in a »sudo --preserve-env=SSH_AUTH_SOCK -i«) shell.' ; exit ; fi
|
||||
if [[ ${SUDO_USER:-} ]] ; then function nix {( args=("$@") ; su - "$SUDO_USER" -c "$(declare -p args)"' ; nix "${args[@]}"' )} ; fi
|
||||
beQuiet=/dev/null ; if [[ ${args[debug]:-} ]] ; then set -x ; beQuiet=/dev/stdout ; fi
|
||||
|
||||
: ${1:?"Required: Target disk or image paths."}
|
||||
: ${argv[0]:?"Required: Target disk or image paths."}
|
||||
|
||||
if [[ $debug ]] ; then set +e ; set -E ; trap 'code= ; bash -l || 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 [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run as root.' ; exit 1 ; fi
|
||||
umask 0022 # Ensure consistent umask (default permissions for new files).
|
||||
|
||||
if [[ -e "/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}" ]] ; then echo "Keystore »/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}/« is already open. Close it and remove the mountpoint before running the installer." ; exit 1 ; fi
|
||||
|
||||
# (partitions are checked in »partition-disks« once the target devices are known)
|
||||
local luksName ; for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
|
||||
if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." ; exit 1 ; fi
|
||||
done
|
||||
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
|
||||
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 [[ ${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.
|
||||
|
||||
}
|
||||
|
||||
## Depending on the presence or absence of the »--restore« CLI flag, either runs the system's initialization or restore commands.
|
||||
# The initialization commands are expected to create files that can't be stored in the (host's or target system's) nix store (i.e. secrets).
|
||||
# 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?
|
||||
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}
|
||||
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
|
||||
)}
|
||||
|
||||
## The default command that will activate the system and install the bootloader. In a separate function to make it easy to replace.
|
||||
function nixos-install-cmd {( set -eu # 1: mnt, 2: topLevel
|
||||
# »nixos-install« by default does some stateful things (see the »--no« options below), builds and copies the system config (but that's already done), and then calls »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- $topLevel/bin/switch-to-configuration boot«, which is essentially the same as »NIXOS_INSTALL_BOOTLOADER=1 nixos-enter -- @{config.system.build.installBootLoader} $targetSystem«, i.e. the side effects of »nixos-enter« and then calling the bootloader-installer.
|
||||
PATH=@{config.systemd.package}/bin:$PATH TMPDIR=/tmp LC_ALL=C nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" #--debug
|
||||
)}
|
||||
|
||||
## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards.
|
||||
# »$topLevel« may point to an alternative top-level dependency to install.
|
||||
function install-system-to {( # 1: mnt, 2?: inspect, 3?: topLevel
|
||||
mnt=$1 ; inspect=${2:-} ; topLevel=${3:-}
|
||||
targetSystem=@{config.system.build.toplevel}
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
function install-system-to {( set -eu # 1: mnt, 2?: topLevel
|
||||
mnt=$1 ; topLevel=${2:-}
|
||||
targetSystem=@{config.system.build.toplevel}
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
|
||||
for dir in dev/ sys/ run/ ; do mkdir -p $mnt/$dir ; mount tmpfs -t tmpfs $mnt/$dir ; prepend_trap "while umount -l $mnt/$dir 2>$beQuiet ; do : ; done" EXIT ; done # proc/ run/
|
||||
mkdir -p -m 755 $mnt/nix/var ; mkdir -p -m 1775 $mnt/nix/store
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
|
||||
# Copy system closure to new nix store:
|
||||
mkdir -p -m 755 $mnt/nix/var/nix ; mkdir -p -m 1775 $mnt/nix/store
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var ; fi
|
||||
( set -x ; time nix copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} ) ; rm -rf $mnt/nix/var/nix/gcroots
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var ; chown :30000 $mnt/nix/store ; fi
|
||||
|
||||
( set -x ; time nix copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
|
||||
ln -sT $(realpath $targetSystem) $mnt/run/current-system
|
||||
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system
|
||||
mkdir -p $mnt/etc/ ; [[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS
|
||||
# Link/create files that some tooling expects:
|
||||
mkdir -p $mnt/etc $mnt/run ; mkdir -p -m 1777 $mnt/tmp
|
||||
mount tmpfs -t tmpfs $mnt/run ; prepend_trap "umount -l $mnt/run" EXIT # If there isn't anything mounted here, »activate« will mount a tmpfs (inside »nixos-enter«'s private mount namespace). That would hide the additions below.
|
||||
[[ -e $mnt/etc/NIXOS ]] || touch $mnt/etc/NIXOS # for »switch-to-configuration«
|
||||
[[ -e $mnt/etc/mtab ]] || ln -sfn /proc/mounts $mnt/etc/mtab
|
||||
ln -sT $(realpath $targetSystem) $mnt/run/current-system
|
||||
#mkdir -p /nix/var/nix/db # »nixos-containers« requires this but nothing creates it before nix is used. BUT »nixos-enter« screams: »/nix/var/nix/db exists and is not a regular file.«
|
||||
|
||||
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.preface.hardware}"-linux ]] ; then # cross architecture installation
|
||||
mkdir -p $mnt/run/binfmt ; cp -a {,$mnt}/run/binfmt/"@{config.preface.hardware}"-linux || true
|
||||
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.preface.hardware}-static«.
|
||||
fi
|
||||
# Set this as the initial system generation:
|
||||
mkdir -p -m 755 $mnt/nix/var/nix/profiles ; ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link ; ln -sT system-1-link $mnt/nix/var/nix/profiles/system
|
||||
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix ; chown :30000 $mnt/nix/store ; fi
|
||||
# Support cross architecture installation (not sure if this is actually required)
|
||||
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.preface.hardware}"-linux ]] ; then
|
||||
mkdir -p $mnt/run/binfmt ; cp -a {,$mnt}/run/binfmt/"@{config.preface.hardware}"-linux || true
|
||||
# Ubuntu (by default) expects the "interpreter" at »/usr/bin/qemu-@{config.preface.hardware}-static«.
|
||||
fi
|
||||
|
||||
mount -o bind /nix/store $mnt/nix/store # all the things required to _run_ the system are copied, but (may) need some more things to initially install it
|
||||
code=0 ; TMPDIR=/tmp LC_ALL=C nixos-install --system ${topLevel:-$targetSystem} --no-root-passwd --no-channel-copy --root $mnt || code=$? #--debug
|
||||
umount -l $mnt/nix/store
|
||||
# Run the main install command (primarily for the bootloader):
|
||||
mount -o bind,ro /nix/store $mnt/nix/store ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT # all the things required to _run_ the system are copied, but (may) need some more things to initially install it
|
||||
code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" || code=$?
|
||||
#umount -l $mnt/nix/store # »nixos-enter« below still needs the bind mount, if installing cross-arch
|
||||
|
||||
if [[ $inspect ]] ; then
|
||||
if (( code != 0 )) ; then
|
||||
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the system mounted in CWD to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." )
|
||||
else
|
||||
( set +x ; echo "Installation done, but the system is still mounted in CWD for inspection. Exit the shell to unmount it." )
|
||||
fi
|
||||
( cd $mnt ; mnt=$mnt bash -l )
|
||||
fi
|
||||
# Done!
|
||||
if [[ ! ${args[no-inspect]:-} ]] ; then
|
||||
if (( code != 0 )) ; then
|
||||
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the mounted system in this chroot shell to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." )
|
||||
else
|
||||
( set +x ; echo "Installation done! This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." )
|
||||
fi
|
||||
PATH=@{config.systemd.package}/bin:$PATH nixos-enter --root $mnt
|
||||
#( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} )
|
||||
elif (( code != 0 )) ; then
|
||||
exit $code
|
||||
fi
|
||||
|
||||
( mkdir -p $mnt/var/lib/systemd/timesync ; touch $mnt/var/lib/systemd/timesync/clock ) || true # save current time
|
||||
( mkdir -p $mnt/var/lib/systemd/timesync ; touch $mnt/var/lib/systemd/timesync/clock ) || true # save current time
|
||||
)}
|
||||
|
79
lib/setup-scripts/keys.sh
Normal file
79
lib/setup-scripts/keys.sh
Normal file
@ -0,0 +1,79 @@
|
||||
|
||||
|
||||
## Prompts for the password of every user that uses a »passwordFile«, to later use that password for home encryption and/or save it in the »passwordFile«.
|
||||
function prompt-for-user-passwords { # (void)
|
||||
declare -g -A userPasswords=( ) # (this ends up in the caller's scope)
|
||||
for user in "@{!config.users.users!catAttrSets.password[@]}" ; do # Also grab any plaintext passwords for testing setups.
|
||||
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
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
## Mounts a ramfs as the host's keystore and populates it with keys as requested by »config.wip.fs.keystore.keys«.
|
||||
# Depending on the specified key types/sources, this may prompt for user input.
|
||||
function populate-keystore { { # (void)
|
||||
local keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}
|
||||
|
||||
mkdir -p $keystore && chmod 750 $keystore && prepend_trap "rmdir $keystore" EXIT
|
||||
mount ramfs -t ramfs $keystore && prepend_trap "umount $keystore" EXIT
|
||||
} && ( set -eu
|
||||
|
||||
declare -A methods=( ) ; declare -A options=( )
|
||||
for usage in "@{!config.wip.fs.keystore.keys[@]}" ; do
|
||||
methodAndOptions="@{config.wip.fs.keystore.keys[$usage]}"
|
||||
method=$(<<<"$methodAndOptions" cut -d= -f1)
|
||||
methods[$usage]=$method ; options[$usage]=${methodAndOptions/$method=/} # TODO: if no options are provided, this passes the method string as options (use something like ${methodAndOptions:(- $(( ${#method} + 1 ))})
|
||||
done
|
||||
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" != inherit ]] ; then continue ; fi
|
||||
from=${options[$usage]}
|
||||
methods[$usage]=${methods[$from]} ; options[$usage]=${options[$from]}
|
||||
done
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" == home-pw || "${methods[$usage]}" == copy ]] ; then continue ; fi
|
||||
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
|
||||
done
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" != home-pw ]] ; then continue ; fi
|
||||
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
|
||||
done
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" != copy ]] ; then continue ; fi
|
||||
add-key-"${methods[$usage]}" "$usage" "${options[$usage]}"
|
||||
done
|
||||
)}
|
||||
|
||||
|
||||
## Creates the LUKS devices specified by the host using the keys created by »populate-keystore«.
|
||||
function create-luks-layers {( set -eu # (void)
|
||||
keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}
|
||||
for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
|
||||
rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]}
|
||||
if ! is-partition-on-disks "$rawDev" "${blockDevs[@]}" ; then echo "Partition alias $rawDev used by LUKS device $luksName does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
|
||||
primaryKey="$keystore"/luks/"$luksName"/0.key
|
||||
|
||||
keyOptions=( --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 )
|
||||
( PATH=@{native.cryptsetup}/bin ; set -x ; cryptsetup --batch-mode luksFormat --key-file="$primaryKey" "${keyOptions[@]}" -c aes-xts-plain64 -s 512 -h sha256 "$rawDev" )
|
||||
for index in 1 2 3 4 5 6 7 ; do
|
||||
if [[ -e "$keystore"/luks/"$luksName"/"$index".key ]] ; then
|
||||
( PATH=@{native.cryptsetup}/bin ; set -x ; cryptsetup luksAddKey --key-file="$primaryKey" "${keyOptions[@]}" "$rawDev" "$keystore"/luks/"$luksName"/"$index".key )
|
||||
fi
|
||||
done
|
||||
done
|
||||
)}
|
||||
|
||||
## Opens the LUKS devices specified by the host, using the opened host's keystore.
|
||||
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
|
||||
rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]}
|
||||
primaryKey="$keystore"/luks/"$luksName"/0.key
|
||||
|
||||
( PATH=@{native.cryptsetup}/bin ; set -x ; cryptsetup --batch-mode luksOpen --key-file="$primaryKey" "$rawDev" "$luksName" ) &&
|
||||
prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $luksName" EXIT
|
||||
done
|
||||
}
|
@ -4,25 +4,30 @@
|
||||
##
|
||||
|
||||
## On the host and for the user it is called by, creates/registers a VirtualBox VM meant to run the shells target host. Requires the path to the target host's »diskImage« as the result of running the install script. The image file may not be deleted or moved. If »bridgeTo« is set (to a host interface name, e.g. as »eth0«), it is added as bridged network "Adapter 2" (which some hosts need).
|
||||
function register-vbox {( set -eu # 1: diskImage, 2?: bridgeTo
|
||||
diskImage=$1 ; bridgeTo=${2:-}
|
||||
function register-vbox {( set -eu # 1: diskImages, 2?: bridgeTo
|
||||
diskImages=$1 ; bridgeTo=${2:-}
|
||||
vmName="nixos-@{config.networking.hostName}"
|
||||
VBoxManage=$( PATH=$hostPath which VBoxManage ) # The host is supposed to run these anyway, and »pkgs.virtualbox« is marked broken on »aarch64«.
|
||||
|
||||
if [[ ! -e $diskImage.vmdk ]] ; then
|
||||
VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through
|
||||
fi
|
||||
$VBoxManage createvm --name "$vmName" --register --ostype Linux26_64
|
||||
$VBoxManage modifyvm "$vmName" --memory 2048 --pae off --firmware efi
|
||||
|
||||
VBoxManage createvm --name "$vmName" --register --ostype Linux26_64
|
||||
VBoxManage modifyvm "$vmName" --memory 2048 --pae off --firmware efi
|
||||
$VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
|
||||
|
||||
VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
|
||||
VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd --medium $diskImage.vmdk
|
||||
index=0 ; for decl in ${diskImages//:/ } ; do
|
||||
diskImage=${decl/*=/}
|
||||
if [[ ! -e $diskImage.vmdk ]] ; then
|
||||
$VBoxManage internalcommands createrawvmdk -filename $diskImage.vmdk -rawdisk $diskImage # pass-through
|
||||
fi
|
||||
$VBoxManage storageattach "$vmName" --storagectl SATA --port $(( index++ )) --device 0 --type hdd --medium $diskImage.vmdk
|
||||
done
|
||||
|
||||
if [[ $bridgeTo ]] ; then # VBoxManage list bridgedifs
|
||||
VBoxManage modifyvm "$vmName" --nic2 bridged --bridgeadapter2 $bridgeTo
|
||||
$VBoxManage modifyvm "$vmName" --nic2 bridged --bridgeadapter2 $bridgeTo
|
||||
fi
|
||||
|
||||
VBoxManage modifyvm "$vmName" --uart1 0x3F8 4 --uartmode1 server /run/user/$(id -u)/$vmName.socket # (guest sets speed)
|
||||
# TODO: The serial settings between qemu and vBox seem incompatible. With a simple »console=ttyS0«, vBox hangs on start. So just disable this for now an use qemu for headless setups. The UX here is awful anyway.
|
||||
#$VBoxManage modifyvm "$vmName" --uart1 0x3F8 4 --uartmode1 server /run/user/$(id -u)/$vmName.socket # (guest sets speed)
|
||||
|
||||
set +x # avoid double-echoing
|
||||
echo '# VM info:'
|
||||
@ -31,10 +36,73 @@ function register-vbox {( set -eu # 1: diskImage, 2?: bridgeTo
|
||||
echo " VBoxManage startvm $vmName --type headless"
|
||||
echo '# kill VM:'
|
||||
echo " VBoxManage controlvm $vmName poweroff"
|
||||
echo '# create TTY:'
|
||||
echo " socat UNIX-CONNECT:/run/user/$(id -u)/$vmName.socket PTY,link=/run/user/$(id -u)/$vmName.pty"
|
||||
echo '# connect TTY:'
|
||||
echo " screen /run/user/$(id -u)/$vmName.pty"
|
||||
#echo '# create TTY:'
|
||||
#echo " socat UNIX-CONNECT:/run/user/$(id -u)/$vmName.socket PTY,link=/run/user/$(id -u)/$vmName.pty"
|
||||
#echo '# connect TTY:'
|
||||
#echo " screen /run/user/$(id -u)/$vmName.pty"
|
||||
echo '# screenshot:'
|
||||
echo " ssh $(hostname) VBoxManage controlvm $vmName screenshotpng /dev/stdout | display"
|
||||
echo " ssh $(@{native.inetutils}/bin/hostname) VBoxManage controlvm $vmName screenshotpng /dev/stdout | display"
|
||||
)}
|
||||
|
||||
## Runs a host in QEMU, taking the same disk specification as the installer. It infers a number of options from he target system's configuration.
|
||||
# Currently, this only works for x64 (on x64) ...
|
||||
function run-qemu {( set -eu # 1: diskImages
|
||||
generic-arg-parse "$@"
|
||||
diskImages=${argv[0]}
|
||||
if [[ ${args[debug]:-} ]] ; then set -x ; fi
|
||||
|
||||
qemu=( @{native.qemu_full}/bin/qemu-system-@{config.preface.hardware} )
|
||||
qemu+=( -m ${args[mem]:-2048} -smp ${args[smp]:-4} )
|
||||
|
||||
if [[ @{config.preface.hardware}-linux == "@{native.system}" && ! ${args[no-kvm]:-} ]] ; then
|
||||
qemu+=( -cpu host -enable-kvm ) # For KVM to work vBox may not be running anything at the same time (and vBox hangs on start if qemu runs). Pass »--no-kvm« and accept ~10x slowdown, or stop vBox.
|
||||
elif [[ @{config.preface.hardware} == aarch64 ]] ; then # assume it's a raspberry PI (or compatible)
|
||||
# TODO: this does not work yet:
|
||||
qemu+=( -machine type=raspi3b -m 1024 ) ; args[no-nat]=1
|
||||
# ... and neither does this:
|
||||
#qemu+=( -M virt -m 1024 -smp 4 -cpu cortex-a53 ) ; args[no-nat]=1
|
||||
fi # else things are going to be quite slow
|
||||
|
||||
for decl in ${diskImages//:/ } ; do
|
||||
qemu+=( -drive format=raw,file="${decl/*=/}" ) #,if=none,index=0,media=disk,id=disk0 -device "virtio-blk-pci,drive=disk0,disable-modern=on,disable-legacy=off" )
|
||||
done
|
||||
|
||||
if [[ @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then
|
||||
qemu+=( -bios @{pkgs.OVMF.fd}/FV/OVMF.fd ) # UEFI. Otherwise it boots something much like a classic BIOS?
|
||||
fi
|
||||
if [[ @{config.preface.hardware} == aarch64 ]] ; then
|
||||
qemu+=( -kernel @{config.system.build.kernel}/Image -initrd @{config.system.build.initialRamdisk}/initrd -append "$(echo -n "@{config.boot.kernelParams[@]}")" )
|
||||
fi
|
||||
|
||||
for param in "@{config.boot.kernelParams[@]}" ; do if [[ $param == 'console=ttyS0' || $param == 'console=ttyS0',* ]] ; then
|
||||
qemu+=( -nographic ) # »-nographic« by default only shows output once th system reaches the login prompt. Add »config.boot.kernelParams = [ "console=tty1" "console=ttyS0" ]« to log to serial (»-nographic«) and the display (if there is one), preferring the last »console« option for the initrd shell (if enabled and requested).
|
||||
fi ; done
|
||||
|
||||
if [[ ! ${args[no-nat]:-} ]] ; then
|
||||
qemu+=( -nic user,model=virtio-net-pci ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2
|
||||
fi
|
||||
|
||||
# TODO: network bridging:
|
||||
#[[ @{config.networking.hostId} =~ ^(.)(.)(.)(.)(.)(.)(.)(.)$ ]] ; mac=$( printf "52:54:%s%s:%s%s:%s%s:%s%s" "${BASH_REMATCH[@]:1}" )
|
||||
#qemu+=( -netdev bridge,id=enp0s3,macaddr=$mac -device virtio-net-pci,netdev=hn0,id=nic1 )
|
||||
|
||||
# To pass a USB device (e.g. a YubiKey for unlocking), add pass »--usb-port=${bus}-${port}«, where bus and port refer to the physical USB port »/sys/bus/usb/devices/${bus}-${port}« (see »lsusb -tvv«). E.g.: »--usb-port=3-1.1.1.4«
|
||||
if [[ ${args[usb-port]:-} ]] ; then for decl in ${args[usb-port]//:/ } ; do
|
||||
qemu+=( -usb -device usb-host,hostbus="${decl/-*/}",hostport="${decl/*-/}" )
|
||||
done ; fi
|
||||
|
||||
( set -x ; "${qemu[@]}" )
|
||||
|
||||
# https://askubuntu.com/questions/54814/how-can-i-ctrl-alt-f-to-get-to-a-tty-in-a-qemu-session
|
||||
|
||||
)}
|
||||
|
||||
## Creates a random static key on a new key partition on the GPT partitioned »$blockDev«. The drive can then be used as headless but removable disk unlock method.
|
||||
# To create/clear the GPT: $ sgdisk --zap-all "$blockDev"
|
||||
function add-bootkey-to-keydev {( set -eu # 1: blockDev, 2?: hostHash
|
||||
blockDev=$1 ; hostHash=${2:-@{config.networking.hostName!hashString.sha256}}
|
||||
bootkeyPartlabel=bootkey-${hostHash:0:8}
|
||||
@{native.gptfdisk}/bin/sgdisk --new=0:0:+1 --change-name=0:"$bootkeyPartlabel" --typecode=0:0000 "$blockDev" # create new 1 sector (512b) partition
|
||||
@{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"
|
||||
)}
|
||||
|
@ -3,6 +3,22 @@
|
||||
# Utilities
|
||||
##
|
||||
|
||||
## Performs a simple and generic parsing of CLI arguments. Creates a global associative array »args« and a global normal array »argv«.
|
||||
# Named options may be passed as »--name[=value]«, where »value« defaults to »1«, and are assigned to »args«.
|
||||
# Everything else, or everything following the »--« argument, ends up as positional arguments in »argv«.
|
||||
# Checking the validity of the parsed arguments is up to the caller.
|
||||
function generic-arg-parse { # ...
|
||||
declare -g -A args=( ) ; declare -g -a argv=( ) # this ends up in the caller's scope
|
||||
while (( "$#" )) ; do
|
||||
if [[ $1 == -- ]] ; then shift ; argv+=( "$@" ) ; return ; fi
|
||||
if [[ $1 == --* ]] ; then
|
||||
if [[ $1 == *=* ]] ; then
|
||||
local key=${1/=*/} ; args[${key/--/}]=${1/$key=/}
|
||||
else args[${1/--/}]=1 ; fi
|
||||
else argv+=( "$1" ) ; fi
|
||||
shift ; done
|
||||
}
|
||||
|
||||
## 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
|
||||
@ -15,3 +31,34 @@ prepend_trap() { # 1: command, ...: trapNames
|
||||
)" "${name}" || fatal "unable to add to trap ${name}"
|
||||
done
|
||||
} ; 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.
|
||||
function write-secret {( set -eu # 1: path, 2?: owner[:[group]], 3?: mode
|
||||
mkdir -p -- "$(dirname "$1")"/
|
||||
install -o root -g root -m 000 -T /dev/null "$1"
|
||||
secret=$(tee "$1") # copy stdin to path without removing or adding anything
|
||||
if [[ "${#secret}" == 0 ]] ; then echo "write-secret to $1 was empty!" 1>&2 ; exit 1 ; fi # could also stat the file ...
|
||||
chown "${2:-root:root}" -- "$1"
|
||||
chmod "${3:-400}" -- "$1"
|
||||
)}
|
||||
|
||||
## Interactively prompts for a password to be entered and confirmed.
|
||||
function prompt-new-password {( set -eu # 1: usage
|
||||
usage=$1
|
||||
read -s -p "Please enter the new password $usage: " password1 ; echo 1>&2
|
||||
read -s -p "Please enter the same password again: " password2 ; echo 1>&2
|
||||
if (( ${#password1} == 0 )) || [[ "$password1" != "$password2" ]] ; then printf 'Passwords empty or mismatch, aborting.\n' 1>&2 ; exit 1 ; fi
|
||||
printf %s "$password1"
|
||||
)}
|
||||
|
||||
## Runs an installer hook script, optionally stepping through the script.
|
||||
function run-hook-script {( set -eu # 1: title, 2: scriptPath
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
if [[ ${args[inspectScripts]:-} && "$(cat "$2")" != $'' ]] ; then
|
||||
echo "Running $1 commands. For each command printed, press Enter to continue or Ctrl+C to abort the installation:"
|
||||
# (this does not help against intentionally malicious scripts, it's quite easy to trick this)
|
||||
BASH_PREV_COMMAND= ; set -o functrace ; trap 'if [[ $BASH_COMMAND != "$BASH_PREV_COMMAND" ]] ; then echo -n "> $BASH_COMMAND" >&2 ; read ; fi ; BASH_PREV_COMMAND=$BASH_COMMAND' debug
|
||||
fi
|
||||
source "$2"
|
||||
)}
|
||||
|
137
lib/setup-scripts/zfs.sh
Normal file
137
lib/setup-scripts/zfs.sh
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
# These functions have »pkgs.zfs« as undeclared dependency (so that they can alternatively use initramfs' »extraUtils«).
|
||||
|
||||
## Creates the system's ZFS pools and their datasets.
|
||||
function create-zpools { # 1: mnt
|
||||
local mnt=$1 ; local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do ( set -eu
|
||||
eval 'declare -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
|
||||
eval 'declare -a vdevs='"${pool[vdevArgs]}"
|
||||
eval 'declare -A poolProps='"${pool[props]}"
|
||||
eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
|
||||
eval 'declare -A dataProps='"${dataset[props]}"
|
||||
get-zfs-crypt-props "${dataset[name]}" dataProps
|
||||
declare -a args=( )
|
||||
for name in "${!poolProps[@]}" ; do args+=( -o "${name}=${poolProps[$name]}" ) ; done
|
||||
for name in "${!dataProps[@]}" ; do args+=( -O "${name}=${dataProps[$name]}" ) ; done
|
||||
for index in "${!vdevs[@]}" ; do
|
||||
part=${vdevs[$index]} ; if [[ $part =~ ^(mirror|raidz[123]?|draid[123]?.*|spare|log|dedup|special|cache)$ ]] ; then continue ; fi
|
||||
if [[ @{config.boot.initrd.luks.devices!catAttrSets.device[$part]:-} ]] ; then
|
||||
vdevs[$index]=/dev/mapper/$part
|
||||
else
|
||||
part=/dev/disk/by-partlabel/$part ; vdevs[$index]=$part
|
||||
if ! is-partition-on-disks "$part" "${blockDevs[@]}" ; then echo "Partition alias $part used by zpool ${pool[name]} does not point at one of the target disks ${blockDevs[@]}" ; exit 1 ; fi
|
||||
fi
|
||||
done
|
||||
( set -x ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" )
|
||||
) && {
|
||||
prepend_trap "zpool export '$poolName'" EXIT
|
||||
} ; done &&
|
||||
|
||||
ensure-datasets $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.
|
||||
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)
|
||||
filterExp=${2:-'^'}
|
||||
tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
|
||||
|
||||
: 'Step-through is very verbose and breaks the loop, disabling it for this function' ; trap - debug
|
||||
printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z | while IFS= read -r -d $'\0' name ; do
|
||||
if [[ ! $name =~ $filterExp ]] ; then printf 'Skipping dataset »%s« since it does not match »%s«\n' "$name" "$filterExp" >&2 ; continue ; fi
|
||||
|
||||
eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[$name]}"
|
||||
eval 'declare -A props='"${dataset[props]}"
|
||||
|
||||
explicitKeylocation=${props[keylocation]:-}
|
||||
get-zfs-crypt-props "${dataset[name]}" props cryptKey cryptRoot
|
||||
|
||||
if zfs get -o value -H name "${dataset[name]}" &>/dev/null ; then # dataset exists: check its properties
|
||||
|
||||
if [[ ${props[mountpoint]:-} ]] ; then # don't set the current mount point again (no-op), cuz that fails if the dataset is mounted
|
||||
current=$(zfs get -o value -H mountpoint "${dataset[name]}") ; current=${current/$mnt/}
|
||||
if [[ ${props[mountpoint]} == "${current:-/}" ]] ; then unset props[mountpoint] ; fi
|
||||
fi
|
||||
if [[ ${props[keyformat]:-} == ephemeral ]] ; then
|
||||
cryptRoot=${dataset[name]} ; unset props[keyformat] ; props[keylocation]=file:///dev/null
|
||||
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 (
|
||||
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( "${name}=${props[$name]}" ) ; done
|
||||
( set -x ; zfs set "${args[@]}" "${dataset[name]}" )
|
||||
) ; 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
|
||||
fi
|
||||
if [[ $(zfs get -o value -H keystatus "${dataset[name]}") != available ]] ; then
|
||||
zfs load-key -L file://"$cryptKey" "${dataset[name]}" # will unload with parent
|
||||
fi
|
||||
( set -x ; zfs change-key -i "${dataset[name]}" )
|
||||
) ; fi
|
||||
|
||||
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 ...
|
||||
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
|
||||
( set -x ; zfs create "${args[@]}" "${dataset[name]}" )
|
||||
) ; 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" )
|
||||
) ; fi
|
||||
if [[ $explicitKeylocation && $explicitKeylocation != "${props[keylocation]:-}" ]] ; then
|
||||
( set -x ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" )
|
||||
fi
|
||||
zfs snapshot -r "${dataset[name]}"@empty
|
||||
fi
|
||||
|
||||
eval 'declare -A allows='"${dataset[permissions]}"
|
||||
for who in "${!allows[@]}" ; do
|
||||
# »zfs allow $dataset« seems to be the only way to view permissions, and that is not very parsable -.-
|
||||
( set -x ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 )
|
||||
done
|
||||
done
|
||||
|
||||
)}
|
||||
|
||||
## TODO
|
||||
function get-zfs-crypt-props { # 1: datasetPath, 2: name_cryptProps, 3: name_cryptKey, 4: name_cryptRoot
|
||||
local hash=@{config.networking.hostName!hashString.sha256:0:8}
|
||||
local keystore=/run/keystore-$hash
|
||||
local -n __cryptProps=${2:-props} ; local -n __cryptKey=${3:-cryptKey} ; local -n __cryptRoot=${4:-cryptRoot}
|
||||
|
||||
local name=$1 ; {
|
||||
if [[ $name == */* ]] ; then local pool=${name/\/*/}/ ; local path=/${name/$pool/} ; else local pool=$name/ ; local path= ; fi
|
||||
} ; local key=${pool/-$hash'/'/}$path # strip hash from pool name
|
||||
|
||||
__cryptKey='' ; __cryptRoot=''
|
||||
if [[ @{config.wip.fs.keystore.keys[zfs/$name]:-} ]] ; then
|
||||
if [[ @{config.wip.fs.keystore.keys[zfs/$name]} == unencrypted ]] ; then
|
||||
__cryptProps[encryption]=off # empty key to disable encryption
|
||||
else
|
||||
__cryptProps[encryption]=aes-256-gcm ; __cryptProps[keyformat]=hex ; __cryptProps[keylocation]=file://"$keystore"/zfs/"$name".key
|
||||
__cryptKey=$keystore/zfs/$name.key ; __cryptRoot=$name
|
||||
fi
|
||||
else
|
||||
while true ; do
|
||||
name=$(dirname $name) ; if [[ $name == . ]] ; then break ; fi
|
||||
if [[ @{config.wip.fs.keystore.keys[zfs/$name]:-} ]] ; then
|
||||
if [[ @{config.wip.fs.keystore.keys[zfs/$name]} != unencrypted ]] ; then
|
||||
__cryptKey=$keystore/zfs/$name.key ; __cryptRoot=$name
|
||||
fi ; break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
Reference in New Issue
Block a user