From 65c1691644da0b20ea268cae06880b14d03319db Mon Sep 17 00:00:00 2001 From: Niklas Gollenstede Date: Fri, 31 May 2024 22:41:35 +0200 Subject: [PATCH] update to nixos-24.05, make installation in VM more versatile, allow imports in host config file based on `name` --- .vscode/settings.json | 4 ++ flake.lock | Bin 1669 -> 1669 bytes flake.nix | 2 +- lib/nixos.nix | 36 +++++++-------- lib/setup-scripts/disk.sh | 14 +++--- lib/setup-scripts/install.sh | 74 ++++++++++++++++--------------- lib/setup-scripts/maintenance.sh | 36 ++++++++------- lib/setup-scripts/utils.sh | 3 +- modules/setup/keystore.nix.md | 2 +- modules/vm-exec.nix.md | 32 ++++++++++--- overlays/gptfdisk.nix.md | 5 ++- overlays/nixos-install-tools.nix | 8 ++++ 12 files changed, 125 insertions(+), 91 deletions(-) create mode 100644 overlays/nixos-install-tools.nix diff --git a/.vscode/settings.json b/.vscode/settings.json index f972b89..f6d7413 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "gdisk", // program "getsize64", // cli arg "getty", // program + "github", // name "gnugrep", // package "gnused", // package "gollenstede", // name @@ -112,6 +113,7 @@ "mountpoint", // program / function "msize", // option "mtab", // linux + "nameserver", // concat "namespacing", // word "netdev", // cli arg "niklas", // name @@ -137,6 +139,7 @@ "optimise", // B/E "ostype", // virtual box "overlayed", // word + "overridable", // word "ovmf", // package "partlabel", // linux "partprobe", // program / function @@ -162,6 +165,7 @@ "reexec", // option "refreservation", // zfs "relatime", // mount option + "resolv", // abbr "rootfs", // linux "rpool", // zfs "sandboxing", // word diff --git a/flake.lock b/flake.lock index 6b0a9150be1243d39a250e9c617cad306e96f5e0..0f7cd084abd0c4f1b69f831362d09be9e640ea49 100644 GIT binary patch delta 368 zcmXZXJx&5a7{>9S5N(L1HX34~CTrZ8opb%7 delta 367 zcmXZX&n^Q&9Ki9ky5Z6|a`7h)Dyi7t%(;vkZF z4?TDdZ{Z%LbQW7m)Ngjl0KZahr7@t&dZ@x^6wR zI=%7m#P-e_S>Psxc{e+?>OOPx{>Wr2o69RYV|f*ZRTUUcjF&l4o$y6RO!G7ff}>` ZNR$>_W1_5-RsjE{_6ux-5clGJeFKSXZVdnc diff --git a/flake.nix b/flake.nix index b09a6da..618acd7 100644 --- a/flake.nix +++ b/flake.nix @@ -2,7 +2,7 @@ "Fully automated NixOS CLI installer" ); inputs = { - nixpkgs = { url = "github:NixOS/nixpkgs/nixos-23.11"; }; + nixpkgs = { url = "github:NixOS/nixpkgs/nixos-24.05"; }; functions = { url = "github:NiklasGollenstede/nix-functions"; inputs.nixpkgs.follows = "nixpkgs"; }; config.url = "github:NiklasGollenstede/nixos-installer?dir=example/defaultConfig"; # "path:./example/defaultConfig"; # (The latter only works on each host after using this flake directly (not as dependency or another flake). The former effectively points to the last commit, i.e. it takes two commits to apply changes to the default config.) diff --git a/lib/nixos.nix b/lib/nixos.nix index 5852d01..de4c686 100644 --- a/lib/nixos.nix +++ b/lib/nixos.nix @@ -1,7 +1,6 @@ dirname: inputs@{ self, nixpkgs, functions, ...}: let inherit (nixpkgs) lib; - inherit (functions.lib) extractBashFunction forEachSystem getModulesFromInputs getNixFiles getOverlaysFromInputs importWrapped mapMerge mapMergeUnique mergeAttrsUnique substituteImplicit; - setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs); + inherit (functions.lib) forEachSystem getModulesFromInputs getNixFiles getOverlaysFromInputs importWrapped mapMerge mapMergeUnique mergeAttrsUnique; # trace; inherit (inputs.config.rename) installer; preface' = inputs.config.rename.preface; getModuleConfig = module: inputs: args: if builtins.isFunction module then ( @@ -33,8 +32,12 @@ in rec { #system = null; # (This actually does nothing more than setting »config.nixpkgs.system« (which is the same as »config.nixpkgs.buildPlatform.system«) and can be null/unset here.) modules = (nixosArgs.modules or [ ]) ++ [ { imports = [ # Anything specific to only this evaluation of the module tree should go here. - (if (builtins.isPath mainModule) || (builtins.isString mainModule) then (importWrapped inputs mainModule).module else mainModule) - { _module.args.name = lib.mkOverride 99 name; } # (specialisations can somehow end up with the name »configuration«, which is very incorrect) + (let + module = if (builtins.isPath mainModule) || (builtins.isString mainModule) then (importWrapped inputs mainModule).module else mainModule; + bindName = func: lib.setFunctionArgs (args: func (args // { inherit name; })) (builtins.removeAttrs (lib.functionArgs func) [ "name" ]); + bindToModule = module: if lib.isFunction module then bindName module else if module?imports then module // { imports = map bindToModule module.imports; } else module; + in bindToModule module) # ensure that in the main module, the "name" parameter is available during the import stage already + { _module.args.name = lib.mkOverride 0 name; } # (specialisations can somehow end up with the name »configuration«, which is very incorrect) { networking.hostName = name; } ]; _file = "${dirname}/nixos.nix#modules"; } ]; @@ -101,8 +104,8 @@ in rec { # Builds a system of NixOS hosts and exports them, plus »apps« and »devShells« to manage them, as flake outputs. # All arguments are optional, as long as the default can be derived from the other arguments as passed. - mkSystemsFlake = args@{ - # An attrset of imported Nix flakes, for example the argument(s) passed to the flake »outputs« function. All other arguments are optional (and have reasonable defaults) if this is provided and contains »self« and the standard »nixpkgs«. This is also the second argument passed to the individual host's top level config files. + mkSystemsFlake = lib.makeOverridable (args@{ + # An attrset of imported Nix flakes, for example the argument(s) passed to the flake »outputs« function. All other arguments are optional (and have reasonable defaults) if this is provided and contains »self« and the standard »nixpkgs«. This is also the second argument passed to the individual hosts' top level config files. inputs ? { }, # Arguments »{ files, dir, exclude, }« to »mkNixosConfigurations«, see there for details. May also be a list of those attrsets, in which case those multiple sets of hosts will be built separately by »mkNixosConfigurations«, allowing for separate sets of »peers« passed to »mkNixosConfiguration«. Each call will receive all other arguments, and the resulting sets of hosts will be merged. hosts ? ({ dir = "${getFlakeDir inputs.self "Can't determine flake dir from »inputs.self«. Supply »mkSystemsFlake.hosts.dir« explicitly!"}/hosts"; exclude = [ ]; }), @@ -141,25 +144,18 @@ in rec { inherit nixosConfigurations; } // (forEachSystem setupPlatforms (buildSystem: let pkgs = (import inputs.nixpkgs { inherit overlays; system = buildSystem; }); - tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites)); in rec { apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = writeSystemScripts { inherit name pkgs system; }; program = "${derivation}"; }) nixosConfigurations; # dummy that just pulls in all system builds packages = let all-systems = pkgs.runCommandLocal "all-systems" { } '' - ${'' - mkdir -p $out/systems - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${system.config.system.build.toplevel} $out/systems/${getName name}") nixosConfigurations)} - ''} - ${'' - mkdir -p $out/scripts - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${apps.${name}.program} $out/scripts/${getName name}") nixosConfigurations)} - ''} - ${lib.optionalString (inputs != { }) '' - mkdir -p $out/inputs - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: { outPath, ... }: "ln -sT ${outPath} $out/inputs/${name}") inputs)} - ''} + mkdir -p $out/systems + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${system.config.system.build.toplevel} $out/systems/${getName name}") nixosConfigurations)} + mkdir -p $out/scripts + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${apps.${name}.program} $out/scripts/${getName name}") nixosConfigurations)} + mkdir -p $out/inputs + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: { outPath, ... }: "ln -sT ${outPath} $out/inputs/${name}") inputs)} ''; in { inherit all-systems; } // (lib.optionalAttrs asDefaultPackage { default = all-systems ; }); checks.all-systems = packages.all-systems; @@ -169,7 +165,7 @@ in rec { apps = mapMergeUnique (k: v: { ${renameOutputs k} = v; }) outputs.apps.${buildSystem}; packages.${renameOutputs "all-systems"} = outputs.packages.${buildSystem}.all-systems; checks.${renameOutputs "all-systems"} = outputs.checks.${buildSystem}.all-systems; - })); + }))); # This makes the »./setup-scripts/*« callable from the command line: writeSystemScripts = { diff --git a/lib/setup-scripts/disk.sh b/lib/setup-scripts/disk.sh index 073046f..49cb1e6 100644 --- a/lib/setup-scripts/disk.sh +++ b/lib/setup-scripts/disk.sh @@ -8,7 +8,7 @@ declare-flag install-system skip-formatting "" "Skip partitioning, formatting, a ## Prepares the disks of the target system for the copying of files. function do-disk-setup { # 1: diskPaths - ensure-disks "$1" || return + ensure-disks || return export 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« @@ -51,14 +51,14 @@ function do-disk-setup { # 1: diskPaths 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.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 +function ensure-disks { declare -g -A blockDevs=( ) # this ends up in the caller's scope - if [[ $1 == */ ]] ; then - mkdir -p "$1" - for name in "@{!config.setup.disks.devices[@]}" ; do blockDevs[$name]=${1}${name}.img ; done + if [[ ${args[disks]} == */ ]] ; then + mkdir -p "${args[disks]}" + for name in "@{!config.setup.disks.devices[@]}" ; do blockDevs[$name]=${args[disks]}${name}.img ; done else - local path ; for path in ${1//:/ } ; do + local path ; for path in ${args[disks]//:/ } ; 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 blockDevs[$name]=$path @@ -73,7 +73,7 @@ function ensure-disks { # 1: diskPaths, 2?: skipLosetup local outFile=${blockDevs[$name]} && 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 + if [[ ${arg_skipLosetup:-} ]] ; then continue ; fi blockDevs[$name]=$( @{native.util-linux}/bin/losetup --show -f "$outFile" ) && prepend_trap "@{native.util-linux}/bin/losetup -d '${blockDevs[$name]}'" EXIT || return else local size=$( @{native.util-linux}/bin/blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} )) diff --git a/lib/setup-scripts/install.sh b/lib/setup-scripts/install.sh index 79a58f6..4867aec 100644 --- a/lib/setup-scripts/install.sh +++ b/lib/setup-scripts/install.sh @@ -3,12 +3,9 @@ # NixOS Installation ## -declare-command install-system diskPaths << 'EOD' +declare-command install-system '[--disks=]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 »=« pairs (the name may be omitted only for the "default" disk). +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); see the »--disks=« flag. 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, @@ -25,10 +22,13 @@ What the installation does is defined solely by the target host's NixOS configur 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 +declare-flag install-system disks "diskPaths" "The disk(s) (to be) used by this system installation. +If »diskPaths« points to something in »/dev/«, then it is directly used as block device, otherwise »diskPaths« is (re-)created as raw image file and then used as loop device. +For hosts that install to multiple disks, pass a :-separated list of »=« pairs (the name may be omitted only for the "default" disk)." function install-system {( # 1: diskPaths trap - EXIT # start with empty traps for sub-shell prepare-installer "$@" || exit - do-disk-setup "$1" || exit + do-disk-setup || exit install-system-to $mnt || exit )} @@ -42,15 +42,15 @@ declare-flag install-system no-vm "" "Never perform the installation in a VM. Fa ## 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 - : ${1:?"The first positional argument must specify the path(s) to the disk(s) and/or image file(s) to install to"} + if [[ ! ${args[disks]:-} ]] ; then args[disks]=${1:?"The disks flag or the first positional argument must specify the path(s) to the disk(s) and/or image file(s) to install to"} ; shift ; fi - umask g-w,o-w # Ensure that files created without explicit permissions are not writable for group and other (0022). + umask g-w,o-w # Ensure that files created without explicit permissions are not writable for group and other. if [[ "$(id -u)" != '0' ]] ; then - if [[ ! ${args[no-vm]:-} ]] ; then reexec-in-qemu "$@" || return ; \exit 0 ; fi + if [[ ! ${args[no-vm]:-} ]] ; then exec-in-qemu install-system || 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 + if [[ ${args[vm]:-} ]] ; then exec-in-qemu install-system || return ; \exit 0 ; 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 @@ -62,7 +62,7 @@ function prepare-installer { # 1: diskPaths 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:-} && ! $( PATH=$hostPath which nix 2>/dev/null ) && $( PATH=$hostPath which su 2>/dev/null ) ]] ; then # use Nix as the user who called this script, as Nix may not be set up for root + if [[ ${SUDO_USER:-} && ! $( PATH=$hostPath which nix 2>/dev/null ) && $( PATH=$hostPath which su 2>/dev/null ) ]] ; then # use Nix as the user who called this script, if Nix is not be set up for root function nix {( set +x ; declare -a args=("$@") ; PATH=$hostPath su - "$SUDO_USER" -s "@{native.bashInteractive!getExe}" -c "$(declare -p args)"' ; nix "${args[@]}"' )} else # use Nix by absolute path, as it won't be on »$PATH« PATH=$PATH:@{native.nix}/bin @@ -75,28 +75,32 @@ function prepare-installer { # 1: diskPaths declare-flag install-system vm-shared "dir-path" "When installing inside the VM, specifies a host path that is read-write mounted at »/tmp/shared« inside the VM." declare-flag install-system vm-args "qemu-args" "When installing inside the VM, extra arguments to pass to qemu." -## 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 "$1" 1 || return - 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 - -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 +## (Re-)executes the current system's script in a qemu VM. +function exec-in-qemu { # 1: entry, ...: argv + qemu=( ) ; apply-vm-args args[vm]='' ; args[no-vm]=1 - newArgs=( ) ; for arg in "${!args[@]}" ; do newArgs+=( --"$arg"="${args[$arg]}" ) ; done - devSpec= ; for name in "${!qemuDevs[@]}" ; do devSpec+="$name"="${qemuDevs[$name]}": ; done - newArgs+=( ${devSpec%:} ) ; shift ; (( $# == 0 )) || args+=( "$@" ) # (( ${#argv[@]} > 1 )) && args+=( "${argv[@]:1}" ) + + if [[ ${args[disks]:-} ]] ; then + # (not sure whether this works for block devices) + arg_skipLosetup=1 ensure-disks || return + args[disks]='' + local index=2 # 1/a is used by the (unused) root disk + 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) + ) + args[disks]+="$name"=/dev/vd"$( printf "\x$(printf %x $(( index - 1 + 97 )) )" )": ; let index+=1 + done + args[disks]=${args[disks]%:} + fi + + newArgs=( ) ; (( $# == 0 )) || newArgs+=( "$@" ) + for arg in "${!args[@]}" ; do newArgs+=( --"$arg"="${args[$arg]}" ) ; done #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 @@ -107,7 +111,7 @@ function reexec-in-qemu { local scripts=$self ; if [[ @{pkgs.system} != "@{native.system}" ]] ; then scripts=$( build-lazy @{inputs.self}'#'apps.@{pkgs.system}.@{config.installer.outputName:?}.derivation ) || return fi - local command="$scripts install-system $( printf '%q ' "${newArgs[@]}" ) || exit" + local command="$scripts $( printf '%q ' "${newArgs[@]}" ) || exit" local runInVm ; runInVm=$( build-lazy $output )/bin/run-@{config.system.name}-vm-exec || return @@ -119,10 +123,10 @@ function reexec-in-qemu { function nixos-install-cmd {( # 1: mnt, 2: topLevel # »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=@{native.nix}/bin:$PATH:@{config.systemd.package}/bin 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: + #PATH=@{native.nix}/bin:$PATH:@{config.systemd.package}/bin TMPDIR=/tmp LC_ALL=C @{native.nixos-install-tools-no-doc}/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 - LC_ALL=C PATH=@{native.busybox}/bin:$PATH:@{native.util-linux}/bin @{native.nixos-install-tools}/bin/nixos-enter --silent --root "$1" -c "source /etc/set-environment ; ${_set_x:-:} ; @{config.system.build.installBootLoader} $2" || exit + LC_ALL=C PATH=@{native.busybox}/bin:$PATH:@{native.util-linux}/bin @{native.nixos-install-tools-no-doc}/bin/nixos-enter --silent --root "$1" -c "source /etc/set-environment ; ${_set_x:-:} ; @{config.system.build.installBootLoader} $2" || exit # (newer versions of »mount« seem to be unable to do »--make-private« on »rootfs« (in the initrd), but busybox's mount still works) )} @@ -201,7 +205,7 @@ function install-system-to {( set -u # 1: mnt, 2?: topLevel 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 ) fi - LC_ALL=C PATH=@{native.busybox}/bin:$PATH:@{native.util-linux}/bin @{native.nixos-install-tools}/bin/nixos-enter --root $mnt -- /nix/var/nix/profiles/system/sw/bin/bash -c 'source /etc/set-environment ; NIXOS_INSTALL_BOOTLOADER=1 CHROOT_DIR="'"$mnt"'" mnt=/ exec "'"$self"'" bash' || exit # +o monitor + LC_ALL=C PATH=@{native.busybox}/bin:$PATH:@{native.util-linux}/bin @{native.nixos-install-tools-no-doc}/bin/nixos-enter --root $mnt -- /nix/var/nix/profiles/system/sw/bin/bash -c 'source /etc/set-environment ; NIXOS_INSTALL_BOOTLOADER=1 CHROOT_DIR="'"$mnt"'" mnt=/ exec "'"$self"'" bash' || exit # +o monitor fi mkdir -p $mnt/var/lib/systemd/timesync && touch $mnt/var/lib/systemd/timesync/clock || true # save current time diff --git a/lib/setup-scripts/maintenance.sh b/lib/setup-scripts/maintenance.sh index 7415c7d..0bdf07b 100644 --- a/lib/setup-scripts/maintenance.sh +++ b/lib/setup-scripts/maintenance.sh @@ -57,8 +57,7 @@ declare-flag run-qemu efi "" "Treat the target system as EFI syste 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 "[1|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 no-kvm "" "Do not rey to use (or complain about the unavailability of) KVM." +declare-flag run-qemu no-kvm "" "Do not try to use (or complain about the unavailability of) KVM." 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-nat "" "Do not provide a NATed NIC to the guest." declare-flag run-qemu nic "type[,options]" "Create an additional network interface using the »-nic« flag. Automatically sets a decent »model« and a »mac« derived from »config.networking.hostId«. @@ -67,12 +66,9 @@ $ ... --nic=socket,listen=:4321 # once $ ... --nic=socket,connect=:4321 # once Example 2 (connect many VMs, unprivileged): $ nix shell nixpkgs#vde2 --command vde_switch -sock /tmp/vm-net -$ ... --nic=vde,sock=/tmp/vm-net # multiple times -" +$ ... --nic=vde,sock=/tmp/vm-net # multiple times" 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 »-«, where bus and port refer to the physical USB port »/sys/bus/usb/devices/-« (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 @@ -99,9 +95,6 @@ function run-qemu { qemu+=( -machine type=virt ) # aarch64 has no default, but this seems good fi ; qemu+=( -cpu max ) - qemu+=( -m ${args[mem]:-2048} ) - if [[ ${args[smp]:-} ]] ; then qemu+=( -smp ${args[smp]} ) ; fi - if [[ @{config.virtualisation.useEFIBoot:-} || @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then # UEFI. Otherwise it boots SeaBIOS. local ovmf ; ovmf=$( build-lazy @{pkgs.OVMF.drvPath!unsafeDiscardStringContext} 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). @@ -159,18 +152,15 @@ function run-qemu { qemu+=( -nic model=virtio-net-pci,mac=$mac,type="${args[nic]}" ) fi - # 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 local decl ; for decl in ${args[usb-port]//:/ } ; do - qemu+=( -usb -device usb-host,hostbus="${decl/-*/}",hostport="${decl/*-/}" ) - done ; fi + apply-vm-args 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[trace]:-} ]] ; then verbosity=--trace ; fi ; if [[ ${args[debug]:-} ]] ; then verbosity=--debug ; fi - hostPath=${hostPath:-} ${args[dry-run]:+echo} "$self" install-system "$diskImages" $verbosity --no-inspect || return - fi + if [[ ${args[install]:-} == always ]] && [[ ! ${args[dry-run]:-} ]] ; then ( + if [[ ! ${args[trace]:-} ]] && [[! ${args[debug]:-} ]] ; then args[quiet]=1 ; fi + args[no-inspect]=1 ; install-system "$diskImages" || exit + ) || return ; fi qemu+=( "${argv[@]}" ) if [[ ${args[dry-run]:-} ]] ; then @@ -182,6 +172,18 @@ function run-qemu { # https://askubuntu.com/questions/54814/how-can-i-ctrl-alt-f-to-get-to-a-tty-in-a-qemu-session } +declare-flag run-qemu,install-system,'*' vm-mem "num" "VM RAM in MiB (»qemu -m«)." +declare-flag run-qemu,install-system,'*' vm-smp "num" "Number of guest CPU cores." +declare-flag run-qemu,install-system,'*' vm-usb-port "path" "A physical USB port (or hub) to pass to the guest (e.g. a YubiKey for unlocking). Specified as »-«, where bus and port refer to the physical USB port »/sys/bus/usb/devices/-« (see »lsusb -tvv«). E.g.: »--vm-usb-port=3-1.1.1.4«." +function apply-vm-args { + qemu+=( -m ${args[vm-mem]:-2048} ) + if [[ ${args[vm-smp]:-} ]] ; then qemu+=( -smp ${args[vm-smp]} ) ; fi + + if [[ ${args[vm-usb-port]:-} ]] ; then local decl ; for decl in ${args[vm-usb-port]//:/ } ; do + qemu+=( -usb -device usb-host,hostbus="${decl/-*/}",hostport="${decl/*-/}" ) + done ; fi +} + 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 (»usbPartition«/»usb-part«). To create/clear the GPT beforehand, run: $ sgdisk --zap-all "$blockDev" diff --git a/lib/setup-scripts/utils.sh b/lib/setup-scripts/utils.sh index 5c4904f..5ca2ffb 100644 --- a/lib/setup-scripts/utils.sh +++ b/lib/setup-scripts/utils.sh @@ -47,8 +47,9 @@ function prompt-new-password {( set -u # 1: usage )} ## If »secretFile« does not exist, interactively prompts up to three times for the secret to be stored in that file. +declare-flag '*' no-optional-prompts "" "Skip prompting for (and thus saving) secret marked as optional." function prompt-secret-as {( set -u # 1: what, 2: secretFile, 3?: owner[:[group]], 4?: mode - if [[ -e $2 ]] ; then \return ; fi + if [[ ${arg_optional:-} && ${args[no-optional-prompts]:-} ]] ; then \return ; fi ; if [[ -e $2 ]] ; then \return ; fi what=$1 ; shift function prompt { read -s -p "Please enter $what: " value || exit ; echo 1>&2 diff --git a/modules/setup/keystore.nix.md b/modules/setup/keystore.nix.md index dc6037f..091120d 100644 --- a/modules/setup/keystore.nix.md +++ b/modules/setup/keystore.nix.md @@ -170,7 +170,7 @@ in let module = { initrd-cleanup.preStart = '' umount ${keystore} || true rmdir ${keystore} || true - ${config.systemd.package}/lib/systemd/systemd-cryptsetup detach keystore-${hash} + systemd-cryptsetup detach keystore-${hash} ''; }; boot.initrd.luks.devices."keystore-${hash}".keyFileTimeout = 10; diff --git a/modules/vm-exec.nix.md b/modules/vm-exec.nix.md index 015d76c..7eddbfb 100644 --- a/modules/vm-exec.nix.md +++ b/modules/vm-exec.nix.md @@ -61,6 +61,7 @@ in let hostModule = { if [[ ! ''${args[initrd-console]:-} ]] ; then noConsole=1 ; fi if [[ ''${args[initrd-console]:-} ]] ; then touch $tmp/xchg/initrd-console ; fi if [[ ''${args[quiet]:-} ]] ; then touch $tmp/xchg/quiet ; fi + $tmp/xchg/host ${cfg.virtualisation.qemu.package}/bin/qemu-img create -f qcow2 $tmp/dummyImage 4M &>/dev/null # do this silently @@ -69,7 +70,9 @@ in let hostModule = { export QEMU_KERNEL_PARAMS="init=${config.system.build.toplevel}/init ''${noConsole:+console=tty1} edd=off boot.shell_on_fail" export QEMU_NET_OPTS= QEMU_OPTS= if [[ ''${args[quiet]:-} ]] ; then - ${launch} "''${argv[@]}" &> >( ${pkgs.coreutils}/bin/tr -dc '[[:print:]]\r\n\t' | { while IFS= read line ; do if [[ $line == magic:cm4alv0wly79p6i4aq32hy36i* ]] ; then break ; fi ; done ; cat ; } ) || { e=$? ; echo "Execution of VM failed!" 1>&2 ; exit $e ; } + ${launch} "''${argv[@]}" &> >( ${pkgs.coreutils}/bin/tr -dc '[[:print:]]\r\n\t' | { + while IFS= read line ; do if [[ $line == magic:cm4alv0wly79p6i4aq32hy36i* ]] ; then break ; fi ; done ; cat ; + } ) || { e=$? ; echo "Execution of VM failed!" 1>&2 ; exit $e ; } else ${launch} "''${argv[@]}" || exit fi @@ -90,7 +93,7 @@ in let hostModule = { # Instead of tearing down the initrd environment, adjust some mounts and run the »command« in the initrd: boot.initrd.systemd.enable = lib.mkVMOverride false; boot.initrd.postMountCommands = '' - + set -x for fs in tmp/shared tmp/xchg nix/store.lower nix/var/nix/db.lower ; do mkdir -p /$fs && mount --move $targetRoot/$fs /$fs || fail done @@ -108,14 +111,24 @@ in let hostModule = { toplevel=$(dirname $stage2Init) ln -sfT $toplevel /run/current-system ln -sfT $toplevel /run/booted-system - ln -sfT $toplevel/kernel-modules/lib/modules /lib/modules + rm -rf /lib/modules ; ln -sfT $toplevel/kernel-modules/lib/modules /lib/modules - # Also mostly dor debugging shells: + # Set up /etc: mv /etc /etc.initrd mkdir -p -m 755 /etc.work /etc.upper /etc mount -t overlay overlay -o lowerdir=$toplevel/etc,workdir=/etc.work,upperdir=/etc.upper /etc ( cd /etc.initrd ; cp -a mtab udev /etc/ ) # (keep these) + # Set up NATed networking: + cat /etc/hosts >/etc/hosts.cp ; rm /etc/hosts ; mv /etc/hosts.cp /etc/hosts + perl -pe 's/127.0.0.[12](?! localhost)/# /' -i /etc/hosts + perl -pe 's/::1(?! localhost)/# /' -i /etc/hosts + { printf '10.0.2.2 ' ; cat /tmp/xchg/host ; } >>/etc/hosts + ip addr add 10.0.2.15/24 dev eth0 + ip link set dev eth0 up + ip route add default via 10.0.2.2 dev eth0 + echo nameserver 1.1.1.1 >/etc/resolv.conf # 10.0.2.3 doesn't reply + # »nix copy« complains without »nixbld« group: rm -f /etc/passwd /etc/group printf '%s\n' 'root:x:0:0:root:/root:/bin/bash' >/etc/passwd @@ -126,7 +139,7 @@ in let hostModule = { console=/dev/ttyS0 ; if [[ -e /tmp/xchg/initrd-console ]] ; then console=/dev/console ; fi # (does this even make a difference?) if [[ -e /tmp/xchg/quiet ]] ; then printf '\n%s\n' 'magic:cm4alv0wly79p6i4aq32hy36i...' >$console ; fi - exit=0 ; bash /tmp/xchg/script <$console >$console 2>$console || exit=$? + set +x ; exit=0 ; bash /tmp/xchg/script <$console >$console 2>$console || exit=$? echo $exit >/tmp/xchg/exit sync ; sync @@ -135,6 +148,7 @@ in let hostModule = { sleep infinity # the VM will halt very soon ''; boot.initrd.kernelModules = [ "overlay" ]; # for writable »/etc«, chown of »/nix/store« and locks in »/nix/var/nix/db« + #boot.initrd.extraUtilsCommands = ''copy_bin_and_libs ${pkgs.perl}/bin/perl''; }) ({ @@ -148,6 +162,7 @@ in let hostModule = { "/nix/store".mountPoint = lib.mkForce "/nix/store.lower"; }; # mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 nix-var-nix-db /nix/var/nix/db virtualisation.qemu.options = [ "-virtfs local,path=/nix/var/nix/db,security_model=none,mount_tag=nix-var-nix-db,readonly=on" ]; # (doing this manually to pass »readonly«, to not ever corrupt the host's Nix DBs) + boot.resumeDevice = lib.mkVMOverride ""; }) ({ @@ -159,10 +174,13 @@ in let hostModule = { }) ({ - virtualisation = if (builtins.substring 0 5 pkgs.lib.version) > "22.05" then { host.pkgs = lib.mkDefault pkgs.buildPackages; } else { }; - }) ({ + virtualisation.host.pkgs = lib.mkDefault pkgs.buildPackages; virtualisation.qemu.package = lib.mkIf (pkgs.buildPackages.system != pkgs.system) (cfg.virtualisation.host or { pkgs = pkgs.buildPackages; }).pkgs.qemu_full; + }) ({ + + #virtualisation.qemu.options = [ "-nic user,model=virtio-net-pci" ]; # NAT + }) ({ specialisation = lib.mkForce { }; diff --git a/overlays/gptfdisk.nix.md b/overlays/gptfdisk.nix.md index 8b7132b..d898cea 100644 --- a/overlays/gptfdisk.nix.md +++ b/overlays/gptfdisk.nix.md @@ -12,9 +12,10 @@ GPT-FDisk patched to be able to move not only the primary, but also the backup p dirname: inputs: final: prev: let inherit (final) pkgs; lib = inputs.self.lib.__internal__; debug = false; + enable = lib.versionOlder prev.gptfdisk.version "1.0.10"; # patch merged in that version in { - gptfdisk = ( + gptfdisk = if !enable then prev.gptfdisk else ( if debug then pkgs.enableDebugging else (x: x) ) (prev.gptfdisk.overrideAttrs (old: let pname = "gptfdisk"; @@ -31,6 +32,6 @@ in { dontStrip = true; } else { }))); - libblockdev = prev.libblockdev.override { inherit (prev) gptfdisk; }; + libblockdev = prev.libblockdev.override (lib.optionalAttrs enable { inherit (prev) gptfdisk; }); } diff --git a/overlays/nixos-install-tools.nix b/overlays/nixos-install-tools.nix new file mode 100644 index 0000000..a9a6586 --- /dev/null +++ b/overlays/nixos-install-tools.nix @@ -0,0 +1,8 @@ +dirname: inputs: final: prev: let + lib = inputs.self.lib.__internal__; +in { + ## The options reference manual takes long to generate, is also available online (in this exact form, as this version is the generic nixpkgs one), and has failed to generate on occasion. + + nixos-install-tools-no-doc = (prev.nixos-install-tools.override (old: { nixos = args: lib.recursiveUpdate (old.nixos args) { config.system.build.manual.nixos-configuration-reference-manpage = null; }; })); + #nixos-install-tools-no-doc = (prev.nixos-install-tools.overrideAttrs (old: { pkgs = builtins.toJSON (builtins.tail (builtins.fromJSON old.pkgs)); })); +}