mirror of
https://github.com/NiklasGollenstede/nixos-installer.git
synced 2025-08-09 07:31:24 +02:00
add VPS-worker factory, add vm-exec module, improve run-qemu function, add push-flake script, support installing systems as non-root, script refactoring
This commit is contained in:
@ -33,3 +33,15 @@ Once done, the disk can be transferred -- or the image be copied -- to the final
|
||||
If the host's hardware target allows, a resulting image can also be passed to [`register-vbox`](./maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user, or to [`run-qemu`](./maintenance.sh#run-qemu) to start it in a qemu VM.
|
||||
|
||||
The "Installation" section of each host's documentation should contain host specific details, if any.
|
||||
|
||||
|
||||
## Development Notes
|
||||
|
||||
* The functions are designed to be (and by default are) executed with the bash options `pipefail` and `nounset` (`-u`) set.
|
||||
* When the functions are executed, `generic-arg-parse` has already been called on the CLI arguments, and the parsed result can be accessed as `"${args[<name>]:-}"` for named arguments and `"${argv[<index>]}"` for positional arguments (except the first one, which has been removed and used as the command or name of the entry function to run).
|
||||
* Do not use `set -e`. It has some unexpected and unpredictable behavior, and *does not* actually provide the expected semantic of "exit the shell if a command fails (exits != 0)". For example, the internal exit behavior of commands in a function depends on *how the function is called*.
|
||||
* If the `--debug` flag is passed, then `return` and `exit` are aliased to open a shell when `$?` is not zero. This effectively turns any `|| return` / `|| exit` into break-on-error point.
|
||||
* The aliasing does not work if an explicit code is provided to `return` or `exit`. In these cases, or where the breakpoint behavior is not desired, use `\return` or `\exit` (since the `\` suppresses the alias expansion).
|
||||
* For/in loops, do not write to / `read` from stdin/fd1, which conflicts with the `return`/`exit` aliasing. Instead use a different file descriptor, e.g.: `while read -u3 a b c ; do ... done 3< <( LC_ALL=C sort ... )`.
|
||||
* Similarly in functions that expect stdin data, read all of it before using the first `|| return`.
|
||||
* `@{native}` is an instance of `nixpkgs` for the calling system (not the target system) with the overlays (implicitly or explicitly) passed to `mkSystemsFlake` applied, but without other `nixpkgs.overlays` set by the system configuration itself.
|
||||
|
@ -14,14 +14,14 @@ function gen-key-unencrypted {( set -eu # 1: usage
|
||||
## Uses the hostname as a trivial key.
|
||||
function gen-key-hostname {( set -eu # 1: usage
|
||||
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' 1>&2 ; exit 1 ; fi
|
||||
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf '»trivial« key mode is only available for the keystore itself.\n' 1>&2 ; \exit 1 ; fi
|
||||
printf %s "@{config.networking.hostName}"
|
||||
)}
|
||||
|
||||
## Obtains a key by reading it from a bootkey partition (see »add-bootkey-to-keydev«).
|
||||
function gen-key-usb-part {( set -eu # 1: usage
|
||||
usage=$1
|
||||
if [[ ! "$usage" =~ ^(luks/keystore-[^/]+/[1-8])$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' 1>&2 ; exit 1 ; fi
|
||||
if [[ ! "$usage" =~ ^(luks/keystore-[^/]+/[1-8])$ ]] ; then printf '»usb-part« key mode is only available for the keystore itself.\n' 1>&2 ; \exit 1 ; fi
|
||||
bootkeyPartlabel=bootkey-"@{config.networking.hostName!hashString.sha256:0:8}"
|
||||
cat /dev/disk/by-partlabel/"$bootkeyPartlabel"
|
||||
)}
|
||||
@ -41,7 +41,7 @@ function gen-key-constant {( set -eu # 1: _, 2: value
|
||||
## Obtains a key by prompting for a password.
|
||||
function gen-key-password {( set -eu # 1: usage
|
||||
usage=$1
|
||||
( prompt-new-password "as key for @{config.networking.hostName}:$usage" || exit 1 )
|
||||
( prompt-new-password "as key for @{config.networking.hostName}:$usage" || \exit 1 )
|
||||
)}
|
||||
|
||||
## Generates a key by prompting for (or reusing) a »$user«'s password, combining it with »$keystore/home/$user.key«.
|
||||
@ -51,9 +51,9 @@ function gen-key-home-composite {( set -eu # 1: usage, 2: user
|
||||
password=${userPasswords[$user]}
|
||||
else
|
||||
password=$(prompt-new-password "that will be used as component of the key for »@{config.networking.hostName}:$usage«")
|
||||
if [[ ! $password ]] ; then exit 1 ; fi
|
||||
if [[ ! $password ]] ; then \exit 1 ; fi
|
||||
fi
|
||||
( cat "$keystore"/home/"$user".key && cat <<<"$password" ) | sha256sum | head -c 64
|
||||
{ cat "$keystore"/home/"$user".key && cat <<<"$password" ; } | sha256sum | head -c 64
|
||||
)}
|
||||
|
||||
## Generates a reproducible, host-independent key by challenging slot »$slot« of YubiKey »$serial« with »$user«'s password.
|
||||
@ -65,7 +65,7 @@ function gen-key-home-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndUser(as
|
||||
password=${userPasswords[$user]}
|
||||
else
|
||||
password=$(prompt-new-password "as YubiKey challenge for »@{config.networking.hostName}:$usage«")
|
||||
if [[ ! $password ]] ; then exit 1 ; fi
|
||||
if [[ ! $password ]] ; then \exit 1 ; fi
|
||||
fi
|
||||
gen-key-yubikey-challenge "$usage" "$serial:$slot:home-$user=$password" true "»${user}«'s password (for key »${usage}«)"
|
||||
)}
|
||||
@ -74,7 +74,7 @@ function gen-key-home-yubikey {( set -eu # 1: usage, 2: serialAndSlotAndUser(as
|
||||
function gen-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
|
||||
usage=$1 ; serialAndSlot=$2
|
||||
pin=$( prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for »@{config.networking.hostName}:$usage«" )
|
||||
if [[ ! $pin ]] ; then exit 1 ; fi
|
||||
if [[ ! $pin ]] ; then \exit 1 ; fi
|
||||
gen-key-yubikey-challenge "$usage" "$serialAndSlot:$pin" true "password / pin as key for »@{config.networking.hostName}:$usage«"
|
||||
)}
|
||||
|
||||
@ -100,19 +100,19 @@ function gen-key-yubikey-challenge {( set -eu # 1: _, 2: serialAndSlotAndChallen
|
||||
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" 1>&2 ; exit 1 ; fi
|
||||
if [[ "$serial" != "$( @{native.yubikey-personalization}/bin/ykinfo -sq )" ]] ; then printf 'YubiKey with serial %s not present, aborting.\n' "$serial" 1>&2 ; \exit 1 ; fi
|
||||
|
||||
if [[ ! "${3:-}" ]] ; then
|
||||
secret="$( @{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge":1 )""$( sleep .5 || : ; @{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge":2 || @{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge":2 )" # the second consecutive challenge tends to fail if it follows immediately
|
||||
if [[ ${#secret} != 80 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; exit 1 ; fi
|
||||
if [[ ${#secret} != 80 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; \exit 1 ; fi
|
||||
else
|
||||
secret="$( @{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge" )"
|
||||
if [[ ${#secret} != 40 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; exit 1 ; fi
|
||||
if [[ ${#secret} != 40 ]] ; then printf 'YubiKey challenge failed, aborting.\n' "$serial" 1>&2 ; \exit 1 ; fi
|
||||
fi
|
||||
printf %s "$secret" | head -c 64
|
||||
{ printf %s "$secret" || true ; } | head -c 64
|
||||
)}
|
||||
|
||||
## Generates a random secret key.
|
||||
function gen-key-random {( set -eu # 1: usage
|
||||
</dev/urandom tr -dc 0-9a-f | head -c 64
|
||||
</dev/urandom @{native.xxd}/bin/xxd -l 32 -c 64 -p
|
||||
)}
|
||||
|
@ -1,12 +1,7 @@
|
||||
dirname: inputs: let
|
||||
|
||||
getNamedScriptFiles = dir: builtins.removeAttrs (builtins.listToAttrs (map (name: let
|
||||
match = builtins.match ''^(.*)[.]sh([.]md)?$'' name;
|
||||
in if (match != null) then {
|
||||
name = builtins.head match; value = "${dir}/${name}";
|
||||
} else { name = ""; value = null; }) (builtins.attrNames (builtins.readDir dir)))) [ "" ];
|
||||
|
||||
inherit (inputs.config) prefix;
|
||||
inherit (import "${dirname}/../imports.nix" dirname inputs) getFilesExt;
|
||||
|
||||
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
|
||||
builtins.toFile name (builtins.replaceStrings
|
||||
@ -16,4 +11,4 @@ dirname: inputs: let
|
||||
)
|
||||
)));
|
||||
|
||||
in replacePrefix (getNamedScriptFiles dirname)
|
||||
in replacePrefix (getFilesExt "sh(.md)?" dirname)
|
||||
|
@ -11,7 +11,8 @@ function do-disk-setup { # 1: diskPaths
|
||||
|
||||
mnt=/tmp/nixos-install-@{config.networking.hostName} && mkdir -p "$mnt" && prepend_trap "rmdir $mnt" EXIT || return # »mnt=/run/user/0/...« would be more appropriate, but »nixos-install« does not like the »700« permissions on »/run/user/0«
|
||||
|
||||
partition-disks "$1" || return
|
||||
ensure-disks "$1" || return
|
||||
partition-disks || return
|
||||
create-luks-layers && open-luks-layers || return # 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} || return
|
||||
|
||||
@ -34,11 +35,8 @@ function do-disk-setup { # 1: diskPaths
|
||||
# * So alignment at the default »align=8MiB« actually seems a decent choice.
|
||||
|
||||
|
||||
## 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
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
## Parses and expands »diskPaths« to ensure that a disk or image exists for each »config.wip.fs.disks.devices«, creates and loop-mounts images for non-/dev/ paths, and checks whether physical device sizes match.
|
||||
function ensure-disks { # 1: diskPaths, 2?: skipLosetup
|
||||
|
||||
declare -g -A blockDevs=( ) # this ends up in the caller's scope
|
||||
if [[ $1 == */ ]] ; then
|
||||
@ -47,42 +45,52 @@ function partition-disks { # 1: diskPaths
|
||||
else
|
||||
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" 1>&2 ; return 1 ; fi
|
||||
if [[ ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name specified more than once. Duplicate definition: $path" 1>&2 ; \return 1 ; fi
|
||||
blockDevs[$name]=$path
|
||||
done
|
||||
fi
|
||||
|
||||
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do
|
||||
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" 1>&2 ; return 1 ; fi
|
||||
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" 1>&2 ; \return 1 ; fi
|
||||
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
if [[ ${blockDevs[$name]} != /dev/* ]] ; then
|
||||
local outFile=${blockDevs[$name]} &&
|
||||
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!
|
||||
install -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" || return
|
||||
if [[ ${args[image-owner]:-} ]] ; then chown "${args[image-owner]}" "$outFile" || return ; fi
|
||||
if [[ ${2:-} ]] ; then continue ; fi
|
||||
blockDevs[$name]=$( losetup --show -f "$outFile" ) && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT || return
|
||||
else
|
||||
local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} ))
|
||||
if [[ ! $size ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" 1>&2 ; return 1 ; fi
|
||||
if (( waste < 0 )) ; then echo "Block device ${blockDevs[$name]}'s size $size is smaller than the size ${disk[size]} declared for $name" ; return 1 ; fi
|
||||
if (( waste > 0 )) && [[ ! ${disk[allowLarger]:-} ]] ; then echo "Block device ${blockDevs[$name]}'s size $size is bigger than the size ${disk[size]} declared for $name" 1>&2 ; return 1 ; fi
|
||||
if [[ ! $size ]] ; then echo "Block device $name does not exist at ${blockDevs[$name]}" 1>&2 ; \return 1 ; fi
|
||||
if (( waste < 0 )) ; then echo "Block device ${blockDevs[$name]}'s size $size is smaller than the size ${disk[size]} declared for $name" 1>&2 ; \return 1 ; fi
|
||||
if (( waste > 0 )) && [[ ! ${disk[allowLarger]:-} ]] ; then echo "Block device ${blockDevs[$name]}'s size $size is bigger than the size ${disk[size]} declared for $name" 1>&2 ; \return 1 ; fi
|
||||
if (( waste > 0 )) ; then echo "Wasting $(( waste / 1024))K of ${blockDevs[$name]} due to the size declared for $name (should be ${size}b)" 1>&2 ; fi
|
||||
blockDevs[$name]=$(realpath "${blockDevs[$name]}")
|
||||
blockDevs[$name]=$( realpath "${blockDevs[$name]}" ) || return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
## Partitions the »blockDevs« (matching »config.wip.fs.disks.devices«) to ensure that all specified »config.wip.fs.disks.partitions« exist.
|
||||
# Tries to abort if any partition already exists on the host.
|
||||
function partition-disks {
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
|
||||
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
|
||||
eval 'local -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]} 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!" 1>&2 ; return 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!" 1>&2 ; \return 1 ; fi
|
||||
done
|
||||
|
||||
for name in "@{!config.wip.fs.disks.devices[@]}" ; do
|
||||
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
if [[ ${disk[serial]:-} ]] ; then
|
||||
actual=$( udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' )
|
||||
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" 1>&2 ; return 1 ; fi
|
||||
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" 1>&2 ; \return 1 ; fi
|
||||
fi
|
||||
# can (and probably should) restore the backup:
|
||||
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent || exit ) || return
|
||||
#partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}"
|
||||
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent ) || return
|
||||
#partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}" || return
|
||||
done
|
||||
@{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud || return
|
||||
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
|
||||
@ -127,10 +135,10 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
|
||||
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" >$ || exit ) || return # running all at once is much faster
|
||||
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk "${sgdisk[@]}" "$blockDev" >$ ) || return # running all at once is much faster
|
||||
|
||||
if [[ ${disk[mbrParts]:-} ]] ; then
|
||||
printf "
|
||||
printf %s "
|
||||
M # edit hybrid MBR
|
||||
d;1 # delete parts 1 (GPT)
|
||||
|
||||
@ -147,46 +155,44 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
|
||||
|
||||
${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" &>$beLoud || exit ) || return
|
||||
" | @{native.gnused}/bin/sed -E 's/^ *| *(#.*)?$//g' | @{native.gnused}/bin/sed -E 's/\n\n+| *; */\n/g' |
|
||||
tee >( [[ ! ${_set_x:-} ]] || ( echo -n '++ ' ; tr $'\n' '|' ; echo ) 1>&2 ; cat >/dev/null ) |
|
||||
( PATH=@{native.util-linux}/bin ; ${_set_x:-:} ; fdisk "$blockDev" &>$beLoud ) || return
|
||||
fi
|
||||
}
|
||||
|
||||
## Checks whether a »partition« resides on one of the provided »blockDevs«.
|
||||
function is-partition-on-disks { # 1: partition, ...: blockDevs
|
||||
local partition=$1 ; shift ; local -a blockDevs=( "$@" )
|
||||
local 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"' '* ]]
|
||||
local blockDev=/dev/$( basename "$( readlink -f /sys/class/block/"$( basename "$( realpath "$partition" )" )"/.. )" ) || return
|
||||
[[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]] || return
|
||||
}
|
||||
|
||||
## 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 -u
|
||||
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
function format-partitions {
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; 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
|
||||
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[@]}" 1>&2 ; \return 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
|
||||
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[@]}" 1>&2 ; \return 1 ; fi
|
||||
else continue ; fi
|
||||
#if [[ ${fs[fsType]} == ext4 && ' '${fs[formatOptions]}' ' != *' -F '* ]] ; then fs[formatOptions]+=' -F' ; fi
|
||||
#if [[ ${fs[fsType]} == f2fs && ' '${fs[formatOptions]}' ' != *' -f '* ]] ; then fs[formatOptions]+=' -f' ; 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]}" >$beLoud 2>$beSilent ) || exit
|
||||
( PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH ; ${_set_x:-:} ; mkfs.${fs[fsType]} ${fs[formatOptions]} "${fs[device]}" >$beLoud 2>$beSilent ) || return
|
||||
@{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
|
||||
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[@]}" 1>&2 ; \return 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
|
||||
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[@]}" 1>&2 ; \return 1 ; fi
|
||||
else continue ; fi
|
||||
( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || exit
|
||||
( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || return
|
||||
done
|
||||
)}
|
||||
}
|
||||
|
||||
## This makes the installation of grub to loop devices shut up, but booting still does not work (no partitions are found). I'm done with GRUB; EXTLINUX works.
|
||||
# (This needs to happen before mounting.)
|
||||
@ -197,7 +203,7 @@ function fix-grub-install {
|
||||
if [[ ! @{config.fileSystems[$mount]:-} ]] ; then continue ; fi
|
||||
device=$( eval 'declare -A fs='"@{config.fileSystems[$mount]}" ; echo "${fs[device]}" )
|
||||
label=${device/\/dev\/disk\/by-partlabel\//}
|
||||
if [[ $label == "$device" || $label == *' '* || ' '@{config.wip.fs.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; return 1 ; fi
|
||||
if [[ $label == "$device" || $label == *' '* || ' '@{config.wip.fs.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; \return 1 ; fi
|
||||
bootLoop=$( losetup --show -f /dev/disk/by-partlabel/$label ) || return ; prepend_trap "losetup -d $bootLoop" EXIT
|
||||
ln -sfT ${bootLoop/\/dev/..\/..} /dev/disk/by-partlabel/$label || return
|
||||
done
|
||||
@ -215,13 +221,13 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath, 3?: allowFail
|
||||
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"} ; allowFail=${3:-}
|
||||
PATH=@{native.e2fsprogs}/bin:@{native.f2fs-tools}/bin:@{native.xfsprogs}/bin:@{native.dosfstools}/bin:$PATH
|
||||
|
||||
<$fstabPath grep -v '^#' | while read source target type options numbers ; do
|
||||
while read -u3 source target type options numbers ; do
|
||||
if [[ ! $target || $target == none ]] ; then continue ; fi
|
||||
options=,$options, ; options=${options//,ro,/,}
|
||||
|
||||
if ! mountpoint -q "$mnt"/"$target" ; then (
|
||||
mkdir -p "$mnt"/"$target" || exit
|
||||
[[ $type == tmpfs || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
|
||||
[[ $type == tmpfs || $type == auto || $type == */* ]] || @{native.kmod}/bin/modprobe --quiet $type || true # (this does help sometimes)
|
||||
|
||||
if [[ $type == overlay ]] ; then
|
||||
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target.
|
||||
@ -238,16 +244,16 @@ function mount-system {( set -eu # 1: mnt, 2?: fstabPath, 3?: allowFail
|
||||
mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
|
||||
|
||||
) || [[ $options == *,nofail,* || $allowFail ]] || exit ; fi # (actually, nofail already makes mount fail silently)
|
||||
done || exit
|
||||
done 3< <( <$fstabPath grep -v '^#' )
|
||||
)}
|
||||
|
||||
## 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 grep -v '^#' | LC_ALL=C sort -k2 -r | while read source target rest ; do
|
||||
while read -u3 source target rest ; do
|
||||
if [[ ! $target || $target == none ]] ; then continue ; fi
|
||||
if mountpoint -q "$mnt"/"$target" ; then
|
||||
umount "$mnt"/"$target"
|
||||
fi
|
||||
done
|
||||
done 3< <( { <$fstabPath grep -v '^#' ; echo ; } | tac )
|
||||
)}
|
||||
|
@ -4,33 +4,33 @@
|
||||
##
|
||||
|
||||
## Entry point to the installation, see »./README.md«.
|
||||
function install-system {( set -u # 1: blockDev
|
||||
function install-system {( set -o pipefail -u # (void)
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
prepare-installer "$@" || exit
|
||||
prepare-installer || exit
|
||||
do-disk-setup "${argv[0]}" || exit
|
||||
install-system-to $mnt || exit
|
||||
)}
|
||||
|
||||
## Does very simple argument paring 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 { # ...
|
||||
|
||||
generic-arg-parse "$@" || return
|
||||
|
||||
if [[ ${args[debug]:-} ]] ; then set -x ; fi
|
||||
## Does some argument validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and runs the installation in qemu (if requested).
|
||||
function prepare-installer { # (void)
|
||||
|
||||
: ${argv[0]:?"Required: Target disk or image paths."}
|
||||
|
||||
if [[ "$(id -u)" != '0' ]] ; then echo 'Script must be run as root.' 1>&2 ; return 1 ; fi
|
||||
if [[ "$(id -u)" != '0' ]] ; then
|
||||
if [[ ! ${args[no-vm]:-} ]] ; then reexec-in-qemu || return ; \exit 0 ; fi
|
||||
echo 'Script must be run as root or in qemu (without »--no-vm«).' 1>&2 ; \return 1
|
||||
fi
|
||||
if [[ ${args[vm]:-} ]] ; then reexec-in-qemu || return ; \exit 0 ; 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." 1>&2 ; return 1 ; fi
|
||||
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." 1>&2 ; \return 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." 1>&2 ; return 1 ; fi
|
||||
if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." 1>&2 ; \return 1 ; fi
|
||||
done
|
||||
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
|
||||
if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." 1>&2 ; return 1 ; fi
|
||||
if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." 1>&2 ; \return 1 ; fi
|
||||
done
|
||||
|
||||
if [[ ${SUDO_USER:-} ]] ; then # use Nix as the user who called this script, as Nix may not be set up for root
|
||||
@ -41,16 +41,53 @@ function prepare-installer { # ...
|
||||
|
||||
_set_x='set -x' ; if [[ ${args[quiet]:-} ]] ; then _set_x=: ; fi
|
||||
|
||||
if [[ ${args[debug]:-} ]] ; then set +e ; set -E ; trap 'code= ; timeout .2s cat &>/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 each level of each nested sub-shells. The »timeout cat« eats anything lined up on stdin, which would otherwise be sent to bash and interpreted as commands.
|
||||
#if [[ ${args[debug]:-} ]] ; then set +e ; set -E ; trap 'code= ; timeout .2s cat &>/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 each level of each nested sub-shells. The »timeout cat« eats anything lined up on stdin, which would otherwise be sent to bash and interpreted as commands.
|
||||
|
||||
export PATH=$PATH:@{native.util-linux}/bin # Doing a system installation requires a lot of stuff from »util-linux«. This should probably be moved into the individual functions that actually use the tools ...
|
||||
|
||||
}
|
||||
|
||||
## Re-executes the current system's installation in a qemu VM.
|
||||
function reexec-in-qemu {
|
||||
|
||||
# (not sure whether this works for block devices)
|
||||
ensure-disks "${argv[0]}" 1 || return
|
||||
qemu=( -m 2048 ) ; declare -A qemuDevs=( )
|
||||
local index=2 ; local name ; for name in "${!blockDevs[@]}" ; do
|
||||
#if [[ ${blockDevs[$name]} != /dev/* ]] ; then
|
||||
qemu+=( # not sure how correct the interpretations of the command are
|
||||
-drive format=raw,file="$( realpath "${blockDevs[$name]}" )",media=disk,if=none,index=${index},id=drive${index} # create the disk drive, without attaching it, name it driveX
|
||||
#-device ahci,acpi-index=${index},id=ahci${index} # create an (ich9-)AHCI bus named »ahciX«
|
||||
#-device ide-hd,drive=drive${index},bus=ahci${index}.${index} # attach IDE?! disk driveX as device X on bus »ahciX«
|
||||
-device virtio-blk-pci,drive=drive${index},disable-modern=on,disable-legacy=off # alternative to the two lines above (implies to be faster, but seems to require guest drivers)
|
||||
)
|
||||
qemuDevs[$name]=/dev/vd$( printf "\x$(printf %x $(( index - 1 + 97 )) )" ) # a is used by the (unused) root disk
|
||||
let index+=1
|
||||
done
|
||||
|
||||
args[vm]=''
|
||||
newArgs=( ) ; for arg in "${!args[@]}" ; do newArgs+=( --"$arg"="${args[$arg]}" ) ; done
|
||||
devSpec= ; for name in "${!qemuDevs[@]}" ; do devSpec+="$name"="${qemuDevs[$name]}": ; done
|
||||
newArgs+=( ${devSpec%:} ) ; (( ${#argv[@]} > 1 )) && args+=( "${argv[@]:1}" )
|
||||
|
||||
#local output=@{inputs.self}'#'nixosConfigurations.@{outputName:?}.config.system.build.vmExec
|
||||
local output=@{config.system.build.vmExec.drvPath} # this is more accurate, but also means another system needs to get evaluated every time
|
||||
local command="$0 install-system $( printf '%q ' "${newArgs[@]}" ) || exit"
|
||||
|
||||
local runInVm ; runInVm=$( @{native.nix}/bin/nix --extra-experimental-features nix-command build --no-link --json ${args[quiet]:+--quiet} $output | @{native.jq}/bin/jq -r .[0].outputs.out )/bin/run-@{config.system.name}-vm-exec || return
|
||||
|
||||
$runInVm ${args[vm-shared]:+--shared="${args[vm-shared]}"} ${args[debug]:+--initrd-console} ${args[trace]:+--initrd-console} ${args[quiet]:+--quiet} -- "$command" "${qemu[@]}" || return # --initrd-console
|
||||
}
|
||||
|
||||
|
||||
## 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:@{native.nix}/bin:$PATH TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools}/bin/nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" || exit #--debug
|
||||
# »nixos-install« by default does some stateful things (see »--no-root-passwd« »--no-channel-copy«), builds and copies the system config, registers the system (»nix-env --profile /nix/var/nix/profiles/system --set $targetSystem«), 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:@{native.nix}/bin:$PATH TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools}/bin/nixos-install --system "$2" --no-root-passwd --no-channel-copy --root "$1" || exit # We did most of this, so just install the bootloader:
|
||||
|
||||
export NIXOS_INSTALL_BOOTLOADER=1 # tells some bootloader installers (systemd & grub) to not skip parts of the installation
|
||||
@{native.nixos-install-tools}/bin/nixos-enter --silent --root "$1" -- @{config.system.build.installBootLoader} "$2" || exit
|
||||
)}
|
||||
|
||||
## 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.
|
||||
@ -79,11 +116,6 @@ function install-system-to {( set -u # 1: mnt
|
||||
chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit
|
||||
fi
|
||||
|
||||
# Set this as the initial system generation (just in case »nixos-install-cmd« won't):
|
||||
mkdir -p -m 755 $mnt/nix/var/nix/profiles || exit
|
||||
[[ -e $mnt/nix/var/nix/profiles/system-1-link ]] || ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || exit
|
||||
[[ -e $mnt/nix/var/nix/profiles/system ]] || ln -sT system-1-link $mnt/nix/var/nix/profiles/system || exit
|
||||
|
||||
# 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.wip.preface.hardware}"-linux ]] ; then
|
||||
mkdir -p $mnt/run/binfmt || exit ; [[ ! -e /run/binfmt/"@{config.wip.preface.hardware}"-linux ]] || cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux || exit
|
||||
@ -92,15 +124,25 @@ function install-system-to {( set -u # 1: mnt
|
||||
|
||||
# Copy system closure to new nix store:
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi
|
||||
(
|
||||
cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
|
||||
if [[ ${args[quiet]:-} ]] ; then
|
||||
( set -o pipefail ; "${cmd[@]}" --quiet 2>&1 >/dev/null | { grep -Pe '^error:' || true ; } ) || exit
|
||||
else set -x ; time "${cmd[@]}" || exit ; fi
|
||||
) || exit ; rm -rf $mnt/nix/var/nix/gcroots || exit
|
||||
cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
|
||||
if [[ ${args[quiet]:-} ]] ; then
|
||||
"${cmd[@]}" --quiet >/dev/null 2> >( grep -Pe '^error:' || true ) || exit
|
||||
elif [[ ${args[quiet]:-} ]] ; then
|
||||
( set -x ; time "${cmd[@]}" ) || exit
|
||||
else
|
||||
( set -x ; "${cmd[@]}" ) || exit
|
||||
fi
|
||||
rm -rf $mnt/nix/var/nix/gcroots || exit
|
||||
# TODO: if the target has @{config.nix.settings.auto-optimise-store} and the host doesn't (there is no .links dir?), optimize now
|
||||
if [[ ${SUDO_USER:-} ]] ; then chown -R root:root $mnt/nix $mnt/nix/var || exit ; chown :30000 $mnt/nix/store || exit ; fi
|
||||
|
||||
# Set this as the initial system generation (in case »nixos-install-cmd« won't):
|
||||
# (does about the same as »nix-env --profile /nix/var/nix/profiles/system --set $targetSystem«)
|
||||
mkdir -p -m 755 $mnt/nix/var/nix/{profiles,gcroots}/per-user/root/ || exit
|
||||
ln -sT $(realpath $targetSystem) $mnt/nix/var/nix/profiles/system-1-link || exit
|
||||
ln -sT system-1-link $mnt/nix/var/nix/profiles/system || exit
|
||||
ln -sT /nix/var/nix/profiles $mnt/nix/var/nix/gcroots/profiles || exit
|
||||
|
||||
# Run the main install command (primarily for the bootloader):
|
||||
mount -o bind,ro /nix/store $mnt/nix/store || exit ; prepend_trap '! mountpoint -q $mnt/nix/store || umount -l $mnt/nix/store' EXIT || exit # all the things required to _run_ the system are copied, but (may) need some more things to initially install it and/or enter the chroot (like qemu, see above)
|
||||
run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands} || exit
|
||||
@ -109,18 +151,17 @@ function install-system-to {( set -u # 1: mnt
|
||||
|
||||
# Done!
|
||||
if [[ ${args[no-inspect]:-} ]] ; then
|
||||
if (( code != 0 )) ; then exit $code ; fi
|
||||
if (( code != 0 )) ; then \exit $code ; fi
|
||||
elif [[ ${args[inspect-cmd]:-} ]] ; then
|
||||
if (( code != 0 )) ; then exit $code ; fi
|
||||
if (( code != 0 )) ; then \exit $code ; fi
|
||||
eval "${args[inspect-cmd]}" || exit
|
||||
else
|
||||
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." 1>&2 )
|
||||
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." 1>&2 )
|
||||
( set +x ; echo "[1;32mInstallation done![0m This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." 1>&2 )
|
||||
fi
|
||||
PATH=@{config.systemd.package}/bin:$PATH @{native.nixos-install-tools}/bin/nixos-enter --root $mnt || exit # TODO: construct path as it would be at login
|
||||
#( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} )
|
||||
PATH=@{config.systemd.package}/bin:$PATH @{native.nixos-install-tools}/bin/nixos-enter --root $mnt -- /nix/var/nix/profiles/system/sw/bin/bash --login || exit # +o monitor
|
||||
fi
|
||||
|
||||
mkdir -p $mnt/var/lib/systemd/timesync && touch $mnt/var/lib/systemd/timesync/clock || true # save current time
|
||||
|
@ -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 return 1 ; fi
|
||||
if ! userPasswords[$user]=$(prompt-new-password "for the user account »$user«") ; then true ; \return 1 ; fi
|
||||
done
|
||||
}
|
||||
|
||||
@ -37,37 +37,37 @@ function populate-keystore { { # (void)
|
||||
if [[ "${methods[$usage]}" == home-composite || "${methods[$usage]}" == copy ]] ; then continue ; fi
|
||||
for attempt in 2 3 x ; do
|
||||
if gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key ; then break ; fi
|
||||
if [[ $attempt == x ]] ; then return 1 ; fi ; echo "Retrying ($attempt/3):"
|
||||
if [[ $attempt == x ]] ; then \exit 1 ; fi ; echo "Retrying ($attempt/3):"
|
||||
done
|
||||
done
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" != home-composite ]] ; then continue ; fi
|
||||
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || return 1
|
||||
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || \exit 1
|
||||
done
|
||||
for usage in "${!methods[@]}" ; do
|
||||
if [[ "${methods[$usage]}" != copy ]] ; then continue ; fi
|
||||
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || return 1
|
||||
gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key || \exit 1
|
||||
done
|
||||
)}
|
||||
|
||||
|
||||
## Creates the LUKS devices specified by the host using the keys created by »populate-keystore«.
|
||||
function create-luks-layers {( set -eu # (void)
|
||||
function create-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]}
|
||||
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
|
||||
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[@]}" 1>&2 ; \return 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" )
|
||||
( PATH=@{native.cryptsetup}/bin ; ${_set_x:-:} ; cryptsetup --batch-mode luksFormat --key-file="$primaryKey" "${keyOptions[@]}" -c aes-xts-plain64 -s 512 -h sha256 "$rawDev" ) || return
|
||||
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 )
|
||||
( PATH=@{native.cryptsetup}/bin ; ${_set_x:-:} ; cryptsetup luksAddKey --key-file="$primaryKey" "${keyOptions[@]}" "$rawDev" "$keystore"/luks/"$luksName"/"$index".key ) || return
|
||||
fi
|
||||
done
|
||||
done
|
||||
)}
|
||||
}
|
||||
|
||||
## Opens the LUKS devices specified by the host, using the opened host's keystore.
|
||||
function open-luks-layers { # (void)
|
||||
@ -76,8 +76,7 @@ function open-luks-layers { # (void)
|
||||
if [[ -e /dev/mapper/$luksName ]] ; then continue ; fi
|
||||
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
|
||||
@{native.cryptsetup}/bin/cryptsetup --batch-mode luksOpen --key-file="$primaryKey" "$rawDev" "$luksName" || return
|
||||
prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $luksName" EXIT || return
|
||||
done
|
||||
}
|
||||
|
@ -47,52 +47,78 @@ function register-vbox {( set -eu # 1: diskImages, 2?: bridgeTo
|
||||
|
||||
## 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
|
||||
function run-qemu { # 1: diskImages, ...: qemuArgs
|
||||
if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{outputName:-@{config.system.name}}/ ; fi
|
||||
diskImages=${argv[0]:?} ; argv=( "${argv[@]:1}" )
|
||||
|
||||
qemu=( @{native.qemu_full}/bin/qemu-system-@{config.wip.preface.hardware} )
|
||||
qemu+=( -m ${args[mem]:-2048} -smp ${args[smp]:-4} )
|
||||
local qemu=( )
|
||||
|
||||
if [[ @{config.wip.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.wip.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
|
||||
if [[ @{pkgs.system} == "@{native.system}" ]] ; then
|
||||
qemu=( $( @{native.nix}/bin/nix --extra-experimental-features nix-command build --no-link --json @{native.qemu_kvm.drvPath} | @{native.jq}/bin/jq -r .[0].outputs.out )/bin/qemu-kvm ) || return
|
||||
if [[ ! ${args[no-kvm]:-} && -r /dev/kvm && -w /dev/kvm ]] ; then
|
||||
# For KVM to work, vBox must 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.
|
||||
qemu+=( -enable-kvm -cpu host )
|
||||
if [[ ! ${args[smp]:-} ]] ; then qemu+=( -smp 4 ) ; fi # else the emulation is single-threaded anyway
|
||||
else
|
||||
if [[ ! ${args[no-kvm]:-} && ! ${args[quiet]:-} ]] ; then
|
||||
echo "KVM is not available (for the current user). Running without hardware acceleration." 1>&2
|
||||
fi
|
||||
qemu+=( -machine accel=tcg ) # this may suppress warnings that qemu is using tcg (slow) instead of kvm
|
||||
if [[ @{pkgs.system} == aarch64-* ]] ; then qemu+=( -cpu max ) ; fi
|
||||
fi
|
||||
if [[ @{pkgs.system} == aarch64-* ]] ; then
|
||||
qemu+=( -machine type=virt -cpu max ) # aarch64 has no default, but this seems good
|
||||
fi
|
||||
else
|
||||
qemu=( $( @{native.nix}/bin/nix --extra-experimental-features nix-command build --no-link --json @{native.qemu_full.drvPath} | @{native.jq}/bin/jq -r .[0].outputs.out )/bin/qemu-system-@{config.wip.preface.hardware} ) || return
|
||||
if [[ @{config.wip.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+=( -machine type=virt -m 1024 -smp 4 -cpu cortex-a53 ) ; args[no-nat]=1
|
||||
fi
|
||||
fi
|
||||
|
||||
qemu+=( -m ${args[mem]:-2048} )
|
||||
if [[ ${args[smp]:-} ]] ; then qemu+=( -smp ${args[smp]} ) ; fi
|
||||
|
||||
if [[ @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then # UEFI. Otherwise it boots SeaBIOS.
|
||||
local ovmf ; ovmf=$( @{native.nix}/bin/nix --extra-experimental-features nix-command build --no-link --json @{native.OVMF.drvPath} | @{native.jq}/bin/jq -r .[0].outputs.fd ) || return
|
||||
#qemu+=( -bios ${ovmf}/FV/OVMF.fd ) # This works, but is a legacy fallback that stores the EFI vars in /NvVars on the EFI partition (which is really bad).
|
||||
local fwName=OVMF ; if [[ @{pkgs.system} == aarch64-* ]] ; then fwName=AAVMF ; fi # fwName=QEMU
|
||||
qemu+=( -drive file=${ovmf}/FV/${fwName}_CODE.fd,if=pflash,format=raw,unit=0,readonly=on )
|
||||
local efiVars=${args[efi-vars]:-${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qemu-@{outputName:-@{config.system.name}}-VARS.fd}
|
||||
qemu+=( -drive file="$efiVars",if=pflash,format=raw,unit=1 )
|
||||
if [[ ! -e "$efiVars" ]] ; then cat ${ovmf}/FV/${fwName}_VARS.fd >"$efiVars" || return ; fi
|
||||
# https://lists.gnu.org/archive/html/qemu-discuss/2018-04/msg00045.html
|
||||
fi
|
||||
# if [[ @{config.wip.preface.hardware} == aarch64 ]] ; then
|
||||
# qemu+=( -kernel @{config.system.build.kernel}/Image -initrd @{config.system.build.initialRamdisk}/initrd -append "$(echo -n "@{config.boot.kernelParams[@]}")" )
|
||||
# fi
|
||||
|
||||
if [[ $diskImages == */ ]] ; then
|
||||
disks=( ${diskImages}primary.img ) ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do if [[ $name != primary ]] ; then disks+=( ${diskImages}${name}.img ) ; fi ; done
|
||||
#disks=( "@{!config.wip.fs.disks.devices[@]}" ) ; disks=( "${disks[@]/#/$diskImages}" )
|
||||
else disks=( ${diskImages//:/ } ) ; fi
|
||||
for index in ${!disks[@]} ; do
|
||||
local index ; for index in ${!disks[@]} ; do
|
||||
# qemu+=( -drive format=raw,if=ide,file="${disks[$index]/*=/}" ) # »if=ide« is the default, which these days isn't great for driver support inside the VM
|
||||
qemu+=( # not sure how correct the interpretations if the command are, and whether this works for more than one disk
|
||||
qemu+=( # not sure how correct the interpretations of the command are
|
||||
-drive format=raw,file="${disks[$index]/*=/}",media=disk,if=none,index=${index},id=drive${index} # create the disk drive, without attaching it, name it driveX
|
||||
-device ahci,acpi-index=${index},id=ahci${index} # create an (ich9-)AHCI bus named »ahciX«
|
||||
-device ide-hd,drive=drive${index},bus=ahci${index}.${index} # attach IDE?! disk driveX as device X on bus »ahciX«
|
||||
#-device virtio-blk-pci,drive=drive${index},disable-modern=on,disable-legacy=off # alternative to the two lines above (implies to be faster, but seems to require guest drivers)
|
||||
#-device ahci,acpi-index=${index},id=ahci${index} # create an (ich9-)AHCI bus named »ahciX«
|
||||
#-device ide-hd,drive=drive${index},bus=ahci${index}.${index} # attach IDE?! disk driveX as device X on bus »ahciX«
|
||||
-device virtio-blk-pci,drive=drive${index},disable-modern=on,disable-legacy=off # alternative to the two lines above (implies to be faster, but seems to require guest drivers)
|
||||
)
|
||||
done
|
||||
|
||||
if [[ @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then # UEFI. Otherwise it boots something much like a classic BIOS?
|
||||
ovmf=$( @{native.nixVersions.nix_2_9}/bin/nix --extra-experimental-features 'nix-command flakes' build --no-link --print-out-paths @{inputs.nixpkgs}'#'legacyPackages.@{pkgs.system}.OVMF.fd )
|
||||
#qemu+=( -bios ${ovmf}/FV/OVMF.fd ) # This works, but is a legacy fallback that stores the EFI vars in /NvVars on the EFI partition (which is really bad).
|
||||
qemu+=( -drive file=${ovmf}/FV/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on )
|
||||
qemu+=( -drive file="${args[efi-vars]:-/tmp/qemu-@{config.networking.hostName}-VARS.fd}",if=pflash,format=raw,unit=1 )
|
||||
if [[ ! -e "${args[efi-vars]:-/tmp/qemu-@{config.networking.hostName}-VARS.fd}" ]] ; then cat ${ovmf}/FV/OVMF_VARS.fd > "${args[efi-vars]:-/tmp/qemu-@{config.networking.hostName}-VARS.fd}" ; fi
|
||||
# https://lists.gnu.org/archive/html/qemu-discuss/2018-04/msg00045.html
|
||||
fi
|
||||
if [[ @{config.wip.preface.hardware} == aarch64 ]] ; then
|
||||
qemu+=( -kernel @{config.system.build.kernel}/Image -initrd @{config.system.build.initialRamdisk}/initrd -append "$(echo -n "@{config.boot.kernelParams[@]}")" )
|
||||
if [[ ${args[share]:-} ]] ; then # e.g. --share='foo:/home/user/foo,readonly=on bar:/tmp/bar'
|
||||
local share ; for share in ${args[share]} ; do
|
||||
qemu+=( -virtfs local,security_model=none,mount_tag=${share/:/,path=} )
|
||||
# In the VM: $ mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 -o ro foo /foo
|
||||
done
|
||||
fi
|
||||
|
||||
# Add »config.boot.kernelParams = [ "console=tty1" "console=ttyS0" ]« to log to serial (»ttyS0«) and/or the display (»tty1«), preferring the last »console« option for the initrd shell (if enabled and requested).
|
||||
logSerial= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=ttyS0'@( |,)* ]] ; then logSerial=1 ; fi
|
||||
logScreen= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=tty1 '* ]] ; then logScreen=1 ; fi
|
||||
local logSerial= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=ttyS0'@( |,)* ]] ; then logSerial=1 ; fi
|
||||
local logScreen= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=tty1 '* ]] ; then logScreen=1 ; fi
|
||||
if [[ ! ${args[no-serial]:-} && $logSerial ]] ; then
|
||||
if [[ $logScreen || ${args[graphic]:-} ]] ; then
|
||||
qemu+=( -serial mon:stdio )
|
||||
@ -101,8 +127,8 @@ function run-qemu {( set -eu # 1: diskImages
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=8000-:8000,8001-:8001,2022-:22
|
||||
qemu+=( -nic user,model=virtio-net-pci${args[nat-fw]:+,hostfwd=tcp::${args[nat-fw]//,/,hostfwd=tcp::}} ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2
|
||||
if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=:8000-:8000,:8001-:8001,127.0.0.1:2022-:22
|
||||
qemu+=( -nic user,model=virtio-net-pci${args[nat-fw]:+,hostfwd=tcp:${args[nat-fw]//,/,hostfwd=tcp:}} ) # NATed, IPs: 10.0.2.15+/32, gateway: 10.0.2.2
|
||||
fi
|
||||
|
||||
# TODO: network bridging:
|
||||
@ -110,19 +136,27 @@ function run-qemu {( set -eu # 1: diskImages
|
||||
#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
|
||||
if [[ ${args[usb-port]:-} ]] ; then local decl ; for decl in ${args[usb-port]//:/ } ; do
|
||||
qemu+=( -usb -device usb-host,hostbus="${decl/-*/}",hostport="${decl/*-/}" )
|
||||
done ; fi
|
||||
|
||||
if [[ ${args[install]:-} == 1 ]] ; then local disk ; for disk in "${disks[@]}" ; do
|
||||
if [[ ! -e $disk ]] ; then args[install]=always ; fi
|
||||
done ; fi
|
||||
if [[ ${args[install]:-} == always ]] ; then
|
||||
local verbosity=--quiet ; if [[ ${args[debug]:-} ]] ; then verbosity=--debug ; fi
|
||||
${args[dry-run]:+echo} $0 install-system "$diskImages" $verbosity --no-inspect || return
|
||||
fi
|
||||
|
||||
qemu+=( "${argv[@]}" )
|
||||
if [[ ${args[dry-run]:-} ]] ; then
|
||||
( echo "${qemu[@]}" )
|
||||
echo "${qemu[@]}"
|
||||
else
|
||||
( set -x ; "${qemu[@]}" )
|
||||
( set -x ; "${qemu[@]}" ) || return
|
||||
fi
|
||||
|
||||
# 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"
|
||||
|
@ -10,7 +10,7 @@
|
||||
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 shift ; argv+=( "$@" ) ; \return 0 ; fi
|
||||
if [[ $1 == --* ]] ; then
|
||||
if [[ $1 == *=* ]] ; then
|
||||
local key=${1/=*/} ; args[${key/--/}]=${1/$key=/}
|
||||
@ -25,15 +25,15 @@ function generic-arg-parse { # ...
|
||||
# »name« should be the program name/path (usually »$0«), »args« the form/names of any positional arguments expected (e.g. »SOURCE... DEST«) and is included in the "Usage" description,
|
||||
# »description« the introductory text shown before the "Usage", and »suffix« any text printed after the argument list.
|
||||
function generic-arg-help { # 1: name, 2?: args, 3?: description, 4?: suffix
|
||||
if [[ ! ${args[help]:-} ]] ; then : ${allowedArgs[help]:=1} ; return 0 ; fi
|
||||
if [[ ! ${args[help]:-} ]] ; then : ${allowedArgs[help]:=1} ; \return 0 ; fi
|
||||
[[ ! ${3:-} ]] || echo "$3"
|
||||
printf 'Usage:\n %s [ARG[=value]]... [--] %s\n\nWhere »ARG« may be any of:\n' "$1" "${2:-}"
|
||||
local name ; while IFS= read -r name ; do
|
||||
local name ; while IFS= read -u3 -r name ; do
|
||||
printf ' %s\n %s\n' "$name" "${allowedArgs[$name]}"
|
||||
done < <( printf '%s\n' "${!allowedArgs[@]}" | LC_ALL=C sort )
|
||||
done 3< <( printf '%s\n' "${!allowedArgs[@]}" | LC_ALL=C sort )
|
||||
printf ' %s\n %s\n' "--help" "Do nothing but print this message and exit with success."
|
||||
[[ ! ${4:-} ]] || echo "$4"
|
||||
exit 0
|
||||
\exit 0
|
||||
}
|
||||
|
||||
## Performs a basic verification of the named arguments passed by the user and parsed by »generic-arg-parse« against the names in »allowedArgs«.
|
||||
@ -46,22 +46,23 @@ function generic-arg-verify { # 1: exitCode
|
||||
for name in "${!args[@]}" ; do
|
||||
if [[ ${allowedArgs[--$name]:-} ]] ; then
|
||||
if [[ ${args[$name]} == '' || ${args[$name]} == 1 ]] ; then continue ; fi
|
||||
echo "Argument »--$name« should be a boolean, but its value is: ${args[$name]}" ; return $exitCode
|
||||
echo "Argument »--$name« should be a boolean, but its value is: ${args[$name]}" 1>&2 ; \return $exitCode
|
||||
fi
|
||||
if [[ $names == *' --'"$name"'='* || $names == *' --'"$name"'[='* ]] ; then continue ; fi
|
||||
echo "Unexpected argument »--$name«.${allowedArgs[help]:+ Call with »--help« for a list of valid arguments.}" ; return $exitCode
|
||||
echo "Unexpected argument »--$name«.${allowedArgs[help]:+ Call with »--help« for a list of valid arguments.}" 1>&2 ; \return $exitCode
|
||||
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)).
|
||||
function prepend_trap { # 1: command, ...: trapNames
|
||||
fatal() { printf "ERROR: $@\n" >&2 ; return 1 ; }
|
||||
local cmd=$1 ; shift || fatal "${FUNCNAME} usage error"
|
||||
fatal() { printf "ERROR: $@\n" 1>&2 ; \return 1 ; }
|
||||
local cmd=$1 ; shift 1 || fatal "${FUNCNAME} usage error"
|
||||
local name ; for name in "$@" ; do
|
||||
trap -- "$( set +x
|
||||
printf '%s\n' "( ${cmd} ) || true ; "
|
||||
p3() { printf '%s\n' "${3:-}" ; } ; eval "p3 $(trap -p "${name}")"
|
||||
p3() { printf '%s\n' "${3:-}" ; }
|
||||
eval "p3 $(trap -p "${name}")"
|
||||
)" "${name}" || fatal "unable to add to trap ${name}"
|
||||
done
|
||||
}
|
||||
@ -69,7 +70,7 @@ declare -f -t prepend_trap # required to modify DEBUG or RETURN traps
|
||||
|
||||
## Given the name to an existing bash function, this creates a copy of that function with a new name (in the current scope).
|
||||
function copy-function { # 1: existingName, 2: newName
|
||||
local original=$(declare -f "${1?existingName not provided}") ; if [[ ! $original ]] ; then echo "Function $1 is not defined" ; return 1 ; fi
|
||||
local original=$(declare -f "${1?existingName not provided}") ; if [[ ! $original ]] ; then echo "Function $1 is not defined" 1>&2 ; \return 1 ; fi
|
||||
eval "${original/$1/${2?newName not provided}}" # run the code declaring the function again, replacing only the first occurrence of the name
|
||||
}
|
||||
|
||||
@ -78,7 +79,7 @@ function write-secret {( set -u # 1: path, 2?: owner[:[group]], 3?: mode
|
||||
mkdir -p -- "$(dirname "$1")"/ || exit
|
||||
install -o root -g root -m 000 -T /dev/null "$1" || exit
|
||||
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 ...
|
||||
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" || exit
|
||||
chmod "${3:-400}" -- "$1" || exit
|
||||
)}
|
||||
@ -87,7 +88,7 @@ function write-secret {( set -u # 1: path, 2?: owner[:[group]], 3?: mode
|
||||
function prompt-new-password {( set -u # 1: usage
|
||||
read -s -p "Please enter the new password $1: " password1 || exit ; echo 1>&2
|
||||
read -s -p "Please enter the same password again: " password2 || exit ; echo 1>&2
|
||||
if (( ${#password1} == 0 )) || [[ "$password1" != "$password2" ]] ; then printf 'Passwords empty or mismatch, aborting.\n' 1>&2 ; exit 1 ; fi
|
||||
if (( ${#password1} == 0 )) || [[ "$password1" != "$password2" ]] ; then printf 'Passwords empty or mismatch, aborting.\n' 1>&2 ; \exit 1 ; fi
|
||||
printf %s "$password1" || exit
|
||||
)}
|
||||
|
||||
|
@ -9,31 +9,32 @@ function create-zpools { # 1: mnt
|
||||
|
||||
## Creates a single of the system's ZFS pools and its datasets.
|
||||
function create-zpool { # 1: mnt, 2: poolName
|
||||
local mnt=$1 ; local poolName=$2 ; ( set -u
|
||||
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=( ) ; keySrc=/dev/null
|
||||
if [[ ${dataProps[keyformat]:-} == ephemeral ]] ; then
|
||||
dataProps[encryption]=aes-256-gcm ; dataProps[keyformat]=hex ; dataProps[keylocation]=file:///dev/stdin ; keySrc=/dev/urandom
|
||||
local mnt=$1 ; local poolName=$2
|
||||
eval 'local -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
|
||||
eval 'local -a vdevs='"${pool[vdevArgs]}"
|
||||
eval 'local -A poolProps='"${pool[props]}"
|
||||
eval 'local -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
|
||||
eval 'local -A dataProps='"${dataset[props]}"
|
||||
local dummy ; get-zfs-crypt-props "${dataset[name]}" dataProps dummy dummy
|
||||
local -a zpoolCreate=( ) ; keySrc=/dev/null
|
||||
if [[ ${dataProps[keyformat]:-} == ephemeral ]] ; then
|
||||
dataProps[encryption]=aes-256-gcm ; dataProps[keyformat]=hex ; dataProps[keylocation]=file:///dev/stdin ; keySrc=/dev/urandom
|
||||
fi
|
||||
local name ; for name in "${!poolProps[@]}" ; do zpoolCreate+=( -o "${name}=${poolProps[$name]}" ) ; done
|
||||
local name ; for name in "${!dataProps[@]}" ; do zpoolCreate+=( -O "${name}=${dataProps[$name]}" ) ; done
|
||||
local index ; 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[@]}" 1>&2 ; \return 1 ; fi
|
||||
fi
|
||||
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
|
||||
<$keySrc tr -dc 0-9a-f | head -c 64 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${args[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" || exit ) || exit
|
||||
if [[ $keySrc == /dev/urandom ]] ; then @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null ; fi
|
||||
) || return
|
||||
done
|
||||
@{native.kmod}/bin/modprobe zfs || true
|
||||
<$keySrc @{native.xxd}/bin/xxd -l 32 -c 64 -p | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create "${zpoolCreate[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" ) || return
|
||||
if [[ $keySrc == /dev/urandom ]] ; then @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null ; fi
|
||||
|
||||
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
|
||||
ensure-datasets $mnt '^'"$poolName"'($|[/])' || return
|
||||
if [[ ${args[debug]:-} ]] ; then @{native.zfs}/bin/zfs list -o name,canmount,mounted,mountpoint,keystatus,encryptionroot -r "$poolName" ; fi
|
||||
@ -43,26 +44,26 @@ function create-zpool { # 1: mnt, 2: poolName
|
||||
# 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)
|
||||
filterExp=${2:-'^'}
|
||||
tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
|
||||
zfs=@{native.zfs}/bin/zfs
|
||||
if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then \return ; fi
|
||||
local mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes)
|
||||
local filterExp=${2:-'^'}
|
||||
local tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
|
||||
local zfs=@{native.zfs}/bin/zfs
|
||||
|
||||
: '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
|
||||
local name ; while IFS= read -u3 -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]}"
|
||||
eval 'local -A dataset='"@{config.wip.fs.zfs.datasets[$name]}"
|
||||
eval 'local -A props='"${dataset[props]}"
|
||||
|
||||
explicitKeylocation=${props[keylocation]:-}
|
||||
local explicitKeylocation=${props[keylocation]:-} cryptKey cryptRoot
|
||||
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/}
|
||||
local 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
|
||||
@ -70,51 +71,52 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
|
||||
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 (
|
||||
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( "${name}=${props[$name]}" ) ; done
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs set "${args[@]}" "${dataset[name]}" )
|
||||
) ; fi
|
||||
|
||||
local propNames=$( IFS=, ; echo "${!props[*]}" )
|
||||
local propValues=$( IFS=$'\n' ; echo "${props[*]}" )
|
||||
if [[ $propValues != "$( $zfs get -o value -H "$propNames" "${dataset[name]}" )" ]] ; then
|
||||
local -a zfsSet=( ) ; local propName ; for propName in "${!props[@]}" ; do zfsSet+=( "${propName}=${props[$propName]}" ) ; done
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs set "${zfsSet[@]}" "${dataset[name]}" ) || return
|
||||
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)
|
||||
if [[ $($zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then
|
||||
$zfs load-key -L file://"$cryptKey" "$cryptRoot" ; trap "$zfs unload-key $cryptRoot || true" EXIT
|
||||
$zfs load-key -L file://"$cryptKey" "$cryptRoot" || exit ; 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 cryptRoot
|
||||
$zfs load-key -L file://"$cryptKey" "${dataset[name]}" || exit # will unload with cryptRoot
|
||||
fi
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs change-key -i "${dataset[name]}" )
|
||||
) ; fi
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs change-key -i "${dataset[name]}" ) || exit
|
||||
) || return ; 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 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${args[@]}" "${dataset[name]}" )
|
||||
$zfs unload-key "${dataset[name]}"
|
||||
declare -a zfsCreate=( ) ; for name in "${!props[@]}" ; do zfsCreate+=( -o "${name}=${props[$name]}" ) ; done
|
||||
{ </dev/urandom tr -dc 0-9a-f || true ; } | head -c 64 | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${zfsCreate[@]}" "${dataset[name]}" ) || exit
|
||||
$zfs unload-key "${dataset[name]}" || exit
|
||||
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
|
||||
$zfs load-key -L file://"$cryptKey" "$cryptRoot" || exit
|
||||
trap "$zfs unload-key $cryptRoot || true" EXIT
|
||||
fi
|
||||
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${args[@]}" "${dataset[name]}" )
|
||||
declare -a zfsCreate=( ) ; for name in "${!props[@]}" ; do zfsCreate+=( -o "${name}=${props[$name]}" ) ; done
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${zfsCreate[@]}" "${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
|
||||
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"
|
||||
) || exit ; fi
|
||||
if [[ $explicitKeylocation && $explicitKeylocation != "${props[keylocation]:-}" ]] ; then
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" )
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs set keylocation="$explicitKeylocation" "${dataset[name]}" ) || exit
|
||||
fi
|
||||
$zfs snapshot -r "${dataset[name]}"@empty
|
||||
) ; fi
|
||||
$zfs snapshot -r "${dataset[name]}"@empty || exit
|
||||
) || return ; fi
|
||||
|
||||
eval 'declare -A allows='"${dataset[permissions]}"
|
||||
eval 'local -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 -.-
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 )
|
||||
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 ) || return
|
||||
done
|
||||
done
|
||||
done 3< <( printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z )
|
||||
|
||||
)}
|
||||
|
||||
|
Reference in New Issue
Block a user