mirror of
https://github.com/NiklasGollenstede/nixos-installer.git
synced 2025-08-16 19:57:05 +02:00
refactoring: create separate repo for "installer"
This commit is contained in:
@ -3,42 +3,22 @@
|
||||
|
||||
This is a library of bash functions, mostly for NixOS system installation.
|
||||
|
||||
The (paths to these) scripts are meant to be (and by default are) set as `config.wip.setup.scripts.*` (see [`../flakes.nix`](../flakes.nix)), which makes their functions available in the per-host [`devShells`/`apps`](../flakes.nix#mkSystemsFlake).
|
||||
The (paths to these) scripts are meant to be (and are by default when importing [`../../modules/installer.nix.md`](../../modules/installer.nix.md)) set as `config.installer.scripts.*`.
|
||||
[`mkSystemsFlake`](../nixos.nix#mkSystemsFlake) then 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, [a simple three-liner](./install.sh) is enough to do a completely automated NixOS installation:
|
||||
```bash
|
||||
function install-system {( # 1: diskPaths
|
||||
prepare-installer "$@" || exit
|
||||
do-disk-setup "${argv[0]}" || exit
|
||||
install-system-to $mnt || exit
|
||||
)}
|
||||
Any script passed later in `scripts` [can override](../../example/install.sh.md#implementation) the functions of these (earlier) default scripts, e.g.:
|
||||
```nix
|
||||
{ config.installer.scripts.override = { path = .../override.sh; order = 1500; }; }
|
||||
```
|
||||
|
||||
|
||||
# `install-system` Documentation
|
||||
|
||||
For repositories that use the `lib.wip.mkSystemsFlake` Nix function in their `flake.nix`, the above bash function performs the automated installation of any `nixosConfigurations.$HOST`s (where the host's configurations would usually be placed in the `/hosts/` directory of the repository) to the local disk(s) (or image file(s)) `$DISK`.
|
||||
On a NixOS host or with a Nix multi-user installation, this can be run by root as: `#` `nix run .#"$HOST" -- install-system "$DISK"`.
|
||||
|
||||
Doing an installation on non-NixOS (but Linux), where nix isn't installed for root, the process is a bit of a hack, but works as well.
|
||||
In this case, all `nix` commands will be run as `$SUDO_USER`, but this script and some other user-owned (or user-generated) code will (need to) be run as root.
|
||||
If that is acceptable, run with `sudo` as first argument: `$` `nix run .#"$HOST" -- sudo install-system "$DISK"` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.)
|
||||
|
||||
If `$DISK` points to something in `/dev/`, then it is directly formatted and written to as block device, otherwise `$DISK` is (re-)created as raw image and then used as loop device.
|
||||
For hosts that install to multiple disks, pass a `:`-separated list of `<disk-name>=<path>` pairs (the name may be omitted only for the "`default`" disk).
|
||||
|
||||
Once done, the disk can be transferred -- or the image be copied -- to the final system, and should boot there.
|
||||
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.
|
||||
See `nix run .#$HOST -- --help` to see how to use the result.
|
||||
|
||||
|
||||
## 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).
|
||||
* When adding functions that are meant to be called as top-level `COMMAND`s, make sure to document them by calling `declare-command`. See esp. [`maintenance.sh`](./maintenance.sh) for examples. Similarly, use `declare-flag` to add new flags to the `--help` output.
|
||||
* 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).
|
||||
|
@ -8,13 +8,13 @@
|
||||
|
||||
## Outputs nothing (/ an empty key), causing that ZFS dataset to be unencrypted, even if it's parent is encrypted.
|
||||
function gen-key-unencrypted {( set -eu # 1: usage
|
||||
:
|
||||
: # TODO: write-secret does not allow empty secrets anymore (might want to change that back)
|
||||
)}
|
||||
|
||||
## 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 'The trivial »hostname« key mode is only available for the keystore itself.\n' 1>&2 ; \exit 1 ; fi
|
||||
printf %s "@{config.networking.hostName}"
|
||||
)}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
dirname: inputs: let
|
||||
|
||||
inherit (inputs.config) prefix;
|
||||
inherit (import "${dirname}/../imports.nix" dirname inputs) getFilesExt;
|
||||
inherit (inputs.config.rename) setup installer;
|
||||
|
||||
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
|
||||
doRenames = if setup == "setup" && installer == "installer" then (x: x) else (builtins.mapAttrs (name: path: (
|
||||
builtins.toFile name (builtins.replaceStrings
|
||||
[ "@{config.wip." "@{#config.wip." "@{!config.wip." ]
|
||||
[ "@{config.${prefix}." "@{#config.${prefix}." "@{!config.${prefix}." ]
|
||||
[ "@{config.setup." "@{#config.setup." "@{!config.setup." "@{config.installer." "@{#config.installer." "@{!config.installer." ]
|
||||
[ "@{config.${setup}." "@{#config.${setup}." "@{!config.${setup}." "@{config.${installer}." "@{#config.${installer}." "@{!config.${installer}." ]
|
||||
(builtins.readFile path)
|
||||
)
|
||||
)));
|
||||
|
||||
in replacePrefix (getFilesExt "sh(.md)?" dirname)
|
||||
in doRenames (inputs.functions.lib.getFilesExt "sh(.md)?" dirname)
|
||||
|
@ -14,16 +14,16 @@ function do-disk-setup { # 1: diskPaths
|
||||
|
||||
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
|
||||
run-hook-script 'Post Partitioning' @{config.installer.commands.postPartition!writeText.postPartitionCommands} || return
|
||||
|
||||
format-partitions || return
|
||||
if [[ $(LC_ALL=C type -t create-zpools) == function ]] ; then create-zpools $mnt || return ; fi
|
||||
run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} || return
|
||||
run-hook-script 'Post Formatting' @{config.installer.commands.postFormat!writeText.postFormatCommands} || return
|
||||
|
||||
fix-grub-install || return
|
||||
|
||||
prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt || return
|
||||
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} || return
|
||||
run-hook-script 'Post Mounting' @{config.installer.commands.postMount!writeText.postMountCommands} || return
|
||||
}
|
||||
|
||||
# Notes on segmentation and alignment:
|
||||
@ -34,14 +34,15 @@ function do-disk-setup { # 1: diskPaths
|
||||
# * (source: https://lwn.net/Articles/428584/)
|
||||
# * So alignment at the default »align=8MiB« actually seems a decent choice.
|
||||
|
||||
declare-flag install-system image-owner "" "When using image files, »chown« them to this »owner[:group]« before the installation."
|
||||
|
||||
## 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.
|
||||
## Parses and expands »diskPaths« to ensure that a disk or image exists for each »config.setup.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
|
||||
mkdir -p "$1"
|
||||
for name in "@{!config.wip.fs.disks.devices[@]}" ; do blockDevs[$name]=${1}${name}.img ; done
|
||||
for name in "@{!config.setup.disks.devices[@]}" ; do blockDevs[$name]=${1}${name}.img ; done
|
||||
else
|
||||
local path ; for path in ${1//:/ } ; do
|
||||
local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
|
||||
@ -50,10 +51,10 @@ function ensure-disks { # 1: diskPaths, 2?: skipLosetup
|
||||
done
|
||||
fi
|
||||
|
||||
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do
|
||||
if [[ ! @{config.wip.fs.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
|
||||
local name ; for name in "@{!config.setup.disks.devices[@]}" ; do
|
||||
if [[ ! @{config.setup.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; 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]}"
|
||||
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
|
||||
if [[ ${blockDevs[$name]} != /dev/* ]] ; then
|
||||
local outFile=${blockDevs[$name]} &&
|
||||
install -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" || return
|
||||
@ -72,36 +73,36 @@ function ensure-disks { # 1: diskPaths, 2?: skipLosetup
|
||||
}
|
||||
|
||||
|
||||
## Partitions the »blockDevs« (matching »config.wip.fs.disks.devices«) to ensure that all specified »config.wip.fs.disks.partitions« exist.
|
||||
## Partitions the »blockDevs« (matching »config.setup.disks.devices«) to ensure that all specified »config.setup.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
|
||||
for partDecl in "@{config.setup.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
|
||||
done
|
||||
|
||||
for name in "@{!config.wip.fs.disks.devices[@]}" ; do
|
||||
if [[ ! @{config.wip.fs.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
|
||||
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
for name in "@{!config.setup.disks.devices[@]}" ; do
|
||||
if [[ ! @{config.setup.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
|
||||
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
|
||||
if [[ ${disk[serial]:-} ]] ; then
|
||||
actual=$( @{native.systemd}/bin/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
|
||||
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 ) || return
|
||||
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.setup.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
|
||||
|
||||
# ensure that filesystem creation does not complain about the devices already being occupied by a previous filesystem
|
||||
local toWipe=( ) ; for part in "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do [[ ! -e "$part" ]] || toWipe+=( "$part" ) ; done
|
||||
local toWipe=( ) ; for part in "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do [[ ! -e "$part" ]] || toWipe+=( "$part" ) ; done
|
||||
@{native.util-linux}/bin/wipefs --all "${toWipe[@]}" >$beLoud 2>$beSilent || return
|
||||
#</dev/zero head -c 4096 | tee "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >/dev/null
|
||||
#for part in "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do @{native.util-linux}/bin/blkdiscard -f "$part" || return ; done
|
||||
#</dev/zero head -c 4096 | tee "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >/dev/null
|
||||
#for part in "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do @{native.util-linux}/bin/blkdiscard -f "$part" || return ; done
|
||||
}
|
||||
|
||||
## Given a declared disk device's »name« and a path to an actual »blockDev« (or image) file, partitions the device as declared in the config.
|
||||
@ -109,7 +110,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
|
||||
local name=$1 ; local blockDev=$2
|
||||
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
|
||||
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
|
||||
local devSize=${3:-$( @{native.util-linux}/bin/blockdev --getsize64 "$blockDev" )}
|
||||
|
||||
local -a sgdisk=( --zap-all ) # delete existing part tables
|
||||
@ -119,7 +120,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
|
||||
fi
|
||||
sgdisk+=( --disk-guid="${disk[guid]}" )
|
||||
|
||||
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
|
||||
for partDecl in "@{config.setup.disks.partitionList[@]}" ; do
|
||||
eval 'local -A part='"$partDecl"
|
||||
if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi
|
||||
if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then
|
||||
@ -206,7 +207,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.setup.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; \return 1 ; fi
|
||||
bootLoop=$( @{native.util-linux}/bin/losetup --show -f /dev/disk/by-partlabel/$label ) || return ; prepend_trap "@{native.util-linux}/bin/losetup -d $bootLoop" EXIT
|
||||
ln -sfT ${bootLoop/\/dev/..\/..} /dev/disk/by-partlabel/$label || return
|
||||
done
|
||||
|
@ -3,7 +3,28 @@
|
||||
# NixOS Installation
|
||||
##
|
||||
|
||||
## Entry point to the installation, see »./README.md«.
|
||||
declare-command install-system diskPaths << 'EOD'
|
||||
This command installs a NixOS system to local disks or image files.
|
||||
It gets all the information it needs from the system's NixOS configuration -- except for the path(s) of the target disk(s) / image file(s).
|
||||
|
||||
If »diskPaths« points to something in »/dev/«, then it is directly formatted and written to as block device, otherwise »diskPaths« is (re-)created as raw image and then used as loop device.
|
||||
For hosts that install to multiple disks, pass a :-separated list of »<disk-name>=<path>« pairs (the name may be omitted only for the "default" disk).
|
||||
|
||||
Since the installation needs to format and mount (image files as) disks, it needs some way of elevating permissions. It can:
|
||||
* be run as »root«, requiring Nix to be installed system-wide / for root,
|
||||
* be run with the »sudo« argument (see »--help« output; this runs »nix« commands as the original user, and the rest as root),
|
||||
* or automatically perform the installation in a qemu VM (see »--vm« flag).
|
||||
|
||||
Installing inside the VM is safer (will definitely only write wi the supplied »diskPaths«), more secure (executes the VM), and does not require privilege elevation, but does currently only work for the same ISA, is significantly slower (painfully slow without KVM), and may break custom »*Commands« hooks (esp. those passing in secrets).
|
||||
Without VM, installations across different ISAs (e.g. from an x64 desktop to a Raspberry Pi microSD) works if the installing host is NixOS and sets »boot.binfmt.emulatedSystems« for the target systems ISA, or on other Linux with a matching »binfmt_misc« registration with the preload (F) flag.
|
||||
|
||||
Once done, the disk(s) can be transferred -- or the image(s) be copied -- to the final system, and should boot there.
|
||||
If the target host's hardware target allows, a resulting image can also be passed to the »register-vbox« command to create a bootable VirtualBox instance for the current user, or to »run-qemu« to start it in a qemu VM.
|
||||
|
||||
What the installation does is defined solely by the target host's NixOS configuration.
|
||||
The "Installation" section of each host's documentation should contain host specific details, if any.
|
||||
Various »FLAG«s below affect how the installation is performed (in VM, verbosity, debugging, ...).
|
||||
EOD
|
||||
function install-system {( # 1: diskPaths
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
prepare-installer "$@" || exit
|
||||
@ -11,6 +32,13 @@ function install-system {( # 1: diskPaths
|
||||
install-system-to $mnt || exit
|
||||
)}
|
||||
|
||||
declare-flag install-system vm "" "Perform the system installation in a qemu VM instead of on the host itself. This is implied when not running as »root« (or with the »sudo« option).
|
||||
The VM boots the target system's kernel (or a slight modification of it, if the system kernel is not bootable in qemu) and performs the installation at the end of the first boot stage (instead of mounting the root filesystem and starting systemd).
|
||||
The target disks or images are passed into the VM as block devices (and are the only devices available there). The host's »/nix/« folder is passed as a read-only network share. This makes the installation safe and secure, but also slower (network share), and may cause problems with custom install commands.
|
||||
The calling user should have access to KVM, or the installation will be very very slow.
|
||||
See also the »--no-vm« and »--vm-shared=« flags."
|
||||
declare-flag install-system no-vm "" "Never perform the installation in a VM. Fail if not executed as »root«."
|
||||
|
||||
## 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 { # 1: diskPaths
|
||||
|
||||
@ -30,7 +58,7 @@ function prepare-installer { # 1: diskPaths
|
||||
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
|
||||
done
|
||||
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
|
||||
local poolName ; for poolName in "@{!config.setup.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
|
||||
done
|
||||
|
||||
@ -44,6 +72,8 @@ function prepare-installer { # 1: diskPaths
|
||||
|
||||
}
|
||||
|
||||
declare-flag install-system vm-shared "" "When installing inside the VM, specifies a host path that is read-write mounted at »/tmp/shared« inside the VM."
|
||||
|
||||
## Re-executes the current system's installation in a qemu VM.
|
||||
function reexec-in-qemu {
|
||||
|
||||
@ -51,7 +81,7 @@ function reexec-in-qemu {
|
||||
|
||||
# (not sure whether this works for block devices)
|
||||
ensure-disks "$1" 1 || return
|
||||
qemu=( -m 2048 ) ; declare -A qemuDevs=( )
|
||||
qemu=( -m 3072 ) ; 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
|
||||
@ -69,10 +99,10 @@ function reexec-in-qemu {
|
||||
devSpec= ; for name in "${!qemuDevs[@]}" ; do devSpec+="$name"="${qemuDevs[$name]}": ; done
|
||||
newArgs+=( ${devSpec%:} ) ; shift ; (( $# == 0 )) || args+=( "$@" ) # (( ${#argv[@]} > 1 )) && args+=( "${argv[@]:1}" )
|
||||
|
||||
#local output=@{inputs.self}'#'nixosConfigurations.@{outputName:?}.config.system.build.vmExec
|
||||
#local output=@{inputs.self}'#'nixosConfigurations.@{config.installer.outputName:?}.config.system.build.vmExec
|
||||
local output=@{config.system.build.vmExec.drvPath!unsafeDiscardStringContext} # this is more accurate, but also means another system needs to get evaluated every time
|
||||
local scripts=$0 ; if [[ @{pkgs.system} != "@{native.system}" ]] ; then
|
||||
scripts=$( build-lazy @{inputs.self}'#'apps.@{pkgs.system}.@{outputName:?}.derivation ) || return
|
||||
scripts=$( build-lazy @{inputs.self}'#'apps.@{pkgs.system}.@{config.installer.outputName:?}.derivation ) || return
|
||||
fi
|
||||
local command="$scripts install-system $( printf '%q ' "${newArgs[@]}" ) || exit"
|
||||
|
||||
@ -93,11 +123,15 @@ function nixos-install-cmd {( # 1: mnt, 2: topLevel
|
||||
LC_ALL=C PATH=$PATH:@{native.util-linux}/bin @{native.nixos-install-tools}/bin/nixos-enter --silent --root "$1" -c "${_set_x:-:} ; @{config.system.build.installBootLoader} $2" || exit
|
||||
)}
|
||||
|
||||
declare-flag install-system toplevel "" "Optional replacement for the actual »config.system.build.toplevel«."
|
||||
declare-flag install-system no-inspect "" "Do not inspect the (successfully) installed system before unmounting its filesystems."
|
||||
declare-flag install-system inspect-cmd "script" "Instead of opening an interactive shell for the post-installation inspection, »eval« this script."
|
||||
|
||||
## 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 {( set -u # 1: mnt
|
||||
mnt=$1 ; topLevel=${2:-}
|
||||
function install-system-to {( set -u # 1: mnt, 2?: topLevel
|
||||
targetSystem=${args[toplevel]:-@{config.system.build.toplevel}}
|
||||
mnt=$1 ; topLevel=${2:-$targetSystem}
|
||||
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
|
||||
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
@ -120,14 +154,14 @@ function install-system-to {( set -u # 1: mnt
|
||||
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.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 # On NixOS, this is a symlink or wrapper script, pointing to the store.
|
||||
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{pkgs.system}" ]] ; then
|
||||
mkdir -p $mnt/run/binfmt || exit ; [[ ! -e /run/binfmt/"@{pkgs.system}" ]] || cp -a {,$mnt}/run/binfmt/"@{pkgs.system}" || exit # On NixOS, this is a symlink or wrapper script, pointing to the store.
|
||||
# Ubuntu (20.04, by default) uses a statically linked, already loaded qemu binary (F-flag), which therefore does not need to be reference-able from within the chroot.
|
||||
fi
|
||||
|
||||
# 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} )
|
||||
cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt "$topLevel" )
|
||||
if [[ ${args[quiet]:-} ]] ; then
|
||||
"${cmd[@]}" --quiet >/dev/null 2> >( grep -Pe '^error:' || true ) || exit
|
||||
elif [[ ${args[quiet]:-} ]] ; then
|
||||
@ -148,9 +182,9 @@ function install-system-to {( set -u # 1: mnt
|
||||
|
||||
# Run the main install command (primarily for the bootloader):
|
||||
@{native.util-linux}/bin/mount -o bind,ro /nix/store $mnt/nix/store || exit ; prepend_trap '! @{native.util-linux}/bin/mountpoint -q $mnt/nix/store || @{native.util-linux}/bin/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
|
||||
code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" >$beLoud 2>$beSilent || code=$?
|
||||
run-hook-script 'Post Installation' @{config.wip.fs.disks.postInstallCommands!writeText.postInstallCommands} || exit
|
||||
run-hook-script 'Pre Installation' @{config.installer.commands.preInstall!writeText.preInstallCommands} || exit
|
||||
code=0 ; nixos-install-cmd $mnt "$topLevel" >$beLoud 2>$beSilent || code=$?
|
||||
run-hook-script 'Post Installation' @{config.installer.commands.postInstall!writeText.postInstallCommands} || exit
|
||||
|
||||
# Done!
|
||||
if [[ ${args[no-inspect]:-} ]] ; then
|
||||
|
@ -12,7 +12,7 @@ function prompt-for-user-passwords { # (void)
|
||||
}
|
||||
|
||||
|
||||
## Mounts a ramfs as the host's keystore and populates it with keys as requested by »config.wip.fs.keystore.keys«.
|
||||
## Mounts a ramfs as the host's keystore and populates it with keys as requested by »config.setup.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}
|
||||
@ -21,9 +21,9 @@ function populate-keystore { # (void)
|
||||
@{native.util-linux}/bin/mount ramfs -t ramfs $keystore && prepend_trap "@{native.util-linux}/bin/umount $keystore" EXIT || return
|
||||
|
||||
local -A methods=( ) ; local -A options=( )
|
||||
local usage ; for usage in "@{!config.wip.fs.keystore.keys[@]}" ; do
|
||||
methods[$usage]=@{config.wip.fs.keystore.keys[$usage]%%=*}
|
||||
options[$usage]=@{config.wip.fs.keystore.keys[$usage]:$(( ${#methods[$usage]} + 1 ))}
|
||||
local usage ; for usage in "@{!config.setup.keystore.keys[@]}" ; do
|
||||
methods[$usage]=@{config.setup.keystore.keys[$usage]%%=*}
|
||||
options[$usage]=@{config.setup.keystore.keys[$usage]:$(( ${#methods[$usage]} + 1 ))}
|
||||
done
|
||||
|
||||
local usage ; for usage in "${!methods[@]}" ; do
|
||||
|
@ -3,8 +3,10 @@
|
||||
# NixOS Maintenance
|
||||
##
|
||||
|
||||
## 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 {( # 1: diskImages, 2?: bridgeTo
|
||||
declare-command register-vbox diskImages bridgeTo? << 'EOD'
|
||||
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).
|
||||
EOD
|
||||
function register-vbox {(
|
||||
diskImages=$1 ; bridgeTo=${2:-}
|
||||
vmName="nixos-@{config.networking.hostName}"
|
||||
VBoxManage=$( PATH=$hostPath which VBoxManage ) || exit # The host is supposed to run these anyway, and »pkgs.virtualbox« is marked broken on »aarch64«.
|
||||
@ -45,10 +47,27 @@ function register-vbox {( # 1: diskImages, 2?: bridgeTo
|
||||
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 { # 1: diskImages, ...: qemuArgs
|
||||
if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{outputName:-@{config.system.name}}/ ; fi
|
||||
declare-command run-qemu diskImages qemuArgs... << 'EOD'
|
||||
Runs a host in a QEMU VM, directly from its bootable disks, without requiring any change in it's configuration.
|
||||
This function infers many qemu options from the target system's configuration and the current host system.
|
||||
»diskImages« may be passed in the same format as to the installer. Any image files passed are ensured to be loop-mounted. »root« may also pass device paths.
|
||||
EOD
|
||||
declare-flag run-qemu dry-run "" "Instead of running the (main) qemu (and install) command, only print it."
|
||||
declare-flag run-qemu efi "" "Treat the target system as EFI system, even if not recognized as such automatically."
|
||||
declare-flag run-qemu efi-vars "path" "For »--efi« systems, path to a file storing the EFI variables. The default is in »XDG_RUNTIME_DIR«, i.e. it does not persist across host reboots."
|
||||
declare-flag run-qemu graphic "" "Open a graphical window even of the target system logs to serial and not (explicitly) TTY1."
|
||||
declare-flag run-qemu install "[always]" "If any of the guest system's disk images does not exist, perform the its installation before starting the VM. If set to »always«, always install before starting the VM. With this flag set, »diskImages« defaults to paths in »/tmp/."
|
||||
declare-flag run-qemu mem "num" "VM RAM in MiB (»qemu -m«)."
|
||||
declare-flag run-qemu nat-fw "forwards" "Port forwards to the guest's NATed NIC. E.g: »--nat-fw=:8000-:8000,:8001-:8001,127.0.0.1:2022-:22«."
|
||||
declare-flag run-qemu no-kvm "" "Do not rey to use (or complain about the unavailability of) KVM."
|
||||
declare-flag run-qemu no-nat "" "Do not provide a NATed NIC to the guest."
|
||||
declare-flag run-qemu no-serial "" "Do not connect the calling terminal to a serial adapter the guest can log to and open a terminal on the guests serial, as would be the default if the guests logs to ttyS0."
|
||||
declare-flag run-qemu share "decls" "Host dirs to make available as network shares for the guest, as space separated list of »name:host-path,options. E.g. »--share='foo:/home/user/foo,readonly=on bar:/tmp/bar«. In the VM hte share can be mounted with: »$ mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 -o ro foo /foo«."
|
||||
declare-flag run-qemu smp "num" "Number of guest CPU cores."
|
||||
declare-flag run-qemu usb-port "path" "A physical USB port (or hub) to pass to the guest (e.g. a YubiKey for unlocking). Specified as »<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«."
|
||||
declare-flag run-qemu virtio-blk "" "Pass the system's disks/images as virtio disks, instead of using AHCI+IDE. Default iff »boot.initrd.availableKernelModules« includes »virtio_blk« (because it requires that driver)."
|
||||
function run-qemu {
|
||||
if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{config.installer.outputName:-@{config.system.name}}/ ; fi
|
||||
diskImages=${argv[0]:?} ; argv=( "${argv[@]:1}" )
|
||||
|
||||
local qemu=( )
|
||||
@ -66,7 +85,7 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
|
||||
qemu+=( -machine accel=tcg ) # this may suppress warnings that qemu is using tcg (slow) instead of kvm
|
||||
fi
|
||||
else
|
||||
qemu=( $( build-lazy @{native.qemu_full.drvPath!unsafeDiscardStringContext} )/bin/qemu-system-@{config.wip.preface.hardware} ) || return
|
||||
qemu=( $( build-lazy @{native.qemu_full.drvPath!unsafeDiscardStringContext} )/bin/qemu-system-@{pkgs.system%%-linux} ) || return
|
||||
fi
|
||||
if [[ @{pkgs.system} == aarch64-* ]] ; then
|
||||
qemu+=( -machine type=virt ) # aarch64 has no default, but this seems good
|
||||
@ -80,17 +99,17 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
|
||||
#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"}
|
||||
local efiVars=${args[efi-vars]:-"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qemu-@{config.installer.outputName:-@{config.system.name}}-VARS.fd"}
|
||||
qemu+=( -drive file="$efiVars",if=pflash,format=raw,unit=1 )
|
||||
if [[ ! -e "$efiVars" ]] ; then mkdir -pm700 "$( dirname "$efiVars" )" ; 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
|
||||
# if [[ @{pkgs.system} == 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=( ${diskImages}primary.img ) ; for name in "@{!config.setup.disks.devices[@]}" ; do if [[ $name != primary ]] ; then disks+=( ${diskImages}${name}.img ) ; fi ; done
|
||||
else disks=( ${diskImages//:/ } ) ; fi
|
||||
|
||||
[[ ' '"@{boot.initrd.availableKernelModules[@]}"' ' != *' 'virtio_blk' '* ]] || args[virtio-blk]=1
|
||||
@ -155,9 +174,11 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
|
||||
# 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 { # 1: blockDev, 2?: hostHash
|
||||
declare-command add-bootkey-to-keydev blockDev << 'EOD'
|
||||
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 beforehand, run: $ sgdisk --zap-all "$blockDev"
|
||||
EOD
|
||||
function add-bootkey-to-keydev {
|
||||
local blockDev=$1 ; local hostHash=${2:-@{config.networking.hostName!hashString.sha256}}
|
||||
local bootkeyPartlabel=bootkey-${hostHash:0:8}
|
||||
@{native.gptfdisk}/bin/sgdisk --new=0:0:+1 --change-name=0:"$bootkeyPartlabel" --typecode=0:0000 "$blockDev" || exit # create new 1 sector (512b) partition
|
||||
@ -165,26 +186,29 @@ function add-bootkey-to-keydev { # 1: blockDev, 2?: hostHash
|
||||
</dev/urandom tr -dc 0-9a-f | head -c 512 >/dev/disk/by-partlabel/"$bootkeyPartlabel" || exit
|
||||
}
|
||||
|
||||
## Tries to open and mount the systems keystore from its LUKS partition. If successful, adds the traps to close it when the parent shell exits.
|
||||
# For the exit traps to trigger on exit from the calling script / shell, this can't run in a sub shell (and therefore can't be called from a pipeline).
|
||||
# See »open-system«'s implementation for some example calls to this function.
|
||||
function mount-keystore-luks { # ...: cryptsetupOptions
|
||||
declare-command mount-keystore-luks cryptsetupOptions... << 'EOD'
|
||||
Tries to open and mount the systems keystore from its LUKS partition. If successful, this also adds the traps to close the keystore when the parent shell exits (so this is not useful as a standalone »COMMAND«, use the »bash« or »--command« options).
|
||||
For the exit traps to trigger on exit from the calling script / shell, this can't run in a sub shell (and therefore can't be called from a pipeline).
|
||||
See »open-system«'s implementation for some example calls to this function.
|
||||
EOD
|
||||
function mount-keystore-luks {
|
||||
local keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8}
|
||||
mkdir -p -- /run/$keystore && prepend_trap "[[ ! -e /run/$keystore ]] || rmdir /run/$keystore" EXIT || return
|
||||
@{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore && prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $keystore" EXIT || return
|
||||
@{native.util-linux}/bin/mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore && prepend_trap "@{native.util-linux}/bin/umount /run/$keystore" EXIT || return
|
||||
}
|
||||
|
||||
## Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
|
||||
# For any steps taken, it also adds the steps to undo them on exit from the calling shell (so don't call this from a sub-shell that exits too early).
|
||||
# »diskImages« may be passed in the same format as to the installer. If so, any image files are ensured to be loop-mounted.
|
||||
# Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
|
||||
# $ source ${config_wip_fs_disks_initSystemCommands1writeText_initSystemCommands}
|
||||
# $ source ${config_wip_fs_disks_restoreSystemCommands1writeText_restoreSystemCommands}
|
||||
# $ install-system-to $mnt
|
||||
# $ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt
|
||||
# $ nixos-enter --root $mnt
|
||||
function open-system { # 1?: diskImages
|
||||
declare-command open-system diskImages << 'EOD'
|
||||
Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
|
||||
For any steps taken, it also adds the steps to undo them on exit from the calling shell (so this is not useful as a standalone »COMMAND«, use the »bash« or »--command« options, and don't call this from a sub-shell that exits too early).
|
||||
»diskImages« may be passed in the same format as to the installer. Any image files passed are ensured to be loop-mounted. »root« may also pass device paths.
|
||||
|
||||
Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
|
||||
$ install-system-to $mnt
|
||||
$ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt
|
||||
$ nixos-enter --root $mnt
|
||||
EOD
|
||||
function open-system {
|
||||
local diskImages=${1:-} # If »diskImages« were specified and they point at files that aren't loop-mounted yet, then loop-mount them now:
|
||||
local images=$( @{native.util-linux}/bin/losetup --list --all --raw --noheadings --output BACK-FILE )
|
||||
local decl ; for decl in ${diskImages//:/ } ; do
|
||||
@ -195,7 +219,7 @@ function open-system { # 1?: diskImages
|
||||
done
|
||||
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
|
||||
|
||||
if [[ @{config.wip.fs.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
|
||||
if [[ @{config.setup.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
|
||||
mount-keystore-luks --key-file=<( printf %s "@{config.networking.hostName}" ) || return
|
||||
mount-keystore-luks --key-file=/dev/disk/by-partlabel/bootkey-@{config.networking.hostName!hashString.sha256:0:8} || return
|
||||
mount-keystore-luks --key-file=<( read -s -p PIN: pin && echo ' touch!' >&2 && @{native.yubikey-personalization}/bin/ykchalresp -2 "$pin" ) || return
|
||||
@ -208,8 +232,8 @@ function open-system { # 1?: diskImages
|
||||
|
||||
open-luks-layers || return # Load crypt layers and zfs pools:
|
||||
if [[ $( LC_ALL=C type -t ensure-datasets ) == 'function' ]] ; then
|
||||
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
|
||||
if [[ ! @{config.wip.fs.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
|
||||
local poolName ; for poolName in "@{!config.setup.zfs.pools[@]}" ; do
|
||||
if [[ ! @{config.setup.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
|
||||
if ! @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then
|
||||
@{native.zfs}/bin/zpool import -f -N -R "$mnt" "$poolName" && prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
|
||||
fi
|
||||
|
@ -24,12 +24,12 @@ function generic-arg-parse { # ...
|
||||
# Uses »allowedArgs« for the list of the named arguments (the values are the descriptions).
|
||||
# »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
|
||||
function generic-arg-help { # 1: name, 2?: args, 3?: description, 4?: suffix, 5?: usageLine
|
||||
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:-}"
|
||||
printf "${5:-'Usage:\n %s [FLAG[=value]]... [--] %s\n\nWhere »FLAG« may be any of:\n'}" "$1" "${2:-}"
|
||||
local name ; while IFS= read -u3 -r name ; do
|
||||
printf ' %s\n %s\n' "$name" "${allowedArgs[$name]}"
|
||||
printf ' %s\n %s\n' "$name" "${allowedArgs[$name]//$'\n'/$'\n '}"
|
||||
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"
|
||||
@ -92,6 +92,8 @@ function prompt-new-password {( set -u # 1: usage
|
||||
printf %s "$password1" || exit
|
||||
)}
|
||||
|
||||
declare-flag install-system inspectScripts "" "When running installation hooks (»...*Commands« composed as Nix strings) print out and pause before each command. This works ... semi-well."
|
||||
|
||||
## Runs an installer hook script, optionally stepping through the script.
|
||||
function run-hook-script {( # 1: title, 2: scriptPath
|
||||
trap - EXIT # start with empty traps for sub-shell
|
||||
|
@ -1,19 +1,23 @@
|
||||
|
||||
## Creates all of the system's ZFS pools that are »createDuringInstallation«, plus their datasets.
|
||||
function create-zpools { # 1: mnt
|
||||
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
|
||||
if [[ ! @{config.wip.fs.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
|
||||
local poolName ; for poolName in "@{!config.setup.zfs.pools[@]}" ; do
|
||||
if [[ ! @{config.setup.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
|
||||
create-zpool "$1" "$poolName"
|
||||
done
|
||||
}
|
||||
|
||||
## Creates a single of the system's ZFS pools and its datasets.
|
||||
function create-zpool { # 1: mnt, 2: poolName
|
||||
|
||||
declare-command create-zpool mnt poolName << 'EOD'
|
||||
Creates a single of the system's ZFS pools, and its datasets. Can be called manually to create pools that were added to the configuration, or to create those declared with »createDuringInstallation = false«. Expects the backing device(-partition)s to exist as declared for the pool.
|
||||
EOD
|
||||
declare-flag install-system zpool-force "" "(create-zpool) When creating ZFS storage pools, pass the »-f« (force) option. This may be required when installing to disks that are currently part of a pool, or ZFS refuses do reuse them."
|
||||
function create-zpool {
|
||||
local mnt=$1 ; local poolName=$2
|
||||
eval 'local -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
|
||||
eval 'local -A pool='"@{config.setup.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 dataset='"@{config.setup.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
|
||||
@ -33,18 +37,20 @@ function create-zpool { # 1: mnt, 2: poolName
|
||||
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 ${args[zpool-force]:+-f} "${zpoolCreate[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" ) || return
|
||||
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || 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
|
||||
}
|
||||
|
||||
## 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«) 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 { # 1: mnt, 2?: filterExp
|
||||
if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then \return ; fi
|
||||
declare-command ensure-datasets mnt filterExp? << 'EOD'
|
||||
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«) or the keys must be loaded.
|
||||
»keystatus« and »mounted« of existing datasets should remain unchanged by this function, newly crated datasets will not be mounted but have their keys loaded.
|
||||
EOD
|
||||
function ensure-datasets {
|
||||
if (( @{#config.setup.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
|
||||
@ -53,7 +59,7 @@ function ensure-datasets { # 1: mnt, 2?: filterExp
|
||||
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 'local -A dataset='"@{config.wip.fs.zfs.datasets[$name]}"
|
||||
eval 'local -A dataset='"@{config.setup.zfs.datasets[$name]}"
|
||||
eval 'local -A props='"${dataset[props]}"
|
||||
|
||||
local explicitKeylocation=${props[keylocation]:-} cryptKey cryptRoot
|
||||
@ -127,10 +133,10 @@ function ensure-datasets { # 1: mnt, 2?: filterExp
|
||||
# »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 ) || return
|
||||
done
|
||||
done 3< <( printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z )
|
||||
done 3< <( printf '%s\0' "@{!config.setup.zfs.datasets[@]}" | LC_ALL=C sort -z )
|
||||
}
|
||||
|
||||
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.wip.fs.keystore.keys."zfs/..."«).
|
||||
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.setup.keystore.keys."zfs/..."«).
|
||||
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
|
||||
@ -141,8 +147,8 @@ function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_c
|
||||
} ; 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
|
||||
if [[ @{config.setup.keystore.keys[zfs/$name]:-} ]] ; then
|
||||
if [[ @{config.setup.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
|
||||
@ -151,8 +157,8 @@ function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_c
|
||||
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
|
||||
if [[ @{config.setup.keystore.keys[zfs/$name]:-} ]] ; then
|
||||
if [[ @{config.setup.keystore.keys[zfs/$name]} != unencrypted ]] ; then
|
||||
__cryptKey=$keystore/zfs/$name.key ; __cryptRoot=$name
|
||||
fi ; break
|
||||
fi
|
||||
|
Reference in New Issue
Block a user