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:
Niklas Gollenstede 2023-01-29 15:55:56 +01:00
parent e3b5b17620
commit 9edfe9c9d8
30 changed files with 1228 additions and 340 deletions

37
.vscode/settings.json vendored
View File

@ -14,6 +14,7 @@
"ashift", // zfs "ashift", // zfs
"askpass", // program "askpass", // program
"attrset", "attrsets", // nix/abbr (attribute set) "attrset", "attrsets", // nix/abbr (attribute set)
"autoexpand", // zfs option
"autologin", // agetty "autologin", // agetty
"autotrim", // zpool property "autotrim", // zpool property
"binfmt", // abbr "binary format" "binfmt", // abbr "binary format"
@ -25,6 +26,7 @@
"bootloader", // word "bootloader", // word
"bridgeadapter", // virtual box "bridgeadapter", // virtual box
"bridgedifs", // virtual box "bridgedifs", // virtual box
"btrfs", // filesystem
"builtins", // nix "builtins", // nix
"cachefile", // zfs "cachefile", // zfs
"canmount", // zfs "canmount", // zfs
@ -43,6 +45,7 @@
"cryptsetup", // program "cryptsetup", // program
"ctxify", // abbr (~= add context) "ctxify", // abbr (~= add context)
"CWD", // abbr "CWD", // abbr
"cykerway", // name
"dedup", // zfs "dedup", // zfs
"deps", // abbr dependencies "deps", // abbr dependencies
"devs", // abbr (devices) "devs", // abbr (devices)
@ -52,6 +55,7 @@
"dnodesize", // zfs "dnodesize", // zfs
"dontUnpack", // nixos "dontUnpack", // nixos
"dosfstools", // package "dosfstools", // package
"dpkg", // file format
"draid", // zfs "draid", // zfs
"dropbear", // program "dropbear", // program
"dtoverlay", // option "dtoverlay", // option
@ -60,6 +64,8 @@
"eeprom", // abbr "eeprom", // abbr
"elif", // abbr (else if) "elif", // abbr (else if)
"encryptionroot", // zfs "encryptionroot", // zfs
"errexit", // bash
"expandsz", // zfs property
"extglob", // cli arg "extglob", // cli arg
"extlinux", // program "extlinux", // program
"extra_isize", // ext4 option "extra_isize", // ext4 option
@ -69,15 +75,19 @@
"fetchurl", // nix function "fetchurl", // nix function
"filesystems", // plural "filesystems", // plural
"findutils", // package "findutils", // package
"firejail", // program
"firmwareLinuxNonfree", // nixos "firmwareLinuxNonfree", // nixos
"fmask", // mount "fmask", // mount
"foldl", // nix (fold left) "foldl", // nix (fold left)
"foldr", // nix (fold right) "foldr", // nix (fold right)
"fstrim", // program
"FUNCNAME", // bash var "FUNCNAME", // bash var
"fuser", // optoin
"fw_printenv", // program "fw_printenv", // program
"fw_setenv", // program "fw_setenv", // program
"gcroots", // Nix "gcroots", // Nix
"gdisk", // program "gdisk", // program
"genkeytypes", // concat
"getsize64", // cli arg "getsize64", // cli arg
"getty", // serice "getty", // serice
"gids", // abbr/plural (group IDs) "gids", // abbr/plural (group IDs)
@ -86,6 +96,7 @@
"gollenstede", // name "gollenstede", // name
"gpio", // abbr (general purpose IO) "gpio", // abbr (general purpose IO)
"gptfdisk", // package "gptfdisk", // package
"hcloud", // program
"headlessly", // word "headlessly", // word
"hetzner", // comapny "hetzner", // comapny
"HISTCONTROL", // bash option "HISTCONTROL", // bash option
@ -100,6 +111,7 @@
"inodes", // plural "inodes", // plural
"internalcommands", // virtual box "internalcommands", // virtual box
"ints", // plural "ints", // plural
"journalctl", // program
"keydev", // cli arg "keydev", // cli arg
"keyformat", // zfs "keyformat", // zfs
"keylocation", // zfs "keylocation", // zfs
@ -113,6 +125,7 @@
"libubootenv", // package "libubootenv", // package
"libutil", // concat "libutil", // concat
"logbias", // zfs "logbias", // zfs
"logind", // program
"losetup", // program / function "losetup", // program / function
"lowerdir", // mount overlay option "lowerdir", // mount overlay option
"lsusb", // program / function "lsusb", // program / function
@ -125,11 +138,15 @@
"mmap", // abbr "memory map" "mmap", // abbr "memory map"
"modifyvm", // virtual box "modifyvm", // virtual box
"mountpoint", // program / function "mountpoint", // program / function
"msize", // option
"mtab", // linux "mtab", // linux
"namei", // program
"namespacing", // word "namespacing", // word
"netbootxyz", // option "netbootxyz", // option
"netdev", // cli arg "netdev", // cli arg
"netns", // linux
"niklas", // name "niklas", // name
"nixbld", // nix
"nixos", // (duh) "nixos", // (duh)
"nixpkgs", // nix "nixpkgs", // nix
"noatime", // mount option "noatime", // mount option
@ -139,15 +156,20 @@
"nodiscard", // cli arg "nodiscard", // cli arg
"noexec", // mount option "noexec", // mount option
"nofail", // cli arg "nofail", // cli arg
"nofile", // option (number of files)
"nographic", // cli arg "nographic", // cli arg
"noheadings", // cli arg "noheadings", // cli arg
"nohibernate", // kernel param "nohibernate", // kernel param
"noprofile", // option
"nosuid", // mount option "nosuid", // mount option
"notrunc", // dd option "notrunc", // dd option
"nounset", // bash
"ntfs", // filesystem
"ondemand", // concat "ondemand", // concat
"oneshot", // systemd "oneshot", // systemd
"optimise", // B/E "optimise", // B/E
"ostype", // virtual box "ostype", // virtual box
"overlayed", // word
"OVMF", // package "OVMF", // package
"partlabel", // linux "partlabel", // linux
"partprobe", // program / function "partprobe", // program / function
@ -155,12 +177,16 @@
"passthru", // nix "passthru", // nix
"pbkdf", // cli arg "pbkdf", // cli arg
"pflash", // cli arg "pflash", // cli arg
"pipefail", // bash
"pkgs", // nix "pkgs", // nix
"pname", // nix/abbr (package name) "pname", // nix/abbr (package name)
"portcount", // virtual box "portcount", // virtual box
"posix", // word "posix", // word
"posixacl", // zfs "posixacl", // zfs
"poweroff", // program / function "poweroff", // program / function
"poweron", // concat
"pwauth", // abbr (password authentication)
"qcow", // file type (qemu)
"raidz", // zfs "raidz", // zfs
"ramdisk", // word "ramdisk", // word
"ramfs", // linux "ramfs", // linux
@ -168,10 +194,14 @@
"raspberrypifw", // package "raspberrypifw", // package
"raspi3b", // cli arg "raspi3b", // cli arg
"rawdisk", // virtual box "rawdisk", // virtual box
"readlink", // command
"realpath", // program / function "realpath", // program / function
"redistributable", // word "redistributable", // word
"reexec", // option
"refreservation", // zfs "refreservation", // zfs
"relatime", // mount option "relatime", // mount option
"robotnix", // package
"rootfs", // abbr (root filesystem)
"rpiboot", // package "rpiboot", // package
"rpicm4", // abbr (Raspberry PI Compute Module 4) "rpicm4", // abbr (Raspberry PI Compute Module 4)
"rpool", // zfs "rpool", // zfs
@ -195,7 +225,10 @@
"swsuspend", // parameter "swsuspend", // parameter
"syncoid", // program "syncoid", // program
"syslinux", // package "syslinux", // package
"sysrq-trigger", // linux
"sysrq", // linux
"temproot", // abbr (temporary root (FS)) "temproot", // abbr (temporary root (FS))
"timefmt", // option
"timesync", // systemd "timesync", // systemd
"TMPDIR", // env var "TMPDIR", // env var
"tmpfiles", // nixos option "tmpfiles", // nixos option
@ -220,17 +253,19 @@
"vdev", "vdevs", // zfs "vdev", "vdevs", // zfs
"vfat", // linux "vfat", // linux
"virt", // abbr (virtualization) "virt", // abbr (virtualization)
"virtfs", // qemu / filesystem
"virtio", // cli arg "virtio", // cli arg
"virtualisation", // british english
"vmdk", // file type (virtual disk format) "vmdk", // file type (virtual disk format)
"wipefs", // program "wipefs", // program
"wiplib", // name / abbr (WIP library) "wiplib", // name / abbr (WIP library)
"workdir", // mount overlay option "workdir", // mount overlay option
"xattr", // zfs "xattr", // zfs
"xchg", // abbr (exchange)
"xfsprogs", // package "xfsprogs", // package
"ykchalresp", // program "ykchalresp", // program
"ykinfo", // program "ykinfo", // program
"yubikey", // program "yubikey", // program
"yubikey", // program
"YubiKeys", // plural "YubiKeys", // plural
"zfsutil", // program / function "zfsutil", // program / function
"zstd", "zstdcat", // program / function "zstd", "zstdcat", // program / function

View File

@ -12,10 +12,11 @@ See its [README](../lib/setup-scripts/README.md) for more documentation.
```bash ```bash
# Replace the entry point with the same function: # Replace the entry point with the same function:
function install-system {( set -eu # 1: blockDevs function install-system {( set -o pipefail -u # (void)
prepare-installer "$@" trap - EXIT # start with empty traps for sub-shell
do-disk-setup "${argv[0]}" prepare-installer || exit
install-system-to $mnt do-disk-setup "${argv[0]}" || exit
install-system-to $mnt || exit
)} )}
# ... could also replace any other function(s) ... # ... could also replace any other function(s) ...

Binary file not shown.

View File

@ -41,7 +41,7 @@ in { imports = [ ({ ## Hardware
wip.bootloader.extlinux.enable = true; wip.bootloader.extlinux.enable = true;
# Example of adding and/or overwriting setup/maintenance functions: # Example of adding and/or overwriting setup/maintenance functions:
wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; }; #wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; };
}) (lib.mkIf (name == "example-explicit") { ## Minimal explicit FS setup }) (lib.mkIf (name == "example-explicit") { ## Minimal explicit FS setup
@ -53,6 +53,7 @@ in { imports = [ ({ ## Hardware
t;1;c # type ; part1 ; W95 FAT32 (LBA) t;1;c # type ; part1 ; W95 FAT32 (LBA)
a;1 # active/boot ; part1 a;1 # active/boot ; part1
''; }; }; ''; }; };
wip.fs.boot.enable = false;
# Put everything except for /boot and /nix/store on a tmpfs. This is the absolute minimum, most usable systems require some more paths that are persistent (e.g. all of /nix and /home). # Put everything except for /boot and /nix/store on a tmpfs. This is the absolute minimum, most usable systems require some more paths that are persistent (e.g. all of /nix and /home).
fileSystems."/" = { fsType = "tmpfs"; device = "tmpfs"; neededForBoot = true; options = [ "mode=755" ]; }; fileSystems."/" = { fsType = "tmpfs"; device = "tmpfs"; neededForBoot = true; options = [ "mode=755" ]; };
@ -133,7 +134,7 @@ in { imports = [ ({ ## Hardware
## And here would go the things that actually make the host unique (and do something productive). For now just some debugging things: ## And here would go the things that actually make the host unique (and do something productive). For now just some debugging things:
environment.systemPackages = [ pkgs.curl pkgs.htop ]; environment.systemPackages = [ pkgs.curl pkgs.htop pkgs.tree ];
services.getty.autologinUser = "root"; users.users.root.password = "root"; services.getty.autologinUser = "root"; users.users.root.password = "root";

38
hosts/rpi.nix.md Normal file
View File

@ -0,0 +1,38 @@
/*
# Raspberry PI Example
## Installation
Default installation according to [`install-system`](../lib/setup-scripts/README.md#install-system-documentation):
```bash
nix run '.#rpi' -- install-system $DISK
```
Then connect `$DISK` to a PI, boot it, and (not much, because nothing is installed).
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS config flake input:
dirname: inputs: { config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
in { imports = [ ({ ## Hardware
wip.preface.hardware = "aarch64"; system.stateVersion = "21.11";
wip.hardware.raspberry-pi.enable = true;
wip.fs.disks.devices.primary.size = 31914983424; # exact size of the disk/card
## Minimal automatic FS setup
wip.fs.boot.enable = true;
wip.fs.temproot.enable = true;
wip.fs.temproot.temp.type = "tmpfs";
wip.fs.temproot.local.type = "bind";
wip.fs.temproot.local.bind.base = "f2fs";
wip.fs.temproot.remote.type = "none";
}) ({ ## Temporary Test Stuff
services.getty.autologinUser = "root";
}) ]; }

View File

@ -1,8 +1,8 @@
dirname: inputs@{ self, nixpkgs, ...}: let dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib; inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mapMergeUnique mergeAttrsUnique flipNames; inherit (import "${dirname}/vars.nix" dirname inputs) mapMerge mapMergeUnique mergeAttrsUnique flipNames;
inherit (import "${dirname}/imports.nix" dirname inputs) getNixFiles importWrapped; inherit (import "${dirname}/imports.nix" dirname inputs) getNixFiles importWrapped getOverlaysFromInputs getModulesFromInputs;
inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit; inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit extractBashFunction;
setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs); setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs);
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
inherit (import "${dirname}/misc.nix" dirname inputs) trace; inherit (import "${dirname}/misc.nix" dirname inputs) trace;
@ -26,7 +26,7 @@ in rec {
# A flake has »{ inputs; outputs; sourceInfo; } // outputs // sourceInfo«, where »inputs« is what's passed to the outputs function without »self«, and »outputs« is the result of calling the outputs function. Don't know the merge priority. # A flake has »{ inputs; outputs; sourceInfo; } // outputs // sourceInfo«, where »inputs« is what's passed to the outputs function without »self«, and »outputs« is the result of calling the outputs function. Don't know the merge priority.
if (!input?sourceInfo) then sourceInfo else (let if (!input?sourceInfo) then sourceInfo else (let
outputs = (import "${patched.outPath}/flake.nix").outputs ({ self = sourceInfo // outputs; } // input.inputs); outputs = (import "${patched.outPath}/flake.nix").outputs ({ self = sourceInfo // outputs; } // input.inputs);
in { inherit (input) inputs; inherit outputs; inherit sourceInfo; } // outputs // sourceInfo) in outputs // sourceInfo // { inherit (input) inputs; inherit outputs; inherit sourceInfo; })
)) else input) inputs); )) else input) inputs);
# Generates implicit flake outputs by importing conventional paths in the local repo. E.g.: # Generates implicit flake outputs by importing conventional paths in the local repo. E.g.:
@ -73,14 +73,19 @@ in rec {
# Builds the System Configuration for a single host. Since each host depends on the context of all other host (in the same "network"), this is essentially only callable through »mkNixosConfigurations«. # Builds the System Configuration for a single host. Since each host depends on the context of all other host (in the same "network"), this is essentially only callable through »mkNixosConfigurations«.
# See »mkSystemsFlake« for documentation of the arguments. # See »mkSystemsFlake« for documentation of the arguments.
mkNixosConfiguration = args@{ name, entryPath ? null, config ? null, preface ? null,peers ? { }, inputs ? [ ], overlays ? [ ], modules ? [ ], nixosSystem, localSystem ? null, ... }: let mkNixosConfiguration = args@{ name,
entryPath ? null, config ? null, preface ? null,
inputs ? { }, overlays ? (getOverlaysFromInputs inputs), modules ? (getModulesFromInputs inputs),
peers ? { }, nixosSystem ? inputs.nixpkgs.lib.nixosSystem, localSystem ? null,
... }: let
preface = args.preface or (getSystemPreface inputs entryPath (specialArgs // { inherit name; })); preface = args.preface or (getSystemPreface inputs entryPath (specialArgs // { inherit name; }));
targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem; targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem;
specialArgs = (args.specialArgs or { }) // { specialArgs = (args.specialArgs or { }) // {
nodes = peers; # NixOPS nodes = peers; # NixOPS
}; };
outputName = if args?renameOutputs then args.renameOutputs name else name;
in let system = { inherit preface; } // (nixosSystem { in let system = { inherit preface; } // (nixosSystem {
system = buildSystem; system = buildSystem; # (this actually does nothing more than setting »config.nixpkgs.system« and can be null here)
modules = [ { imports = [ # Anything specific to only this evaluation of the module tree should go here. modules = [ { imports = [ # Anything specific to only this evaluation of the module tree should go here.
(args.config or (importWrapped inputs entryPath).module) (args.config or (importWrapped inputs entryPath).module)
@ -110,7 +115,11 @@ in rec {
config.${prefix} = if preface?instances then { } else { preface.instances = [ name ]; }; config.${prefix} = if preface?instances then { } else { preface.instances = [ name ]; };
}) ({ }) ({
options.${prefix}.setup.scripts = lib.mkOption { options.${prefix}.setup.scripts = lib.mkOption {
description = ''Attrset of bash scripts defining functions that do installation and maintenance operations. See »./setup-scripts/README.md« below for more information.''; description = ''
Attrset of bash scripts defining functions that do installation and maintenance operations.
The functions should expect the bash options `pipefail` and `nounset` (`-u`) to be set.
See »./setup-scripts/README.md« for more information.
'';
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = { type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = {
name = lib.mkOption { description = "Symbolic name of the script."; type = lib.types.str; default = name; readOnly = true; }; name = lib.mkOption { description = "Symbolic name of the script."; type = lib.types.str; default = name; readOnly = true; };
path = lib.mkOption { description = "Path of file for ».text« to be loaded from."; type = lib.types.nullOr lib.types.path; default = null; }; path = lib.mkOption { description = "Path of file for ».text« to be loaded from."; type = lib.types.nullOr lib.types.path; default = null; };
@ -122,12 +131,13 @@ in rec {
}) ({ config, options, pkgs, ... }: { }) ({ config, options, pkgs, ... }: {
options.${prefix}.setup.appliedScripts = lib.mkOption { options.${prefix}.setup.appliedScripts = lib.mkOption {
type = lib.types.functionTo lib.types.str; readOnly = true; type = lib.types.functionTo lib.types.str; readOnly = true;
default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs; } // context; }; # inherit (builtins) trace; default = context: substituteImplicit { inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues config.${prefix}.setup.scripts); context = { inherit config options pkgs inputs outputName; } // context; }; # inherit (builtins) trace;
}; };
}) ]; _file = "${dirname}/flakes.nix#extraModules"; } ]; }) ]; _file = "${dirname}/flakes.nix#extraModules"; } ];
#specialArgs = specialArgs; # (This is already set during module import, while »_module.args« only becomes available during module evaluation (before that, using it causes infinite recursion). Since it can't be ensured that this is set in every circumstance where »extraModules« are being used, it should not be set at all.) specialArgs = { inherit outputName; };
# (This is already set during module import, while »_module.args« only becomes available during module evaluation (before that, using it causes infinite recursion). Since it can't be ensured that this is set in every circumstance where »extraModules« are being used, it should generally not be used to set custom arguments. The »outputName« is only applicable in the current evaluation anyway, though.)
}); in system; }); in system;
@ -166,11 +176,11 @@ in rec {
# 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. # 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.
systems ? ({ dir = "${inputs.self}/hosts"; exclude = [ ]; }), systems ? ({ dir = "${inputs.self}/hosts"; exclude = [ ]; }),
# List of overlays to set as »config.nixpkgs.overlays«. Defaults to ».overlays.default« of all »overlayInputs«/»inputs« (incl. »inputs.self«). # List of overlays to set as »config.nixpkgs.overlays«. Defaults to ».overlays.default« of all »overlayInputs«/»inputs« (incl. »inputs.self«).
overlays ? (lib.remove null (map (input: if input?overlays.default then input.overlays.default else if input?overlay then input.overlay else null) (builtins.attrValues overlayInputs))), overlays ? (getOverlaysFromInputs overlayInputs),
# (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«. # (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
overlayInputs ? inputs, overlayInputs ? inputs,
# List of Modules to import for all hosts, in addition to the default ones in »nixpkgs«. The host-individual module should selectively enable these. Defaults to ».nixosModules.default« of all »moduleInputs«/»inputs« (including »inputs.self«). # List of Modules to import for all hosts, in addition to the default ones in »nixpkgs«. The host-individual module should selectively enable these. Defaults to ».nixosModules.default« of all »moduleInputs«/»inputs« (including »inputs.self«).
modules ? (lib.remove null (map (input: if input?nixosModules.default then input.nixosModules.default else if input?nixosModule then input.nixosModule else null) (builtins.attrValues moduleInputs))), modules ? (getModulesFromInputs moduleInputs),
# (Subset of) »inputs« that »modules« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«. # (Subset of) »inputs« that »modules« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
moduleInputs ? inputs, moduleInputs ? inputs,
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function). # Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
@ -182,72 +192,16 @@ in rec {
## If provided, then change the name of each output attribute by passing it through this function. Allows exporting of multiple variants of a repo's hosts from a single flake: ## If provided, then change the name of each output attribute by passing it through this function. Allows exporting of multiple variants of a repo's hosts from a single flake:
renameOutputs ? null, renameOutputs ? null,
... }: let ... }: let
otherArgs = (builtins.removeAttrs args [ "systems" "renameOutputs" ]) // { inherit inputs systems overlays modules specialArgs nixosSystem localSystem; }; otherArgs = args // { inherit inputs overlays modules specialArgs nixosSystem localSystem; };
nixosConfigurations = if builtins.isList systems then mergeAttrsUnique (map (systems: mkNixosConfigurations (otherArgs // systems)) systems) else mkNixosConfigurations (otherArgs // systems); nixosConfigurations = if builtins.isList systems then mergeAttrsUnique (map (systems: mkNixosConfigurations (otherArgs // systems)) systems) else mkNixosConfigurations (otherArgs // systems);
in let outputs = { in let outputs = {
inherit nixosConfigurations; inherit nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: let } // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: let
pkgs = (import inputs.nixpkgs { inherit overlays; system = localSystem; }); pkgs = (import inputs.nixpkgs { inherit overlays; system = localSystem; });
tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites)); tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites));
PATH = lib.concatMapStringsSep ":" (pkg: "${pkg}/bin") tools;
in rec { in rec {
# Do per-host setup and maintenance things: apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = writeSystemScripts { inherit name system pkgs; }; program = "${derivation}"; }) nixosConfigurations;
# SYNOPSIS: nix run REPO#HOST [-- [sudo] [bash | -x [-c SCRIPT | FUNC ...ARGS]]]
# Where »REPO« is the path to a flake repo using »mkSystemsFlake« for it's »apps« output, and »HOST« is the name of a host it defines.
# If the first argument (after »--«) is »sudo«, then the program will re-execute itself as root using sudo (minus that »sudo« argument).
# If the (then) first argument is »bash«, or if there are no (more) arguments, it will execute an interactive shell with the variables and functions sourced (largely equivalent to »nix develop .#$host«).
# »-x« as next argument runs »set -x«. If the next argument is »-c«, it will evaluate (only) the following argument as bash script, otherwise the argument will be called as command, with all following arguments as arguments tot he command.
# Examples:
# Install the host named »$target« to the image file »/tmp/system-$target.img«:
# $ nix run .#$target -- install-system /tmp/system-$target.img
# Run an interactive bash session with the setup functions in the context of the current host:
# $ nix run /etc/nixos/#$(hostname)
# Run an root session in the context of a different host (useful if Nix is not installed for root on the current host):
# $ nix run /etc/nixos/#other-host -- sudo
apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = pkgs.writeShellScript "scripts-${name}" ''
# if first arg is »sudo«, re-execute this script with sudo (as root)
if [[ $1 == sudo ]] ; then shift ; exec sudo --preserve-env=SSH_AUTH_SOCK -- "$0" "$@" ; fi
# if the (now) first arg is »bash« or there are no args, re-execute this script as bash »--init-file«, starting an interactive bash in the context of the script
if [[ $1 == bash ]] || [[ $# == 0 && $0 == *-scripts-${name} ]] ; then
exec ${pkgs.bashInteractive}/bin/bash --init-file <(cat << "EOS"${"\n"+''
# prefix the script to also include the default init files
! [[ -e /etc/profile ]] || . /etc/profile
for file in ~/.bash_profile ~/.bash_login ~/.profile ; do
if [[ -r $file ]] ; then . $file ; break ; fi
done ; unset $file
declare -A args=( ) ; declare -a argv=( ) # some functions expect these
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS
cat $0) -i
fi
# provide installer tools (native to localSystem, not targetSystem)
hostPath=$PATH ; PATH=${PATH}
${system.config.${prefix}.setup.appliedScripts { native = pkgs; }}
# either call »$1« with the remaining parameters as arguments, or if »$1« is »-c« eval »$2«.
if [[ ''${1:-} == -x ]] ; then shift ; set -x ; fi
if [[ ''${1:-} == -c ]] ; then eval "$2" ; else "$@" ; fi
''; program = "${derivation}"; }) nixosConfigurations;
# E.g.: $ nix develop /etc/nixos/#$(hostname)
# ... and then call any of the functions in ./utils/setup-scripts/ (in the context of »$(hostname)«, where applicable).
# To get an equivalent root shell: $ nix run /etc/nixos/#functions-$(hostname) -- sudo bash
devShells = lib.mapAttrs (name: system: pkgs.mkShell {
inherit name;
nativeBuildInputs = tools ++ [ pkgs.nixos-install-tools ];
shellHook = ''
${system.config.${prefix}.setup.appliedScripts { native = pkgs; }}
# add active »hostName« to shell prompt
declare -A args=( ) ; declare -a argv=( ) # some functions expect these
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
'';
}) nixosConfigurations;
# dummy that just pulls in all system builds # dummy that just pulls in all system builds
packages.all-systems = pkgs.runCommandLocal "all-systems" { } '' packages.all-systems = pkgs.runCommandLocal "all-systems" { } ''
@ -270,8 +224,78 @@ in rec {
nixosConfigurations = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.nixosConfigurations; nixosConfigurations = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { } // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: {
apps = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.apps.${localSystem}; apps = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.apps.${localSystem};
devShells = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.devShells.${localSystem};
packages.${renameOutputs "all-systems"} = outputs.packages.${localSystem}.all-systems; packages.${renameOutputs "all-systems"} = outputs.packages.${localSystem}.all-systems;
checks.${renameOutputs "all-systems"} = outputs.checks.${localSystem}.all-systems; checks.${renameOutputs "all-systems"} = outputs.checks.${localSystem}.all-systems;
})); }));
# Do per-host setup and maintenance things:
# SYNOPSIS: nix run REPO#HOST [-- [sudo] [bash | [--command SCRIPT | FUNC] ...[ARG|OPTION]]]
# Where »REPO« is the path to a flake repo using »mkSystemsFlake« for it's »apps« output, and »HOST« is the name of a host it defines.
# If the first argument (after »--«) is »sudo«, then the program will re-execute itself as root using sudo (minus that »sudo« argument).
# If the (then) first argument is »bash«, or if there are no (more) arguments or options, it will execute an interactive shell with the variables and functions sourced.
# If an option »--command« is supplied, then the first argument evaluated as bash instructions, otherwise the first argument is called as a function (or program).
# Either way, the remaining arguments and options have been parsed by »generic-arg-parse« and are available in »argv« and »args«.
# Examples:
# Install the host named »$target« to the image file »/tmp/system-$target.img«:
# $ nix run .#$target -- install-system /tmp/system-$target.img
# Run an interactive bash session with the setup functions in the context of the current host:
# $ nix run /etc/nixos/#$(hostname)
# Run a root session in the context of a different host (useful if Nix is not installed for root on the current host):
# $ nix run /etc/nixos/#other-host -- sudo
writeSystemScripts = {
name ? system._module.args.name,
system, pkgs,
}: let
tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites));
in pkgs.writeShellScript "scripts-${name}" ''
# if first arg is »sudo«, re-execute this script with sudo (as root)
if [[ $1 == sudo ]] ; then shift ; exec sudo --preserve-env=SSH_AUTH_SOCK -- "$0" "$@" ; fi
# if the (now) first arg is »bash« or there are no args, re-execute this script as bash »--init-file«, starting an interactive bash in the context of the script
if [[ $1 == bash ]] || [[ $# == 0 && $0 == *-scripts-${name} ]] ; then
exec ${pkgs.bashInteractive}/bin/bash --init-file <(cat << "EOS"${"\n"+''
# prefix the script to also include the default init files
! [[ -e /etc/profile ]] || . /etc/profile
for file in ~/.bash_profile ~/.bash_login ~/.profile ; do
if [[ -r $file ]] ; then . $file ; break ; fi
done ; unset $file
set -o pipefail -o nounset # (do not rely on errexit)
declare -A args=( ) ; declare -a argv=( ) # some functions expect these
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS
cat $0) -i
fi
# provide installer tools (native to localSystem, not targetSystem)
hostPath=$PATH ; PATH=${lib.makeBinPath tools}
${extractBashFunction (builtins.readFile setup-scripts.utils) "generic-arg-parse"}
set -o pipefail -o nounset # (do not rely on errexit)
generic-arg-parse "$@" || exit
if [[ ''${args[debug]:-} ]] ; then # for the aliases to work, they have to be set before the functions are parsed
shopt -s expand_aliases # enable aliases in non-interactive bash
for control in return exit ; do alias $control='{
status=$? ; if ! (( status )) ; then '$control' 0 ; fi # control flow return
if ! ${pkgs.bashInteractive}/bin/bash --init-file ${system.config.environment.etc.bashrc.source} ; then '$control' $status ; fi # »|| '$control'« as an error-catch
}' ; done
fi
${system.config.${prefix}.setup.appliedScripts { native = pkgs; }}
# either call »argv[0]« with the remaining parameters as arguments, or if »$1« is »-c« eval »$2«.
if [[ ''${args[debug]:-} || ''${args[trace]:-} ]] ; then set -x ; fi
if [[ ''${args[command]:-} ]] ; then
command=''${argv[0]:?'With --command, the first positional argument must specify the commands to run.'}
argv=( "''${argv[@]:1}" ) ; set -- "''${argv[@]}" ; eval "$command"
else
entry=''${argv[0]:?}
argv=( "''${argv[@]:1}" ) ; "$entry" "''${argv[@]}"
fi
'';
} }

View File

@ -4,6 +4,14 @@ dirname: inputs@{ self, nixpkgs, ...}: let
inherit (import "${dirname}/misc.nix" dirname inputs) trace; inherit (import "${dirname}/misc.nix" dirname inputs) trace;
in rec { in rec {
## Builds an attrset that, for each file with extension »ext« in »dir«, maps the the base name of that file, to its full path.
getFilesExt = ext: dir: builtins.removeAttrs (builtins.listToAttrs (map (name: let
match = builtins.match ''^(.*)[.]${builtins.replaceStrings [ "." ] [ "[.]" ] ext}$'' name;
in if (match != null) then {
name = builtins.head match; value = "${dir}/${name}";
} else { name = ""; value = null; }) (builtins.attrNames (builtins.readDir dir)))) [ "" ];
# Builds an attrset that, for each folder that contains a »default.nix«, and for each ».nix« or ».nix.md« file in »dir«, maps the the name of that folder, or the name of the file without extension(s), to its full path. # Builds an attrset that, for each folder that contains a »default.nix«, and for each ».nix« or ».nix.md« file in »dir«, maps the the name of that folder, or the name of the file without extension(s), to its full path.
getNixFiles = dir: mapMergeUnique (name: type: if (type == "directory") then ( getNixFiles = dir: mapMergeUnique (name: type: if (type == "directory") then (
if (builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else { } if (builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else { }
@ -96,6 +104,12 @@ in rec {
overlays = builtins.concatLists (map (input: if input?overlay then [ input.overlay ] else if input?overlays then builtins.attrValues input.overlays else [ ]) (builtins.attrValues inputs)); overlays = builtins.concatLists (map (input: if input?overlay then [ input.overlay ] else if input?overlays then builtins.attrValues input.overlays else [ ]) (builtins.attrValues inputs));
} // args); } // args);
## Given an attrset of nix flake »inputs«, returns the list of all default overlays defined by those other flakes (non-recursive).
getOverlaysFromInputs = inputs: (lib.remove null (map (input: if input?overlays.default then input.overlays.default else if input?overlay then input.overlay else null) (builtins.attrValues inputs)));
## Given an attrset of nix flake »inputs«, returns the list of all default NixOS modules defined by those other flakes (non-recursive).
getModulesFromInputs = inputs: (lib.remove null (map (input: if input?nixosModules.default then input.nixosModules.default else if input?nixosModule then input.nixosModule else null) (builtins.attrValues inputs)));
# Given a list of »overlays« and »pkgs« with them applied, returns the subset of »pkgs« that was directly modified by the overlays. # Given a list of »overlays« and »pkgs« with them applied, returns the subset of »pkgs« that was directly modified by the overlays.
# (But this only works for top-level / non-scoped packages.) # (But this only works for top-level / non-scoped packages.)
getModifiedPackages = pkgs: overlays: let getModifiedPackages = pkgs: overlays: let
@ -108,29 +122,31 @@ in rec {
# This is similar to adding the path to »disabledModules«, but: # This is similar to adding the path to »disabledModules«, but:
# * leaves the module's other definitions (options, imports) untouched, preventing further breakage due to missing options # * leaves the module's other definitions (options, imports) untouched, preventing further breakage due to missing options
# * makes the disabling an option, i.e. it can be changed dynamically based on other config values # * makes the disabling an option, i.e. it can be changed dynamically based on other config values
makeNixpkgsModuleConfigOptional = specialArgs: modulePath: let # NOTE: This can only be used once per module import graph (~= NixOS configuration) and »modulePath«!
fullPath = "${specialArgs.inputs.nixpkgs.outPath}/nixos/modules/${modulePath}"; makeNixpkgsModuleConfigOptional = modulePath: extraOriginalModuleArgs: args@{ config, pkgs, lib, modulesPath, utils, ... }: let
moduleArgs = { utils = import "${specialArgs.inputs.nixpkgs.outPath}/nixos/lib/utils.nix" { inherit (specialArgs) lib config pkgs; }; } // specialArgs; fullPath = "${modulesPath}/${modulePath}";
module = import fullPath moduleArgs; module = import fullPath (args // extraOriginalModuleArgs);
in { _file = fullPath; imports = [ in { _file = fullPath; imports = [
{ options.disableModule.${modulePath} = lib.mkOption { description = "Disable the nixpkgs module ${modulePath}"; type = lib.types.bool; default = false; }; } { options.disableModule.${modulePath} = lib.mkOption { description = "Disable the nixpkgs module ${modulePath}"; type = lib.types.bool; default = false; }; }
(if module?config then ( (if module?config then (
module // { config = lib.mkIf (!specialArgs.config.disableModule.${modulePath}) module.config; } module // { config = lib.mkIf (!config.disableModule.${modulePath}) module.config; }
) else ( ) else (
{ config = lib.mkIf (!specialArgs.config.disableModule.${modulePath}) module; } { config = lib.mkIf (!config.disableModule.${modulePath}) module; }
)) ))
{ disabledModules = [ modulePath ]; } { disabledModules = [ modulePath ]; }
]; }; ]; };
## Given a path to a module, and a function that takes the instantiation of the original module and returns a partial module as override, this recursively applies that override to the original module definition. ## Given a path to a module, and a function that takes the instantiation of the original module and returns a partial module as override, this recursively merges that override onto the original module definition.
# Used as an »imports« entry, this allows for much more fine-grained overriding of the configuration (or even other parts) of a module than »makeNixpkgsModuleConfigOptional«, but the override function needs to be tailored to internal implementation details of the original module. # Used as an »imports« entry, this allows for much more fine-grained overriding of the configuration (or even other parts) of a module than »makeNixpkgsModuleConfigOptional«, but the override function needs to be tailored to internal implementation details of the original module.
# Esp. it is important to know that »mkIf« both existing in the original module and in the return from the override results in an attrset »{ _type="if"; condition; content; }«. Accessing content of an existing »mkIf« thus requires adding ».content« to the lookup path, and the »content« of returned »mkIf«s will get merged with any existing attribute of that name. # Esp., it is important to know that »mkIf« both existing in the original module and in the return from the override results in an attrset »{ _type="if"; condition; content; }«. Accessing content of an existing »mkIf« thus requires adding ».content« to the lookup path, and the »content« of returned »mkIf«s will get merged with any existing attribute of that name.
overrideNixpkgsModule = specialArgs: modulePath: override: let # Also, only use this on modules that are imported by default; otherwise, it gets really confusing if something somewhere imports the module and that has no effect.
fullPath = "${specialArgs.inputs.nixpkgs.outPath}/nixos/modules/${modulePath}"; overrideNixpkgsModule = modulePath: extraOriginalModuleArgs: override: args@{ config, pkgs, lib, modulesPath, utils, ... }: let
moduleArgs = { utils = import "${specialArgs.inputs.nixpkgs.outPath}/nixos/lib/utils.nix" { inherit (specialArgs) lib config pkgs; }; } // specialArgs; fullPath = "${modulesPath}/${modulePath}";
module = import fullPath moduleArgs; module = import fullPath (args // extraOriginalModuleArgs);
in { _file = fullPath; imports = [ overrides = lib.toList (override module);
(mergeAttrsRecursive ([ { imports = module.imports or [ ]; options = module.options or { }; config = module.config or { }; } ] ++ (lib.toList (override module)))) _file = if (lib.head overrides)?config then let pos = builtins.unsafeGetAttrPos "config" (lib.head overrides); in "${pos.file}:${toString pos.line}(override)" else "${fullPath}#override";
in { inherit _file; imports = [
(mergeAttrsRecursive ([ { imports = module.imports or [ ]; options = module.options or { }; config = module.config or { }; } ] ++ overrides))
{ disabledModules = [ modulePath ]; } { disabledModules = [ modulePath ]; }
]; }; ]; };
} }

View File

@ -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. 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. 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.

View File

@ -14,14 +14,14 @@ function gen-key-unencrypted {( set -eu # 1: usage
## Uses the hostname as a trivial key. ## Uses the hostname as a trivial key.
function gen-key-hostname {( set -eu # 1: usage function gen-key-hostname {( set -eu # 1: usage
usage=$1 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}" printf %s "@{config.networking.hostName}"
)} )}
## Obtains a key by reading it from a bootkey partition (see »add-bootkey-to-keydev«). ## Obtains a key by reading it from a bootkey partition (see »add-bootkey-to-keydev«).
function gen-key-usb-part {( set -eu # 1: usage function gen-key-usb-part {( set -eu # 1: usage
usage=$1 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}" bootkeyPartlabel=bootkey-"@{config.networking.hostName!hashString.sha256:0:8}"
cat /dev/disk/by-partlabel/"$bootkeyPartlabel" 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. ## Obtains a key by prompting for a password.
function gen-key-password {( set -eu # 1: usage function gen-key-password {( set -eu # 1: usage
usage=$1 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«. ## 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]} password=${userPasswords[$user]}
else else
password=$(prompt-new-password "that will be used as component of the key for »@{config.networking.hostName}:$usage«") 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 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. ## 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]} password=${userPasswords[$user]}
else else
password=$(prompt-new-password "as YubiKey challenge for »@{config.networking.hostName}:$usage«") 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 fi
gen-key-yubikey-challenge "$usage" "$serial:$slot:home-$user=$password" true "»${user}«'s password (for key »${usage}«)" 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«) function gen-key-yubikey-pin {( set -eu # 1: usage, 2: serialAndSlot(as »serial:slot«)
usage=$1 ; serialAndSlot=$2 usage=$1 ; serialAndSlot=$2
pin=$( prompt-new-password "/ pin as challenge to YubiKey »$serialAndSlot« as key for »@{config.networking.hostName}:$usage«" ) 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«" 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 else
read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' once with '"${message:-challenge »"$challenge"«}"'. Enter to continue, or Ctrl+C to abort:' read -p 'Challenging YubiKey '"$serial"' slot '"$slot"' once with '"${message:-challenge »"$challenge"«}"'. Enter to continue, or Ctrl+C to abort:'
fi 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 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 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 else
secret="$( @{native.yubikey-personalization}/bin/ykchalresp -"$slot" "$challenge" )" 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 fi
printf %s "$secret" | head -c 64 { printf %s "$secret" || true ; } | head -c 64
)} )}
## Generates a random secret key. ## Generates a random secret key.
function gen-key-random {( set -eu # 1: usage 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
)} )}

View File

@ -1,12 +1,7 @@
dirname: inputs: let 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 (inputs.config) prefix;
inherit (import "${dirname}/../imports.nix" dirname inputs) getFilesExt;
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: ( replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
builtins.toFile name (builtins.replaceStrings builtins.toFile name (builtins.replaceStrings
@ -16,4 +11,4 @@ dirname: inputs: let
) )
))); )));
in replacePrefix (getNamedScriptFiles dirname) in replacePrefix (getFilesExt "sh(.md)?" dirname)

View File

@ -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« 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) 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.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. # * 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 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 »diskPaths«, creates and loop-mounts images for non-/dev/ paths, and tries to abort if any partition already exists on the host. function ensure-disks { # 1: diskPaths, 2?: skipLosetup
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
declare -g -A blockDevs=( ) # this ends up in the caller's scope declare -g -A blockDevs=( ) # this ends up in the caller's scope
if [[ $1 == */ ]] ; then if [[ $1 == */ ]] ; then
@ -47,42 +45,52 @@ function partition-disks { # 1: diskPaths
else else
local path ; for path in ${1//:/ } ; do local path ; for path in ${1//:/ } ; do
local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi 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 blockDevs[$name]=$path
done done
fi fi
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do 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]}" eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
if [[ ${blockDevs[$name]} != /dev/* ]] ; then if [[ ${blockDevs[$name]} != /dev/* ]] ; then
local outFile=${blockDevs[$name]} && local outFile=${blockDevs[$name]} &&
install -o root -g root -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" && install -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" || return
blockDevs[$name]=$( losetup --show -f "$outFile" ) && prepend_trap "losetup -d '${blockDevs[$name]}'" EXIT # NOTE: this must not be inside a sub-shell! 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 else
local size=$( blockdev --getsize64 "${blockDevs[$name]}" || : ) ; local waste=$(( size - ${disk[size]} )) 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 [[ ! $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 )) ; 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 )) && [[ ! ${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 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 fi
done 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 for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
eval 'local -A part='"$partDecl" 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 done
for name in "@{!config.wip.fs.disks.devices[@]}" ; do for name in "@{!config.wip.fs.disks.devices[@]}" ; do
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}" eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
if [[ ${disk[serial]:-} ]] ; then if [[ ${disk[serial]:-} ]] ; then
actual=$( udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' ) 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 fi
# can (and probably should) restore the backup: # 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 ( 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]}]}" #partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}" || return
done done
@{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud || return @{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud || return
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet @{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]] sgdisk+=( --hybrid "${disk[mbrParts]}" ) # --hybrid: create MBR in addition to GPT; ${disk[mbrParts]}: make these GPT part 1 MBR parts 2[3[4]]
fi 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 if [[ ${disk[mbrParts]:-} ]] ; then
printf " printf %s "
M # edit hybrid MBR M # edit hybrid MBR
d;1 # delete parts 1 (GPT) d;1 # delete parts 1 (GPT)
@ -147,46 +155,44 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
${disk[extraFDiskCommands]} ${disk[extraFDiskCommands]}
p;w;q # print ; write ; quit 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 fi
} }
## Checks whether a »partition« resides on one of the provided »blockDevs«. ## Checks whether a »partition« resides on one of the provided »blockDevs«.
function is-partition-on-disks { # 1: partition, ...: blockDevs function is-partition-on-disks { # 1: partition, ...: blockDevs
local partition=$1 ; shift ; local -a blockDevs=( "$@" ) local partition=$1 ; shift ; local -a blockDevs=( "$@" )
local blockDev=$(realpath "$partition") ; if [[ $blockDev == /dev/sd* ]] ; then local blockDev=/dev/$( basename "$( readlink -f /sys/class/block/"$( basename "$( realpath "$partition" )" )"/.. )" ) || return
blockDev=$( shopt -s extglob ; echo "${blockDev%%+([0-9])}" ) [[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]] || return
else
blockDev=$( shopt -s extglob ; echo "${blockDev%%p+([0-9])}" )
fi
[[ ' '"${blockDevs[@]}"' ' == *' '"$blockDev"' '* ]]
} }
## For each filesystem in »config.fileSystems« whose ».device« is in »/dev/disk/by-partlabel/«, this creates the specified file system on that partition. ## 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 function format-partitions {
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
for fsDecl in "@{config.fileSystems[@]}" ; do for fsDecl in "@{config.fileSystems[@]}" ; do
eval 'declare -A fs='"$fsDecl" eval 'declare -A fs='"$fsDecl"
if [[ ${fs[device]} == /dev/disk/by-partlabel/* ]] ; then 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 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 else continue ; fi
#if [[ ${fs[fsType]} == ext4 && ' '${fs[formatOptions]}' ' != *' -F '* ]] ; then fs[formatOptions]+=' -F' ; 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 #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 @{native.parted}/bin/partprobe "${fs[device]}" || true
done done
for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do for swapDev in "@{config.swapDevices!catAttrs.device[@]}" ; do
if [[ $swapDev == /dev/disk/by-partlabel/* ]] ; then 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 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 else continue ; fi
( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || exit ( ${_set_x:-:} ; mkswap "$swapDev" >$beLoud 2>$beSilent ) || return
done 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 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.) # (This needs to happen before mounting.)
@ -197,7 +203,7 @@ function fix-grub-install {
if [[ ! @{config.fileSystems[$mount]:-} ]] ; then continue ; fi if [[ ! @{config.fileSystems[$mount]:-} ]] ; then continue ; fi
device=$( eval 'declare -A fs='"@{config.fileSystems[$mount]}" ; echo "${fs[device]}" ) device=$( eval 'declare -A fs='"@{config.fileSystems[$mount]}" ; echo "${fs[device]}" )
label=${device/\/dev\/disk\/by-partlabel\//} 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 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 ln -sfT ${bootLoop/\/dev/..\/..} /dev/disk/by-partlabel/$label || return
done 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:-} 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 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 if [[ ! $target || $target == none ]] ; then continue ; fi
options=,$options, ; options=${options//,ro,/,} options=,$options, ; options=${options//,ro,/,}
if ! mountpoint -q "$mnt"/"$target" ; then ( if ! mountpoint -q "$mnt"/"$target" ; then (
mkdir -p "$mnt"/"$target" || exit 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 if [[ $type == overlay ]] ; then
options=${options//,workdir=/,workdir=$mnt\/} ; options=${options//,upperdir=/,upperdir=$mnt\/} # Work and upper dirs must be in target. 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 mount -t $type -o "${options:1:(-1)}" "$source" "$mnt"/"$target" || exit
) || [[ $options == *,nofail,* || $allowFail ]] || exit ; fi # (actually, nofail already makes mount fail silently) ) || [[ $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«). ## Unmounts all file systems (that would be mounted during boot / by »mount-system«).
function unmount-system {( set -eu # 1: mnt, 2?: fstabPath function unmount-system {( set -eu # 1: mnt, 2?: fstabPath
mnt=$1 ; fstabPath=${2:-"@{config.system.build.toplevel}/etc/fstab"} 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 [[ ! $target || $target == none ]] ; then continue ; fi
if mountpoint -q "$mnt"/"$target" ; then if mountpoint -q "$mnt"/"$target" ; then
umount "$mnt"/"$target" umount "$mnt"/"$target"
fi fi
done done 3< <( { <$fstabPath grep -v '^#' ; echo ; } | tac )
)} )}

View File

@ -4,33 +4,33 @@
## ##
## Entry point to the installation, see »./README.md«. ## 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 trap - EXIT # start with empty traps for sub-shell
prepare-installer "$@" || exit prepare-installer || exit
do-disk-setup "${argv[0]}" || exit do-disk-setup "${argv[0]}" || exit
install-system-to $mnt || 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). ## 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 { # ... function prepare-installer { # (void)
generic-arg-parse "$@" || return
if [[ ${args[debug]:-} ]] ; then set -x ; fi
: ${argv[0]:?"Required: Target disk or image paths."} : ${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). 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) # (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 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 done
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do 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 done
if [[ ${SUDO_USER:-} ]] ; then # use Nix as the user who called this script, as Nix may not be set up for root 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 _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 ... 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. ## 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 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. # »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 #--debug
#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. ## 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 chmod -R u+w $mnt/@{config.environment.etc.nixos.source} || exit
fi 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) # 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 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 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: # Copy system closure to new nix store:
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi 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:-$targetSystem} )
if [[ ${args[quiet]:-} ]] ; then if [[ ${args[quiet]:-} ]] ; then
( set -o pipefail ; "${cmd[@]}" --quiet 2>&1 >/dev/null | { grep -Pe '^error:' || true ; } ) || exit "${cmd[@]}" --quiet >/dev/null 2> >( grep -Pe '^error:' || true ) || exit
else set -x ; time "${cmd[@]}" || exit ; fi elif [[ ${args[quiet]:-} ]] ; then
) || exit ; rm -rf $mnt/nix/var/nix/gcroots || exit ( 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 # 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 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): # 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) 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 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! # Done!
if [[ ${args[no-inspect]:-} ]] ; then if [[ ${args[no-inspect]:-} ]] ; then
if (( code != 0 )) ; then exit $code ; fi if (( code != 0 )) ; then \exit $code ; fi
elif [[ ${args[inspect-cmd]:-} ]] ; then elif [[ ${args[inspect-cmd]:-} ]] ; then
if (( code != 0 )) ; then exit $code ; fi if (( code != 0 )) ; then \exit $code ; fi
eval "${args[inspect-cmd]}" || exit eval "${args[inspect-cmd]}" || exit
else else
if (( code != 0 )) ; then if (( code != 0 )) ; then
( set +x ; echo "Something went wrong in the last step of the installation. Inspect the output above and the mounted system in this chroot shell to decide whether it is critical. Exit the shell with 0 to proceed, or non-zero to abort." 1>&2 ) ( 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 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 "Installation done! This shell is in a chroot in the mounted system for inspection. Exiting the shell will unmount the system." 1>&2 )
fi 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 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
#( cd $mnt ; mnt=$mnt @{native.bashInteractive}/bin/bash --init-file @{config.environment.etc.bashrc.source} )
fi fi
mkdir -p $mnt/var/lib/systemd/timesync && touch $mnt/var/lib/systemd/timesync/clock || true # save current time mkdir -p $mnt/var/lib/systemd/timesync && touch $mnt/var/lib/systemd/timesync/clock || true # save current time

View File

@ -7,7 +7,7 @@ function prompt-for-user-passwords { # (void)
userPasswords[$user]=@{config.users.users!catAttrSets.password[$user]} userPasswords[$user]=@{config.users.users!catAttrSets.password[$user]}
done done
for user in "@{!config.users.users!catAttrSets.passwordFile[@]}" ; do 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 done
} }
@ -37,37 +37,37 @@ function populate-keystore { { # (void)
if [[ "${methods[$usage]}" == home-composite || "${methods[$usage]}" == copy ]] ; then continue ; fi if [[ "${methods[$usage]}" == home-composite || "${methods[$usage]}" == copy ]] ; then continue ; fi
for attempt in 2 3 x ; do for attempt in 2 3 x ; do
if gen-key-"${methods[$usage]}" "$usage" "${options[$usage]}" | write-secret "$keystore"/"$usage".key ; then break ; fi 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
done done
for usage in "${!methods[@]}" ; do for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != home-composite ]] ; then continue ; fi 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 done
for usage in "${!methods[@]}" ; do for usage in "${!methods[@]}" ; do
if [[ "${methods[$usage]}" != copy ]] ; then continue ; fi 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 done
)} )}
## Creates the LUKS devices specified by the host using the keys created by »populate-keystore«. ## 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} keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}
for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]} 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 primaryKey="$keystore"/luks/"$luksName"/0.key
keyOptions=( --pbkdf=pbkdf2 --pbkdf-force-iterations=1000 ) 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 for index in 1 2 3 4 5 6 7 ; do
if [[ -e "$keystore"/luks/"$luksName"/"$index".key ]] ; then 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 fi
done done
done done
)} }
## Opens the LUKS devices specified by the host, using the opened host's keystore. ## Opens the LUKS devices specified by the host, using the opened host's keystore.
function open-luks-layers { # (void) function open-luks-layers { # (void)
@ -76,8 +76,7 @@ function open-luks-layers { # (void)
if [[ -e /dev/mapper/$luksName ]] ; then continue ; fi if [[ -e /dev/mapper/$luksName ]] ; then continue ; fi
rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]} rawDev=@{config.boot.initrd.luks.devices!catAttrSets.device[$luksName]}
primaryKey="$keystore"/luks/"$luksName"/0.key primaryKey="$keystore"/luks/"$luksName"/0.key
@{native.cryptsetup}/bin/cryptsetup --batch-mode luksOpen --key-file="$primaryKey" "$rawDev" "$luksName" || return
( PATH=@{native.cryptsetup}/bin ; ${_set_x:-:} ; cryptsetup --batch-mode luksOpen --key-file="$primaryKey" "$rawDev" "$luksName" ) && prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $luksName" EXIT || return
prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $luksName" EXIT
done done
} }

View File

@ -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. ## 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) ... # Currently, this only works for x64 (on x64) ...
function run-qemu {( set -eu # 1: diskImages function run-qemu { # 1: diskImages, ...: qemuArgs
generic-arg-parse "$@" if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{outputName:-@{config.system.name}}/ ; fi
diskImages=${argv[0]} diskImages=${argv[0]:?} ; argv=( "${argv[@]:1}" )
if [[ ${args[debug]:-} ]] ; then set -x ; fi
qemu=( @{native.qemu_full}/bin/qemu-system-@{config.wip.preface.hardware} ) local qemu=( )
qemu+=( -m ${args[mem]:-2048} -smp ${args[smp]:-4} )
if [[ @{config.wip.preface.hardware}-linux == "@{native.system}" && ! ${args[no-kvm]:-} ]] ; then if [[ @{pkgs.system} == "@{native.system}" ]] ; 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. 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
elif [[ @{config.wip.preface.hardware} == aarch64 ]] ; then # assume it's a raspberry PI (or compatible) 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: # TODO: this does not work yet:
qemu+=( -machine type=raspi3b -m 1024 ) ; args[no-nat]=1 qemu+=( -machine type=raspi3b -m 1024 ) ; args[no-nat]=1
# ... and neither does this: # ... and neither does this:
#qemu+=( -M virt -m 1024 -smp 4 -cpu cortex-a53 ) ; args[no-nat]=1 #qemu+=( -machine type=virt -m 1024 -smp 4 -cpu cortex-a53 ) ; args[no-nat]=1
fi # else things are going to be quite slow 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 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.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 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+=( -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 -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 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 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 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 done
if [[ @{config.boot.loader.systemd-boot.enable} || ${args[efi]:-} ]] ; then # UEFI. Otherwise it boots something much like a classic BIOS? if [[ ${args[share]:-} ]] ; then # e.g. --share='foo:/home/user/foo,readonly=on bar:/tmp/bar'
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 ) local share ; for share in ${args[share]} ; do
#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+=( -virtfs local,security_model=none,mount_tag=${share/:/,path=} )
qemu+=( -drive file=${ovmf}/FV/OVMF_CODE.fd,if=pflash,format=raw,unit=0,readonly=on ) # In the VM: $ mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 -o ro foo /foo
qemu+=( -drive file="${args[efi-vars]:-/tmp/qemu-@{config.networking.hostName}-VARS.fd}",if=pflash,format=raw,unit=1 ) done
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[@]}")" )
fi 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). # 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 local logSerial= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=ttyS0'@( |,)* ]] ; then logSerial=1 ; fi
logScreen= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=tty1 '* ]] ; then logScreen=1 ; fi local logScreen= ; if [[ ' '"@{config.boot.kernelParams[@]}"' ' == *' console=tty1 '* ]] ; then logScreen=1 ; fi
if [[ ! ${args[no-serial]:-} && $logSerial ]] ; then if [[ ! ${args[no-serial]:-} && $logSerial ]] ; then
if [[ $logScreen || ${args[graphic]:-} ]] ; then if [[ $logScreen || ${args[graphic]:-} ]] ; then
qemu+=( -serial mon:stdio ) qemu+=( -serial mon:stdio )
@ -101,8 +127,8 @@ function run-qemu {( set -eu # 1: diskImages
fi fi
fi fi
if [[ ! ${args[no-nat]:-} ]] ; then # e.g. --nat-fw=8000-:8000,8001-:8001,2022-:22 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 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 fi
# TODO: network bridging: # 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 ) #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« # 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/*-/}" ) qemu+=( -usb -device usb-host,hostbus="${decl/-*/}",hostport="${decl/*-/}" )
done ; fi 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 if [[ ${args[dry-run]:-} ]] ; then
( echo "${qemu[@]}" ) echo "${qemu[@]}"
else else
( set -x ; "${qemu[@]}" ) ( set -x ; "${qemu[@]}" ) || return
fi fi
# https://askubuntu.com/questions/54814/how-can-i-ctrl-alt-f-to-get-to-a-tty-in-a-qemu-session # 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. ## 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" # To create/clear the GPT: $ sgdisk --zap-all "$blockDev"

View File

@ -10,7 +10,7 @@
function generic-arg-parse { # ... function generic-arg-parse { # ...
declare -g -A args=( ) ; declare -g -a argv=( ) # this ends up in the caller's scope declare -g -A args=( ) ; declare -g -a argv=( ) # this ends up in the caller's scope
while (( "$#" )) ; do while (( "$#" )) ; do
if [[ $1 == -- ]] ; then shift ; argv+=( "$@" ) ; return ; fi if [[ $1 == -- ]] ; then shift ; argv+=( "$@" ) ; \return 0 ; fi
if [[ $1 == --* ]] ; then if [[ $1 == --* ]] ; then
if [[ $1 == *=* ]] ; then if [[ $1 == *=* ]] ; then
local key=${1/=*/} ; args[${key/--/}]=${1/$key=/} 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, # »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. # »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
if [[ ! ${args[help]:-} ]] ; then : ${allowedArgs[help]:=1} ; return 0 ; fi if [[ ! ${args[help]:-} ]] ; then : ${allowedArgs[help]:=1} ; \return 0 ; fi
[[ ! ${3:-} ]] || echo "$3" [[ ! ${3:-} ]] || echo "$3"
printf 'Usage:\n %s [ARG[=value]]... [--] %s\n\nWhere »ARG« may be any of:\n' "$1" "${2:-}" 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]}" 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." printf ' %s\n %s\n' "--help" "Do nothing but print this message and exit with success."
[[ ! ${4:-} ]] || echo "$4" [[ ! ${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«. ## 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 for name in "${!args[@]}" ; do
if [[ ${allowedArgs[--$name]:-} ]] ; then if [[ ${allowedArgs[--$name]:-} ]] ; then
if [[ ${args[$name]} == '' || ${args[$name]} == 1 ]] ; then continue ; fi 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 fi
if [[ $names == *' --'"$name"'='* || $names == *' --'"$name"'[='* ]] ; then continue ; 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 done
} }
## Prepends a command to a trap. Especially useful fo define »finally« commands via »prepend_trap '<command>' EXIT«. ## 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)). # 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 function prepend_trap { # 1: command, ...: trapNames
fatal() { printf "ERROR: $@\n" >&2 ; return 1 ; } fatal() { printf "ERROR: $@\n" 1>&2 ; \return 1 ; }
local cmd=$1 ; shift || fatal "${FUNCNAME} usage error" local cmd=$1 ; shift 1 || fatal "${FUNCNAME} usage error"
local name ; for name in "$@" ; do local name ; for name in "$@" ; do
trap -- "$( set +x trap -- "$( set +x
printf '%s\n' "( ${cmd} ) || true ; " 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}" )" "${name}" || fatal "unable to add to trap ${name}"
done 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). ## 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 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 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 mkdir -p -- "$(dirname "$1")"/ || exit
install -o root -g root -m 000 -T /dev/null "$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 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 chown "${2:-root:root}" -- "$1" || exit
chmod "${3:-400}" -- "$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 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 new password $1: " password1 || exit ; echo 1>&2
read -s -p "Please enter the same password again: " password2 || 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 printf %s "$password1" || exit
)} )}

View File

@ -9,31 +9,32 @@ function create-zpools { # 1: mnt
## Creates a single of the system's ZFS pools and its datasets. ## Creates a single of the system's ZFS pools and its datasets.
function create-zpool { # 1: mnt, 2: poolName function create-zpool { # 1: mnt, 2: poolName
local mnt=$1 ; local poolName=$2 ; ( set -u local mnt=$1 ; local poolName=$2
eval 'declare -A pool='"@{config.wip.fs.zfs.pools[$poolName]}" eval 'local -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
eval 'declare -a vdevs='"${pool[vdevArgs]}" eval 'local -a vdevs='"${pool[vdevArgs]}"
eval 'declare -A poolProps='"${pool[props]}" eval 'local -A poolProps='"${pool[props]}"
eval 'declare -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}" eval 'local -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
eval 'declare -A dataProps='"${dataset[props]}" eval 'local -A dataProps='"${dataset[props]}"
get-zfs-crypt-props "${dataset[name]}" dataProps local dummy ; get-zfs-crypt-props "${dataset[name]}" dataProps dummy dummy
declare -a args=( ) ; keySrc=/dev/null local -a zpoolCreate=( ) ; keySrc=/dev/null
if [[ ${dataProps[keyformat]:-} == ephemeral ]] ; then if [[ ${dataProps[keyformat]:-} == ephemeral ]] ; then
dataProps[encryption]=aes-256-gcm ; dataProps[keyformat]=hex ; dataProps[keylocation]=file:///dev/stdin ; keySrc=/dev/urandom dataProps[encryption]=aes-256-gcm ; dataProps[keyformat]=hex ; dataProps[keylocation]=file:///dev/stdin ; keySrc=/dev/urandom
fi fi
for name in "${!poolProps[@]}" ; do args+=( -o "${name}=${poolProps[$name]}" ) ; done local name ; for name in "${!poolProps[@]}" ; do zpoolCreate+=( -o "${name}=${poolProps[$name]}" ) ; done
for name in "${!dataProps[@]}" ; do args+=( -O "${name}=${dataProps[$name]}" ) ; done local name ; for name in "${!dataProps[@]}" ; do zpoolCreate+=( -O "${name}=${dataProps[$name]}" ) ; done
for index in "${!vdevs[@]}" ; do 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 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 if [[ @{config.boot.initrd.luks.devices!catAttrSets.device[$part]:-} ]] ; then
vdevs[$index]=/dev/mapper/$part vdevs[$index]=/dev/mapper/$part
else else
part=/dev/disk/by-partlabel/$part ; vdevs[$index]=$part 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 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 fi
done 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 @{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 if [[ $keySrc == /dev/urandom ]] ; then @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null ; fi
) || return
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
ensure-datasets $mnt '^'"$poolName"'($|[/])' || 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 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. # 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. # »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 function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then return ; fi if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then \return ; fi
mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes) local mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes)
filterExp=${2:-'^'} local filterExp=${2:-'^'}
tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT local tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
zfs=@{native.zfs}/bin/zfs local zfs=@{native.zfs}/bin/zfs
: 'Step-through is very verbose and breaks the loop, disabling it for this function' ; trap - debug : '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 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 'local -A dataset='"@{config.wip.fs.zfs.datasets[$name]}"
eval 'declare -A props='"${dataset[props]}" eval 'local -A props='"${dataset[props]}"
explicitKeylocation=${props[keylocation]:-} local explicitKeylocation=${props[keylocation]:-} cryptKey cryptRoot
get-zfs-crypt-props "${dataset[name]}" props 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 $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 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 if [[ ${props[mountpoint]} == "${current:-/}" ]] ; then unset props[mountpoint] ; fi
fi fi
if [[ ${props[keyformat]:-} == ephemeral ]] ; then if [[ ${props[keyformat]:-} == ephemeral ]] ; then
@ -70,51 +71,52 @@ function ensure-datasets {( set -eu # 1: mnt, 2?: filterExp
fi fi
if [[ $explicitKeylocation ]] ; then props[keylocation]=$explicitKeylocation ; fi if [[ $explicitKeylocation ]] ; then props[keylocation]=$explicitKeylocation ; fi
unset props[encryption] ; unset props[keyformat] # can't change these anyway unset props[encryption] ; unset props[keyformat] # can't change these anyway
names=$(IFS=, ; echo "${!props[*]}") ; values=$(IFS=$'\n' ; echo "${props[*]}") local propNames=$( IFS=, ; echo "${!props[*]}" )
if [[ $values != "$($zfs get -o value -H "$names" "${dataset[name]}")" ]] ; then ( local propValues=$( IFS=$'\n' ; echo "${props[*]}" )
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( "${name}=${props[$name]}" ) ; done if [[ $propValues != "$( $zfs get -o value -H "$propNames" "${dataset[name]}" )" ]] ; then
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs set "${args[@]}" "${dataset[name]}" ) local -a zfsSet=( ) ; local propName ; for propName in "${!props[@]}" ; do zfsSet+=( "${propName}=${props[$propName]}" ) ; done
) ; fi ( 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 [[ $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 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 fi
if [[ $($zfs get -o value -H keystatus "${dataset[name]}") != available ]] ; then 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 fi
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs change-key -i "${dataset[name]}" ) ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs change-key -i "${dataset[name]}" ) || exit
) ; fi ) || return ; fi
else ( # create dataset else ( # create dataset
if [[ ${props[keyformat]:-} == ephemeral ]] ; then if [[ ${props[keyformat]:-} == ephemeral ]] ; then
props[encryption]=aes-256-gcm ; props[keyformat]=hex ; props[keylocation]=file:///dev/stdin ; explicitKeylocation=file:///dev/null 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 declare -a zfsCreate=( ) ; for name in "${!props[@]}" ; do zfsCreate+=( -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]}" ) { </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]}" $zfs unload-key "${dataset[name]}" || exit
else else
if [[ $cryptRoot && $cryptRoot != ${dataset[name]} && $($zfs get -o value -H keystatus "$cryptRoot") != available ]] ; then 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 fi
declare -a args=( ) ; for name in "${!props[@]}" ; do args+=( -o "${name}=${props[$name]}" ) ; done declare -a zfsCreate=( ) ; for name in "${!props[@]}" ; do zfsCreate+=( -o "${name}=${props[$name]}" ) ; done
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${args[@]}" "${dataset[name]}" ) ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs create "${zfsCreate[@]}" "${dataset[name]}" )
fi fi
if [[ ${props[canmount]} != off ]] ; then ( if [[ ${props[canmount]} != off ]] ; then (
mount -t zfs -o zfsutil "${dataset[name]}" $tmpMnt ; trap "umount '${dataset[name]}'" EXIT 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" ) chmod 000 "$tmpMnt" && chown "${dataset[uid]}:${dataset[gid]}" -- "$tmpMnt" && chmod "${dataset[mode]}" -- "$tmpMnt"
) ; fi ) || exit ; fi
if [[ $explicitKeylocation && $explicitKeylocation != "${props[keylocation]:-}" ]] ; then 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 fi
$zfs snapshot -r "${dataset[name]}"@empty $zfs snapshot -r "${dataset[name]}"@empty || exit
) ; fi ) || return ; fi
eval 'declare -A allows='"${dataset[permissions]}" eval 'local -A allows='"${dataset[permissions]}"
for who in "${!allows[@]}" ; do for who in "${!allows[@]}" ; do
# »zfs allow $dataset« seems to be the only way to view permissions, and that is not very parsable -.- # »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
done 3< <( printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z )
)} )}

View File

@ -33,7 +33,7 @@ in rec {
then merge (attrPath ++ [ name ]) values then merge (attrPath ++ [ name ]) values
else builtins.elemAt values (builtins.length values - 1) else builtins.elemAt values (builtins.length values - 1)
); );
in merge [ ] attrsList; in if builtins.length attrsList == 1 then builtins.head attrsList else merge [ ] attrsList;
getListAttr = name: attrs: if attrs != null then ((attrs."${name}s" or [ ]) ++ (if attrs?${name} then [ attrs.${name} ] else [ ])) else [ ]; getListAttr = name: attrs: if attrs != null then ((attrs."${name}s" or [ ]) ++ (if attrs?${name} then [ attrs.${name} ] else [ ])) else [ ];
@ -116,7 +116,7 @@ in rec {
in ''${type} ${if pathSubstitute then esc path else noSub (esc path)} ${esc mode} ${esc user} ${esc group} ${esc age} ${if pathSubstitute then argument'' else noSub argument''}''; in ''${type} ${if pathSubstitute then esc path else noSub (esc path)} ${esc mode} ${esc user} ${esc group} ${esc age} ${if pathSubstitute then argument'' else noSub argument''}'';
/* /*
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
(lib.my.mkTmpfile { type = "f+"; path = "/home/user/t\"e\t%t\n!"; user = "user"; argument = " . foo\nbar\r\n\tba%!\n"; }) (lib.wip.mkTmpfile { type = "f+"; path = "/home/user/t\"e\t%t\n!"; user = "user"; argument = " . foo\nbar\r\n\tba%!\n"; })
''f+ "/home/user/test!\"!\t!%%!\x20! !\n!${"\n"}%!%a!\\!" - "user" "user" - \x20. foo%a!\nbar\r\n\tba%%!\n'' ''f+ "/home/user/test!\"!\t!%%!\x20! !\n!${"\n"}%!%a!\\!" - "user" "user" - \x20. foo%a!\nbar\r\n\tba%%!\n''
]; ];
*/ */

401
lib/vps-worker.nix.md Normal file
View File

@ -0,0 +1,401 @@
/*
# Temporary VPS Workers
This file provides a function that returns scripts and complementary config to spin up a temporary VPS worker (on Hetzner Cloud), which can be used for a job like doing a nix build, before being scraped again.
This provides a pretty cheap way to do large, automated Nix builds. Building LineageOS (via `robotnix`) costs about 0.50€ (and ~30GiB traffic to and from the device issuing the build).
This is still somewhat WIP. IO between the issuer and worker could potentially be reduced significantly, if the worker had a persistent nix store, but that is harder to set up (and potentially also relatively expensive).
For now, there is not much documentation, but here is at least a short / shortened example:
```nix
# This requires a »HCLOUD_TOKEN« either as environment variable or stored in »"$baseDir"/keys/lineage-builder.api-token«.
{ build-remote = pkgs: systems: let
builder = lib.wip.vps-worker rec {
name = "lineage-builder";
inherit pkgs inputs;
serverType = "cx41"; # "cpx51"; # "cx41" is the smallest on which this builds (8GB RAM is not enough)
tokenCmd = ''cat "$baseDir"/keys/${name}.api-token'';
suppressCreateEmail = false;
nixosConfig = { };
debug = true; ignoreKill = false;
};
nix = "PATH=${pkgs.openssh}/bin:$PATH ${pkgs.nix}/bin/nix --extra-experimental-features 'nix-command flakes'";
in pkgs.writeShellScriptBin "lineage-build-remote" ''
${lib.wip.extractBashFunction (builtins.readFile lib.wip.setup-scripts.utils) "prepend_trap"}
set -u ; set -x
baseDir=$1
# Use the remote builder for the heavy lifting:
baseDir=$baseDir ${builder.createScript} || exit ; prepend_trap "baseDir=$baseDir ${builder.killScript}" EXIT
${builder.remoteStore.testCmd} || exit
${pkgs.push-flake}/bin/push-flake ${builder.remoteStore.urlArg} ${self} || exit
results=$( mktemp -d ) || exit ; ${if builder.args.debug then "" else ''prepend_trap "rm -rf $results" EXIT''}
for device in ${lib.concatStringsSep " " (lib.attrNames systems)} ; do
${if false then ''
# The disadvantage of this is that it won't even reuse unchanged runtime dependencies, since they are not pushed to the builder. On the plus side, only the original sources and the actual build output will ever touch the issuer.
result=$( ${builder.sshCmd} -- 'nix build --no-link --print-out-paths ${self}#robotnixConfigurations.'$device'.releaseScript' ) || exit
${nix} build ${inputs.nixpkgs}#hello --out-link $results/$device || exit ; ln -sfT $result $results/$device || exit # hack to (at least locally) prevent the copied result to be GCed
${nix} copy --no-check-sigs --from ${builder.remoteStore.urlArg} $result || exit
'' else ''
# This should reuse runtime dependencies (which are copied to and from the issuer), but build dependencies are still lost with the worker (I think). Oddly enough, it also downloads some things from the caches to the issuer.
ulimit -Sn "$( ulimit -Hn )" # "these 3090 derivations will be built", and Nix locally creates a lockfile for each of then (?)
result=$results/$device
${nix} build --out-link $result ${lib.concatStringsSep " " builder.remoteStore.builderArgs} ${self}#robotnixConfigurations.$device.releaseScript
''}
mkdir -p "$baseDir"/build/$device || exit
( cd "$baseDir"/build/$device ; PATH=${pkgs.gawk}/bin:$PATH $result "$baseDir"/keys/$device ) || exit
done
''; }
```
## TODO
### Make the Worker Persistent?
* use compressed and encrypted ZFS as rootfs
* snapshots are bing compressed, though, and quite significantly so:
* ~65GiB ext4 data + ~55GiB swap resulted in ~38.5GiB snapshot, which took 7 minutes to capture.
* dropping pages from the swap does not make much of a difference
* `blkdiscard -f /dev/disk-by-partlabel/swap-...` freed 45GiB swap, remaining compressed 29GiB in 5.5 minutes
* `fstrim -a` freed another 12GiB swap and whatever else, remaining compressed 25.5GiB in 5 minutes
* any sort of encryption should prevent compression of used blocks
* use `-o compress=zstd-2` (should maybe switch to that as the default compression anyway)
* on ext4, `/.local/` had a size of 69GiB after a successful build
* suspend:
* leave gc roots on the last build, then do gc
* disable and discard swap
* take a server snapshot with a fixed name
* attach `realpath /run/current-system` as label to the snapshot
* delete the server
* wake up:
* restore the snapshot
* ssh to initrd
* create swap
* unlock zfs
* if the `realpath /run/current-system` is outdated, transfer the current system build (and delete the old one? do gc?)
* boot stage 2 into the target system
* extlinux can also chain-load, so could:
* use a super cheap cx11 server to upload a boot image to a new volume
* this would contain pretty much a normal installation
* the volume should be small, so the last one could be cached
* then create the main server, restoring the snapshot if there is one, and hooking up the volume
* if there was no snapshot, ssh into Ubuntu, install the chain-loader (using Ubuntu's /boot) pointing to the volume, reboot
* could also completely clear out the disk, install only the bootloader, and thus create a tiny snapshot to be reused for new installs
* booting from the volume, in the initrd:
* if the partitioning is not correct, create partitions, filesystems, bootloader, and swap
* copy the new system components to the persistent store
* either by starting dropbear and waiting for the master to upload, or by unpacking it from a partition on the volume
* the former would work well with supplying encryption keys as well
* maybe remove the old system gc root and gc again
* resume booting of the persistent store
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS lib:
dirname: inputs@{ self, nixpkgs, ... }: let
inherit (self) lib;
defaultInputs = inputs;
## Hetzner VPS types, as of 2023-01: »cpu« in cores, »ram« in GB, »ssd« in GB, »ph« and »pm« price in €ct per hour (o/w VAT), »pm« monthly cap in € (about 26 - 27 days).
# Plus .1ct/h (.6€/m) for the IPv4 (TODO: could add a flag to do IPv6 only):
serverTypes = {
cx11 = { cpu = 1; ram = 2; ssd = 20; ph = .6; pm = 3.92; };
cpx11 = { cpu = 2; ram = 2; ssd = 40; ph = .7; pm = 4.58; };
cx21 = { cpu = 2; ram = 4; ssd = 40; ph = .9; pm = 5.77; };
cpx21 = { cpu = 3; ram = 4; ssd = 80; ph = 1.3; pm = 8.39; };
cx31 = { cpu = 2; ram = 8; ssd = 80; ph = 1.7; pm = 10.95; };
cpx31 = { cpu = 4; ram = 8; ssd = 160; ph = 2.5; pm = 15.59; };
cx41 = { cpu = 4; ram = 16; ssd = 160; ph = 3.3; pm = 20.11; };
cpx41 = { cpu = 8; ram = 16; ssd = 240; ph = 4.9; pm = 29.39; };
cx51 = { cpu = 8; ram = 32; ssd = 240; ph = 6.4; pm = 38.56; };
cpx51 = { cpu = 16; ram = 32; ssd = 360; ph = 10.4; pm = 64.74; };
};
in ({
name, localName ? name,
pkgs, # The nixpkgs instance used to build the worker management scripts.
inputs ? defaultInputs, # The flake inputs used to evaluate the worker's config.
inheritFrom ? null, # Optional nixOS configuration from which to inherit locale settings and the like.
serverType ? "cx21", issuerSystem ? pkgs.system, vpsSystem ? "x86_64-linux",
tokenFile ? null, # File containing the Hetzner Cloud API token (»HCLOUD_TOKEN«). Only relevant if neither the environment variable »HCLOUD_TOKEN« nor the »tokenCmd« argument are set.
tokenCmd ? (if tokenFile == null then "echo 'Environment variable HCLOUD_TOKEN must be set!' 1>&2 ; false" else "cat ${lib.escapeShellArg tokenFile}"),
suppressCreateEmail ? true, # Suppress the email upon server creation. This requires the cloud project to have an SSH key named `dummy` (with any key value).
keysOutPath ? "/tmp/vps-worker-${localName}-keys", # TODO: assert that this does not need to be escaped
useZfs ? true, swapPercent ? if useZfs then 20 else 50,
nixosConfig ? { }, # Extra NixOS config to use when assembling the worker.
debug ? false, ignoreKill ? debug,
}: let
args = { inherit name localName pkgs inputs inheritFrom serverType issuerSystem vpsSystem tokenFile tokenCmd suppressCreateEmail keysOutPath useZfs swapPercent nixosConfig debug; };
esc = lib.escapeShellArg;
hash = builtins.substring 0 8 (builtins.hashString "sha256" name); dataPart = if useZfs then "rpool-${hash}" else "local-${hash}";
workerConfig = { pkgs, config, options, ... }: { _file = "${dirname}/vps-worker.nix.md#workerConfig"; imports = let
noFS = options.virtualisation?useDefaultFilesystems && config.virtualisation.useDefaultFilesystems;
in [ nixosConfig ({
system.stateVersion = builtins.substring 0 5 inputs.nixpkgs.lib.version; # really doesn't matter for these configs
wip.preface.hardware = builtins.replaceStrings [ "-linux" ] [ "" ] vpsSystem;
wip.hardware.hetzner-vps.enable = true; # (this is where the interesting stuff happens)
wip.base.enable = true;
services.openssh.enable = true;
services.openssh.extraConfig = lib.mkOrder (-1) "Include ${builtins.toFile "user-root.conf" ''Match User root
AuthorizedKeysFile /local/etc/ssh/login.pub
''}";
networking.firewall.logRefusedConnections = false; # it's super spam-my and pretty irrelevant
documentation.nixos.enable = lib.mkDefault false; # It just takes way to long to make these, and they rebuild way too often ...
nix.nixPath = [ "nixpkgs=${inputs.nixpkgs}" ];
nix.settings.experimental-features = [ "recursive-nix" "impure-derivations" ]; # might as well enable fun stuff
}) (lib.mkIf (inheritFrom != null) {
networking.domain = inheritFrom.networking.domain;
time.timeZone = inheritFrom.time.timeZone;
i18n.defaultLocale = inheritFrom.i18n.defaultLocale;
}) (lib.mkIf (!noFS) { ## Basic FS Setup
wip.fs.boot.enable = true;
wip.fs.boot.size = "128M"; # will only ever store one boot configuration
wip.fs.temproot.enable = true;
services.logind.extraConfig = "RuntimeDirectorySize=0\nRuntimeDirectoryInodesMax=0\n"; # adjusts the size of »/run/user/X/«
wip.fs.temproot.temp.mounts."/tmp".options = lib.mkIf (!useZfs) { size = 0; nr_inodes = 0; }; # nix build dirs get placed here, no this needs lots of space (but we have swap for that)
wip.fs.temproot.local = { type = "bind"; bind.base = "ext4"; }; # need to use an FS that can be resized (easily)
wip.fs.temproot.remote.type = "none"; # no need to ever back up anything
wip.fs.disks.devices.primary.size = lib.mkDefault (if useZfs then "2G" else "3G"); # define a small-ish disk (will get expanded upon boot)
wip.fs.temproot.swap = { asPartition = true; size = "128M"; }; # will get expanded upon boot
wip.fs.disks.partitions."swap-${hash}" = { index = 4; order = 250; }; # move swap part to the back, so that the start of the local part does not change when expanding both (swap can simply be re-created)
wip.fs.disks.partitions.${dataPart} = { index = 3; size = lib.mkForce "${toString ((lib.wip.parseSizeSuffix config.wip.fs.disks.devices.primary.size) / 1024 / 1024 - 512)}M"; }; # since this is no longer the last partition, it needs an explicit size
fileSystems = lib.mkIf (!useZfs) { "/.local".autoResize = true; };
security.pam.loginLimits = [ { domain = "*"; type = "-"; item = "nofile"; value = 1048576; } ]; # avoid "too many open files"
}) (lib.mkIf (useZfs && !noFS) { ## ZFS
wip.fs.temproot.local.type = lib.mkForce "zfs";
wip.fs.zfs.datasets."rpool-${hash}".props = { compression = "zstd-2"; }; # zstd-2 => 1.95x, lz4 => 1.65x (on just the initial installation)
wip.fs.temproot.temp.type = "zfs";
wip.fs.zfs.datasets."rpool-${hash}/temp".props = { refreservation = lib.mkForce null; }; # (don't need that here)
wip.fs.keystore.enable = true;
wip.fs.keystore.keys."zfs/local" = "random";
wip.fs.keystore.keys."luks/keystore-${hash}/0" = "hostname"; # TODO: change
wip.fs.zfs.pools."rpool-${hash}".props = { autoexpand = "on"; };
boot.initrd.postMountCommands = ''zpool online -e rpool-${hash} /dev/disk/by-partlabel/rpool-${hash} || fail''; # in the initrd, this does not seem to do anything
# TODO: use `services.zfs.expandOnBoot = [ "rpool-${hash}" ]`?
systemd.services.zpool-expand = {
wantedBy = [ "sshd.service" ]; before = [ "sshd.service" ];
serviceConfig.Type = "oneshot"; script = ''
target=$( ${pkgs.util-linux}/bin/blockdev --getsize64 /dev/disk/by-partlabel/rpool-${hash} )
while [[ $(
/run/booted-system/sw/bin/zpool get -Hp -o value expandsz rpool-${hash}
) != - ]] || (( size = $(
/run/booted-system/sw/bin/zpool get -Hp -o value size rpool-${hash}
) < ( $target * 90 / 100 ) )) ; do
/run/booted-system/sw/bin/zpool online -e rpool-${hash} /dev/disk/by-partlabel/rpool-${hash}
echo "waiting for rpool-${hash} to expand (currently ''${size:-??}/$target)" 1>&2 ; sleep 1
done
/run/booted-system/sw/bin/zpool list rpool-${hash} 1>&2
'';
};
}) ({ ## Debugging
wip.base.panic_on_fail = false; boot.kernelParams = [ "boot.shell_on_fail" ];# ++ [ "console=ttyS0" ];
services.getty.autologinUser = "root"; # (better than a trivial root password)
}) (lib.mkIf (!noFS) { ## Expand Partitions (local/rpool+swap)
boot.initrd.postDeviceCommands = let
noSpace = str: str; # TODO: assert that the string contains neither spaces nor single or double quotes
createPart = disk: part: lib.concatStringsSep " " [
"--set-alignment=${toString (if part.alignment != null then part.alignment else disk.alignment)}"
"--new=${toString part.index}:${noSpace part.position}:+'$partSize'"
"--partition-guid=0:${noSpace part.guid}"
"--typecode=0:${noSpace part.type}"
"--change-name=0:${noSpace part.name}"
];
# TODO: referencing »pkgs.*« directly bloats the initrd => use extraUtils instead (or just wait for systemd in initrd)
in ''( set -x
diskSize=$( blockdev --getsize64 /dev/sda )
sgdisk=' --zap-all --load-backup=${config.wip.fs.disks.partitioning}/primary.backup --move-second-header --delete ${toString config.wip.fs.disks.partitions.${dataPart}.index} --delete ${toString config.wip.fs.disks.partitions."swap-${hash}".index} '
partSize=$(( $diskSize / 1024 * ${toString (100 - swapPercent)} / 100 ))K
sgdisk="$sgdisk"' ${createPart config.wip.fs.disks.devices.primary config.wip.fs.disks.partitions.${dataPart}} '
partSize= # rest
sgdisk="$sgdisk"' ${createPart config.wip.fs.disks.devices.primary config.wip.fs.disks.partitions."swap-${hash}"} '
${pkgs.gptfdisk}/bin/sgdisk $sgdisk /dev/sda
${pkgs.parted}/bin/partprobe /dev/sda || true
dd bs=440 conv=notrunc count=1 if=${pkgs.syslinux}/share/syslinux/mbr.bin of=/dev/sda status=none || fail
waitDevice /dev/disk/by-partlabel/swap-${hash}
mkswap /dev/disk/by-partlabel/swap-${hash} || fail
)'';
}) ]; };
system = lib.wip.mkNixosConfiguration {
name = name; config = workerConfig;
preface.hardware = builtins.replaceStrings [ "-linux" ] [ "" ] vpsSystem;
inherit inputs; localSystem = issuerSystem;
renameOutputs = name: null; # (is not exported by the flake)
};
mkScript = job: cmds: pkgs.writeShellScript "${job}-vps-${localName}.sh" ''
export HCLOUD_TOKEN ; HCLOUD_TOKEN=''${HCLOUD_TOKEN:-$( ${tokenCmd} )} || exit
${cmds}
'';
prepend_trap = lib.wip.extractBashFunction (builtins.readFile lib.wip.setup-scripts.utils) "prepend_trap";
hcloud = "${pkgs.hcloud}/bin/hcloud";
ubuntu-init = pkgs.writeText "ubuntu-init" ''
#cloud-config
chpasswd: null
#ssh_pwauth: false
package_update: false
package_upgrade: false
ssh_authorized_keys:
- '@sshLoginPub@'
ssh_genkeytypes: [ ]
ssh_keys:
ed25519_public: '@sshSetupHostPub@'
ed25519_private: |
@sshSetupHostPriv_prefix8@
'';
installTokenCmd = ''
( pw= ; read -s -p "Please paste the API token for VPS worker »"${esc localName}"«: " pw ; echo ; [[ ! $pw ]] || <<<"$pw" write-secret $mnt/${esc tokenFile} )
'';
cerateCmd = ''
${prepend_trap}
set -o pipefail -u${if debug then "x" else ""}
beQuiet=cat ; if [[ ''${quiet:-} ]] ; then beQuiet=: ; fi
keys=${keysOutPath} ; rm -rf "$keys" && mkdir -p "$keys" && chmod 750 "$keys" || exit
for name in setupHost workerHost login ; do
${pkgs.openssh}/bin/ssh-keygen -q -N "" -t ed25519 -f "$keys"/$name -C $name || exit
done
echo 'Building the worker image'
image=$(mktemp -u) && prepend_trap "rm -f '$image'" EXIT
SUDO_USER= ${lib.wip.writeSystemScripts { inherit system pkgs; }} install-system --inspect-cmd='
keys='$( printf %q "$keys" )' ; if [[ -r /tmp/shared/workerHost ]] ; then keys=/tmp/shared ; fi
mkdir -p $mnt/local/etc/ssh/ || exit
cp -aT "$keys"/login.pub $mnt/local/etc/ssh/login.pub || exit
cp -aT "$keys"/workerHost $mnt/local/etc/ssh/ssh_host_ed25519_key || exit
cp -aT "$keys"/workerHost.pub $mnt/local/etc/ssh/ssh_host_ed25519_key.pub || exit
chown 0:0 $mnt/local/etc/ssh/* || exit
' ''${forceVmBuild:+--vm} --vm-shared="$keys" ${if debug then "--trace" else "--quiet"} -- $image & buildPid=$!
wait $buildPid || exit
echo 'Creating the VPS'
prepend_trap 'if [[ ! ''${buildSucceeded:-} ]] ; then ( '${esc killCmd}' ) ; fi' EXIT
cat ${ubuntu-init} |
${pkgs.perl}/bin/perl -pe 's|[@]sshLoginPub[@]|'"$( cat "$keys"/login.pub )"'|' |
${pkgs.perl}/bin/perl -pe 's|[@]sshSetupHostPub[@]|'"$( cat "$keys"/setupHost.pub )"'|' |
${pkgs.perl}/bin/perl -pe 's|[@]sshSetupHostPriv_prefix8[@]|'"$( cat "$keys"/setupHost | ${pkgs.perl}/bin/perl -pe 's/^/ /' )"'|' |
${hcloud} server create --image=ubuntu-22.04 --name=${esc name} --type=${esc serverType} --user-data-from-file - ${if suppressCreateEmail then "--ssh-key dummy" else ""} | $beQuiet || exit
# ${hcloud} server poweron ${esc name} || exit # --start-after-create=false
ip=$( ${hcloud} server ip ${esc name} ) ; echo "$ip" >"$keys"/ip
printf "%s %s\n" "$ip" "$( cat "$keys"/setupHost.pub )" >"$keys"/known_hosts
printf %s 'Preparing the VPS/worker for image transfer '
sleep 5 ; for i in $(seq 20) ; do sleep 1 ; if ${sshCmd} -- true &>/dev/null ; then break ; fi ; printf . ; done ; printf ' '
# The system takes a minimum of time to boot, so might as well chill first. Then the loop fails (loops) only before the VM is created, afterwards it blocks until sshd is up.
${sshCmd} 'set -o pipefail -u -e
# echo u > /proc/sysrq-trigger # remount all FSes as r/o (did not cut it)
mkdir /tmp/tmp-root ; mount -t tmpfs -o size=100% none /tmp/tmp-root
umount /boot/efi ; rm -rf /var/lib/{apt,dpkg} /var/cache /usr/lib/firmware /boot ; printf .
cp -axT / /tmp/tmp-root/ ; printf .
mount --make-rprivate / ; mkdir -p /tmp/tmp-root/old-root
pivot_root /tmp/tmp-root /tmp/tmp-root/old-root
for i in dev proc run sys ; do mkdir -p /$i ; mount --move /old-root/$i /$i ; done
systemctl daemon-reexec ; systemctl restart sshd
' || exit ; echo .
wait $buildPid || exit ; echo 'Writing worker image to VPS'
cat $image | ${pkgs.zstd}/bin/zstd | ${sshCmd} 'set -o pipefail -u -e
</dev/null fuser -mk /old-root 2>&1 | '$beQuiet' ; sleep 2
</dev/null umount /old-root
</dev/null blkdiscard -f /dev/sda &>/dev/null
</dev/null sync # this seems to be crucial
zstdcat >/dev/sda
</dev/null sync # this seems to be crucial
' || exit
${hcloud} server reset ${esc name} | $beQuiet || exit
printf "%s %s\n" "$ip" "$( cat "$keys"/workerHost.pub )" >"$keys"/known_hosts
printf %s 'Waiting for the worker to boot '
sleep 2 ; for i in $(seq 20) ; do sleep 1 ; if ${sshCmd} -- true &>/dev/null ; then buildSucceeded=1 ; break ; fi ; printf . ; done ; echo
if [[ ! ''${buildSucceeded:-} ]] ; then echo 'Unable to connect to VPS worker, it may not have booted correctly ' 1>&2 ; exit 1 ; fi
echo '${sshCmd} "$@"' >"$keys"/ssh ; chmod 555 "$keys"/ssh
echo ${remoteStore.urlArg} >"$keys"/store ; echo ${remoteStore.builderArg} >"$keys"/builder
echo 'nix ${lib.concatStringsSep " " remoteStore.builderArgs} "$@"' >"$keys"/remote ; chmod 555 "$keys"/remote
'';
sshCmd = ''${pkgs.openssh}/bin/ssh -oUserKnownHostsFile=${keysOutPath}/known_hosts -i ${keysOutPath}/login root@$( cat ${keysOutPath}/ip )'';
killCmd = if ignoreKill then ''echo 'debug mode, keeping server '${esc name}'' else ''${hcloud} server delete ${esc name}'';
remoteStore = rec {
urlArg = '''ssh://root@'$( cat ${keysOutPath}/ip )'?compress=true&ssh-key='${keysOutPath}'/login&base64-ssh-public-host-key='$( cat ${keysOutPath}/workerHost.pub | ${pkgs.coreutils}/bin/base64 -w0 )'';
builderArg = (lib.concatStringsSep "' '" [
"'ssh://root@'$( cat ${keysOutPath}/ip )'?compress=true'" # 1. URL (including the keys, the URL gets too ong to create the lockfile path)
"i686-linux,x86_64-linux" # 2. platform type
"${keysOutPath}/login" # 3. SSH login key
"${toString (serverTypes.${serverType} or { cpu = 4; }).cpu}" # 4. max parallel builds
"-" # 5. speed factor (relative to other builders, so irrelevant)
"nixos-test,benchmark,big-parallel" # 6. builder supported features (no kvm)
"-" # 7. job required features
''$( cat ${keysOutPath}/workerHost.pub | ${pkgs.coreutils}/bin/base64 -w0 )'' # 8. builder host key
]);
builderArgs = [
"--max-jobs" "0" # don't build locally
"--builders-use-substitutes" # prefer loading from public cache over loading from build issuer
"--builders" builderArg
];
testCmd = ''PATH=${pkgs.openssh}/bin:$PATH ${pkgs.nix}/bin/nix --extra-experimental-features nix-command store ping --store ${urlArg}'';
};
shell = pkgs.writeShellScriptBin "shell-${name}" ''
quiet=1 ${createScript} || exit ; trap ${killScript} EXIT || exit
${pkgs.bashInteractive}/bin/bash --init-file ${pkgs.writeText "init-${name}" ''
# Execute bash's default logic if no --init-file was provided (to inherit from a normal shell):
! [[ -e /etc/profile ]] || . /etc/profile
for file in ~/.bash_profile ~/.bash_login ~/.profile ; do
if [[ -r $file ]] ; then . $file ; break ; fi
done ; unset $file
ip=$( cat ${keysOutPath}/ip )
keys=${keysOutPath}
ssh="${sshCmd}"
store=${remoteStore.urlArg}
builder=${remoteStore.builderArg}
alias remote="nix ${lib.concatStringsSep " " remoteStore.builderArgs}"
PATH=${lib.makeBinPath [ pkgs.push-flake ]}:$PATH
ulimit -Sn "$( ulimit -Hn )" # Chances are, we want to run really big builds. They might run out of file descriptors for local lock files.
# »remote build ...« as non-root fails until root runs it to the point of having made some build progress?
# This doesn't do the trick, though:
# sudo ${pkgs.nix}/bin/nix build ${lib.concatStringsSep " " remoteStore.builderArgs} --no-link --print-out-paths --impure --expr '(import <nixpkgs> { }).writeText "rand" "'$( xxd -u -l 16 -p /dev/urandom )'"'
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$} # append name to the prompt
''} || exit
'';
createScript = mkScript "create" cerateCmd;
killScript = mkScript "kill" killCmd;
in {
inherit args installTokenCmd cerateCmd sshCmd killCmd createScript killScript shell;
inherit remoteStore;
inherit workerConfig system;
})

View File

@ -9,21 +9,22 @@ Things that really should be (more like) this by default.
```nix ```nix
#*/# end of MarkDown, beginning of NixOS module: #*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let dirname: inputs: specialArgs@{ config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
cfg = config.${prefix}.base; cfg = config.${prefix}.base;
outputName = specialArgs.outputName or null;
in { in {
options.${prefix} = { base = { options.${prefix} = { base = {
enable = lib.mkEnableOption "saner defaults"; enable = lib.mkEnableOption "saner defaults";
includeInputs = lib.mkOption { description = "The system's build inputs, to be included in the flake registry, and on the »NIX_PATH« entry, such that they are available for self-rebuilds and e.g. as »pkgs« on the CLI."; type = lib.types.attrsOf lib.types.anything; apply = lib.filterAttrs (k: v: v != null); default = { }; }; includeInputs = lib.mkOption { description = "The system's build inputs, to be included in the flake registry, and on the »NIX_PATH« entry, such that they are available for self-rebuilds and e.g. as »pkgs« on the CLI."; type = lib.types.attrsOf lib.types.anything; apply = lib.filterAttrs (k: v: v != null); default = { }; };
panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; example = false; }; # It's stupidly hard to remove items from lists ... panic_on_fail = lib.mkEnableOption "Kernel parameter »boot.panic_on_fail«" // { default = true; example = false; }; # It's stupidly hard to remove items from lists ...
autoUpgrade = lib.mkEnableOption "automatic NixOS updates and garbage collection" // { default = cfg.includeInputs?self.nixosConfigurations.${name}; defaultText = lib.literalExpression "config.${prefix}.base.includeInputs?self.nixosConfigurations.\${name}"; example = false; }; autoUpgrade = lib.mkEnableOption "automatic NixOS updates and garbage collection" // { default = outputName != null && cfg.includeInputs?self.nixosConfigurations.${outputName}; defaultText = lib.literalExpression "config.${prefix}.base.includeInputs?self.nixosConfigurations.\${outputName}"; example = false; };
bashInit = lib.mkEnableOption "pretty defaults for interactive bash shells" // { default = true; example = false; }; bashInit = lib.mkEnableOption "pretty defaults for interactive bash shells" // { default = true; example = false; };
}; }; }; };
# Bugfix: # Bugfix:
imports = [ (lib.wip.overrideNixpkgsModule ({ inherit inputs; } // specialArgs) "misc/extra-arguments.nix" (old: { config._module.args.utils = old._module.args.utils // { imports = [ (lib.wip.overrideNixpkgsModule "misc/extra-arguments.nix" { } (old: { config._module.args.utils = old._module.args.utils // {
escapeSystemdPath = s: builtins.replaceStrings [ "/" "-" " " "." ] [ "-" "\\x2d" "\\x20" "\\x2e" ] (lib.removePrefix "/" s); # BUG(PR): The original function does not escape ».«, resulting in mismatching names with units generated from paths with ».« in them (e.g. overwrites for implicit mount units). escapeSystemdPath = s: builtins.replaceStrings [ "/" "-" " " "." ] [ "-" "\\x2d" "\\x20" "\\x2e" ] (lib.removePrefix "/" s); # BUG(PR): The original function does not escape ».«, resulting in mismatching names with units generated from paths with ».« in them (e.g. overwrites for implicit mount units).
}; })) ]; }; })) ];
@ -37,6 +38,7 @@ in {
documentation.man.enable = lib.mkDefault config.documentation.enable; documentation.man.enable = lib.mkDefault config.documentation.enable;
nix.settings.auto-optimise-store = lib.mkDefault true; # file deduplication, see https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-store-optimise.html#description nix.settings.auto-optimise-store = lib.mkDefault true; # file deduplication, see https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-store-optimise.html#description
boot.loader.timeout = lib.mkDefault 1; # save 4 seconds on startup boot.loader.timeout = lib.mkDefault 1; # save 4 seconds on startup
services.getty.helpLine = lib.mkForce "";
system.extraSystemBuilderCmds = (if !config.boot.initrd.enable then "" else '' system.extraSystemBuilderCmds = (if !config.boot.initrd.enable then "" else ''
ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise) ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise)
@ -46,6 +48,17 @@ in {
if [[ -e /run/current-system ]] ; then ${pkgs.nix}/bin/nix --extra-experimental-features nix-command store diff-closures /run/current-system "$systemConfig" ; fi if [[ -e /run/current-system ]] ; then ${pkgs.nix}/bin/nix --extra-experimental-features nix-command store diff-closures /run/current-system "$systemConfig" ; fi
''; deps = [ "etc" ]; }; # (to deactivate this, set »system.activationScripts.diff-systems = lib.mkForce "";«) ''; deps = [ "etc" ]; }; # (to deactivate this, set »system.activationScripts.diff-systems = lib.mkForce "";«)
virtualisation = lib.wip.mapMerge (vm: { ${vm} = let
config' = config.virtualisation.${vm};
in {
virtualisation.graphics = lib.mkDefault false;
virtualisation.writableStore = lib.mkDefault false;
# BUG(PR): When removing all device definitions, also don't use the »resumeDevice«:
boot.resumeDevice = lib.mkIf (!config'.virtualisation?useDefaultFilesystems || config'.virtualisation.useDefaultFilesystems) (lib.mkVMOverride "");
}; }) [ "vmVariant" "vmVariantWithBootLoader" "vmVariantExec" ];
}) ({ }) ({
# Robustness/debugging: # Robustness/debugging:
@ -54,13 +67,13 @@ in {
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot. systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
}) (lib.mkIf (cfg.includeInputs?self.nixosConfigurations.${name}) { # non-flake }) (lib.mkIf (outputName != null && cfg.includeInputs?self.nixosConfigurations.${outputName}) { # non-flake
# Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it: # Importing »<nixpkgs>« as non-flake returns a lambda returning the evaluated Nix Package Collection (»pkgs«). The most accurate representation of what that should be on the target host is the »pkgs« constructed when building it:
system.extraSystemBuilderCmds = '' system.extraSystemBuilderCmds = ''
ln -sT ${pkgs.writeText "pkgs.nix" '' ln -sT ${pkgs.writeText "pkgs.nix" ''
# Provide the exact same version of (nix)pkgs on the CLI as in the NixOS-configuration (but note that this ignores the args passed to it; and it'll be a bit slower, as it partially evaluates the host's configuration): # Provide the exact same version of (nix)pkgs on the CLI as in the NixOS-configuration (but note that this ignores the args passed to it; and it'll be a bit slower, as it partially evaluates the host's configuration):
args: (builtins.getFlake ${builtins.toJSON cfg.includeInputs.self}).nixosConfigurations.${name}.pkgs args: (builtins.getFlake ${builtins.toJSON cfg.includeInputs.self}).nixosConfigurations.${outputName}.pkgs
''} $out/pkgs # (nixpkgs with overlays) ''} $out/pkgs # (nixpkgs with overlays)
''; # (use this indirection so that all open shells update automatically) ''; # (use this indirection so that all open shells update automatically)
@ -93,7 +106,8 @@ in {
system.autoUpgrade = { system.autoUpgrade = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
flake = config.environment.etc.nixos.source; flags = map (dep: if dep == "self" then "" else "--update-input ${dep}") (builtins.attrNames cfg.includeInputs); # there is no "--update-inputs"? flake = "${config.environment.etc.nixos.source}#${outputName}";
flags = map (dep: if dep == "self" then "" else "--update-input ${dep}") (builtins.attrNames cfg.includeInputs); # there is no "--update-inputs"
# (Since all inputs to the system flake are linked as system-level flake registry entries, even "indirect" references that don't really exist on the target can be "updated" (which keeps the same hash but changes the path to point directly to the nix store).) # (Since all inputs to the system flake are linked as system-level flake registry entries, even "indirect" references that don't really exist on the target can be "updated" (which keeps the same hash but changes the path to point directly to the nix store).)
dates = "05:40"; randomizedDelaySec = "30min"; dates = "05:40"; randomizedDelaySec = "30min";
allowReboot = lib.mkDefault false; allowReboot = lib.mkDefault false;
@ -112,7 +126,7 @@ in {
if [[ $1 == -h ]] ; then echo "$help" ; exit 0 ; fi if [[ $1 == -h ]] ; then echo "$help" ; exit 0 ; fi
if [[ $1 == -- ]] ; then shift ; break ; fi ; pkgs+=( "$1" ) if [[ $1 == -- ]] ; then shift ; break ; fi ; pkgs+=( "$1" )
} ; shift ; done } ; shift ; done
if (( ''${#pkgs[@]} == 0 )) ; then echo "$help" ; exit 1 ; fi if (( ''${#pkgs[@]} == 0 )) ; then echo "$help" 1>&2 ; exit 1 ; fi
if (( "$#" == 0 )) ; then set -- bash --login ; fi if (( "$#" == 0 )) ; then set -- bash --login ; fi
nix-shell --run "$( printf ' %q' "$@" )" -p "''${pkgs[@]}" nix-shell --run "$( printf ' %q' "$@" )" -p "''${pkgs[@]}"
#function run { bash -xc "$( printf ' %q' "$@" )" ; } #function run { bash -xc "$( printf ' %q' "$@" )" ; }

View File

@ -15,11 +15,11 @@ This uses the same implementation as `boot.loader.generic-extlinux-compatible` t
```nix ```nix
#*/# end of MarkDown, beginning of NixOS module: #*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let dirname: inputs: args@{ config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
cfg = config.${prefix}.bootloader.extlinux; cfg = config.${prefix}.bootloader.extlinux;
targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.wip.parentPaths cfg.targetDir); in config.fileSystems.${path}; targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.wip.parentPaths cfg.targetDir); in config.fileSystems.${path};
supportedFSes = [ "vfat" ]; fsSupported = fs: builtins.elem fs supportedFSes; supportedFSes = [ "vfat" "ntfs" "ext2" "ext3" "ext4" "btrfs" "xfs" "ufs" ]; fsSupported = fs: builtins.elem fs supportedFSes;
in { in {
options.${prefix} = { bootloader.extlinux = { options.${prefix} = { bootloader.extlinux = {
@ -52,12 +52,12 @@ in {
esc = lib.escapeShellArg; esc = lib.escapeShellArg;
in lib.mkIf cfg.enable ({ in lib.mkMerge [ (lib.mkIf cfg.enable {
assertions = [ { assertions = [ {
assertion = cfg.allowInstableTargetPart || (builtins.match ''^/dev/disk/by-(id|label|partlabel|partuuid|uuid)/.*[^/]$'' cfg.targetPart) != null; assertion = cfg.allowInstableTargetPart || (builtins.match ''^/dev/disk/by-(id|label|partlabel|partuuid|uuid)/.*[^/]$'' cfg.targetPart) != null;
message = '' message = ''
`config.${prefix}.bootloader.extlinux.targetPart` is not set to a stable path in `/dev/disk/by-{id,label,partlabel,partuuid,uuid}/`. Not using a unique identifier (or even using a path that can unexpectedly change) is very risky. `config.${prefix}.bootloader.extlinux.targetPart` is set to `${cfg.targetPart}`, which is not a stable path in `/dev/disk/by-{id,label,partlabel,partuuid,uuid}/`. Not using a unique identifier (or even using a path that can unexpectedly change) is very risky.
''; '';
} { } {
assertion = fsSupported targetMount.fsType; assertion = fsSupported targetMount.fsType;
@ -73,19 +73,16 @@ in {
system.boot.loader.id = "extlinux"; system.boot.loader.id = "extlinux";
system.build.installBootLoader = "${pkgs.writeShellScript "install-extlinux.sh" '' system.build.installBootLoader = "${pkgs.writeShellScript "install-extlinux.sh" ''
if [[ ! ''${1:-} || $1 != /nix/store/* ]] ; then echo "Usage: $0 TOPLEVEL_PATH" ; exit 1 ; fi if [[ ! ''${1:-} || $1 != /nix/store/* ]] ; then echo "Usage: $0 TOPLEVEL_PATH" 1>&2 ; exit 1 ; fi
export PATH=$PATH:${pkgs.stdenv}/bin
${extlinux-conf-builder} "$1" -d ${esc cfg.targetDir} ${extlinux-conf-builder} "$1" -d ${esc cfg.targetDir}
partition=${esc cfg.targetPart} partition=${esc cfg.targetPart}
diskDev=$( realpath "$partition" ) || exit ; if [[ $diskDev == /dev/sd* ]] ; then diskDev=/dev/$( basename "$( readlink -f /sys/class/block/"$( basename "$( realpath "$partition" )" )"/.. )" ) || exit
diskDev=$( shopt -s extglob ; echo "''${diskDev%%+([0-9])}" )
else
diskDev=$( shopt -s extglob ; echo "''${diskDev%%p+([0-9])}" )
fi
if [[ $( cat ${esc cfg.targetDir}/extlinux/installedVersion 2>/dev/null || true ) != ${esc cfg.package} ]] ; then if [[ $( cat ${esc cfg.targetDir}/extlinux/installedVersion 2>/dev/null || true ) != ${esc cfg.package} ]] ; then
if ! output=$( ${esc cfg.package}/bin/extlinux --install --heads=64 --sectors=32 ${esc cfg.targetDir}/extlinux 2>&1 ) ; then if ! output=$( ${esc cfg.package}/bin/extlinux --install --heads=64 --sectors=32 ${esc cfg.targetDir}/extlinux 2>&1 ) ; then
printf '%s\n' "$output" ; exit 1 printf '%s\n' "$output" 1>&2 ; exit 1
fi fi
printf '%s\n' ${esc cfg.package} >${esc cfg.targetDir}/extlinux/installedVersion printf '%s\n' ${esc cfg.package} >${esc cfg.targetDir}/extlinux/installedVersion
fi fi
@ -93,7 +90,7 @@ in {
dd bs=440 conv=notrunc count=1 if=${esc cfg.package}/share/syslinux/mbr.bin of=$diskDev status=none || exit dd bs=440 conv=notrunc count=1 if=${esc cfg.package}/share/syslinux/mbr.bin of=$diskDev status=none || exit
fi fi
if [[ '${toString cfg.showUI}' ]] ; then # showUI if [[ ${toString cfg.showUI} ]] ; then # showUI
for lib in libutil menu ; do for lib in libutil menu ; do
if ! ${pkgs.diffutils}/bin/cmp --quiet ${esc cfg.targetDir}/extlinux/$lib.c32 ${esc cfg.package}/share/syslinux/$lib.c32 ; then if ! ${pkgs.diffutils}/bin/cmp --quiet ${esc cfg.targetDir}/extlinux/$lib.c32 ${esc cfg.package}/share/syslinux/$lib.c32 ; then
cp ${esc cfg.package}/share/syslinux/$lib.c32 ${esc cfg.targetDir}/extlinux/$lib.c32 cp ${esc cfg.package}/share/syslinux/$lib.c32 ${esc cfg.targetDir}/extlinux/$lib.c32
@ -109,5 +106,14 @@ in {
boot.loader.grub.enable = false; boot.loader.grub.enable = false;
}); }) (
(lib.mkIf (options.virtualisation?useDefaultFilesystems) { # (»nixos/modules/virtualisation/qemu-vm.nix« is imported, i.e. we are building a "vmVariant")
${prefix} = {
bootloader.extlinux.enable = lib.mkIf config.virtualisation.useDefaultFilesystems (lib.mkVMOverride false);
bootloader.extlinux.allowInstableTargetPart = lib.mkVMOverride true; # (»/dev/sdX« etc in the VM are stable (if the VM is invoked the same way))
};
})
) ];
} }

View File

@ -69,8 +69,6 @@ in let module = {
}) ({ }) ({
boot.initrd.supportedFilesystems = [ "vfat" ];
boot.initrd.luks.devices."keystore-${hash}" = { boot.initrd.luks.devices."keystore-${hash}" = {
device = "/dev/disk/by-partlabel/keystore-${hash}"; device = "/dev/disk/by-partlabel/keystore-${hash}";
postOpenCommands = '' postOpenCommands = ''
@ -100,6 +98,10 @@ in let module = {
''; '';
}; };
}) (lib.mkIf (config.boot.initrd.luks.devices?"keystore-${hash}") { # (this will be false when overwritten by »nixos/modules/virtualisation/qemu-vm.nix«)
boot.initrd.supportedFilesystems = [ "vfat" ];
boot.initrd.postMountCommands = '' boot.initrd.postMountCommands = ''
${if (lib.any (lib.wip.matches "^home/.*$") (lib.attrNames cfg.keys)) then '' ${if (lib.any (lib.wip.matches "^home/.*$") (lib.attrNames cfg.keys)) then ''
echo "Transferring home key composites" echo "Transferring home key composites"

View File

@ -319,7 +319,7 @@ in {
}); });
}); });
# TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms." # TODO: "F2FS and its tools support various parameters not only for configuring on-disk layout, but also for selecting allocation and cleaning algorithms."
boot.initrd.kernelModules = [ type ]; # This is not generally, but sometimes, required to boot. Strange. (Kernel message: »request_module fs-f2fs succeeded, but still no fs?«) boot.initrd.kernelModules = lib.mkIf (config.fileSystems?${cfg.local.bind.source}) [ config.fileSystems.${cfg.local.bind.source}.fsType ]; # This is not generally, but sometimes, required to boot. Strange. (Kernel message: »request_module fs-f2fs succeeded, but still no fs?«)
})) (lib.mkIf (cfg.remote.type == "none") { })) (lib.mkIf (cfg.remote.type == "none") {

View File

@ -10,7 +10,7 @@ Additionally, this module sets some defaults for ZFS (but only in a "always bett
```nix ```nix
#*/# end of MarkDown, beginning of NixOS module: #*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let dirname: inputs: { config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
cfg = config.${prefix}.fs.zfs; cfg = config.${prefix}.fs.zfs;
prefix = inputs.config.prefix; prefix = inputs.config.prefix;
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName); hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
@ -59,7 +59,7 @@ in let module = {
}; }; }; };
config = let config = let
in lib.mkIf cfg.enable (lib.mkMerge [ ({ in lib.mkMerge [ (lib.mkIf cfg.enable (lib.mkMerge [ ({
# boot.(initrd.)supportedFilesystems = [ "zfs" ]; # NixOS should figure that out itself based on zfs being used in »config.fileSystems«. # boot.(initrd.)supportedFilesystems = [ "zfs" ]; # NixOS should figure that out itself based on zfs being used in »config.fileSystems«.
# boot.zfs.extraPools = [ ]; # Don't need to import pools that have at least one dataset listed in »config.fileSystems« / with ».mount != false«. # boot.zfs.extraPools = [ ]; # Don't need to import pools that have at least one dataset listed in »config.fileSystems« / with ».mount != false«.
@ -93,8 +93,8 @@ in let module = {
canmount = lib.mkOptionDefault "off"; mountpoint = lib.mkOptionDefault "none"; # Assume the pool root is a "container", unless overwritten. canmount = lib.mkOptionDefault "off"; mountpoint = lib.mkOptionDefault "none"; # Assume the pool root is a "container", unless overwritten.
}; }) cfg.pools; }; }) cfg.pools;
# All pools that have at least one dataset that (explicitly or implicitly) has a key to be loaded from »/run/keystore-.../zfs/« have to be imported in the initramfs while the keystore is open: # All pools that have at least one dataset that (explicitly or implicitly) has a key to be loaded from »/run/keystore-.../zfs/« have to be imported in the initramfs while the keystore is open (but only if the keystore is not disabled):
fs.zfs.extraInitrdPools = (lib.mapAttrsToList (name: _: lib.head (lib.splitString "/" name)) (lib.filterAttrs (name: { props, ... }: if props?keylocation then lib.wip.startsWith "file:///run/keystore-" props.keylocation else config.${prefix}.fs.keystore.keys?"zfs/${name}") cfg.datasets)); fs.zfs.extraInitrdPools = lib.mkIf (config.boot.initrd.luks.devices?"keystore-${hash}") (lib.mapAttrsToList (name: _: lib.head (lib.splitString "/" name)) (lib.filterAttrs (name: { props, ... }: if props?keylocation then lib.wip.startsWith "file:///run/keystore-${hash}/" props.keylocation else config.${prefix}.fs.keystore.keys?"zfs/${name}") cfg.datasets));
# Might as well set some defaults for all partitions required (though for all but one at least some of the values will need to be changed): # Might as well set some defaults for all partitions required (though for all but one at least some of the values will need to be changed):
fs.disks.partitions = lib.wip.mapMergeUnique (name: { ${name} = { # (This also implicitly ensures that no partition is used twice for zpools.) fs.disks.partitions = lib.wip.mapMergeUnique (name: { ${name} = { # (This also implicitly ensures that no partition is used twice for zpools.)
@ -145,7 +145,7 @@ in let module = {
fi fi
''); '');
#setsid ${extraUtils}/bin/ash -c "exec ${extraUtils}/bin/ash < /dev/$console >/dev/$console 2>/dev/$console" #setsid ${extraUtils}/bin/ash -c "exec ${extraUtils}/bin/ash < /dev/$console >/dev/$console 2>/dev/$console"
boot.zfs = if (builtins.substring 0 5 inputs.nixpkgs.lib.version) == "22.05" then { } else { allowHibernation = true; }; boot.zfs = if (options.boot.zfs?allowHibernation) then { allowHibernation = true; } else { };
}) (let ## Implement »cfg.pools.*.autoApplyDuringBoot« and »cfg.pools.*.autoApplyOnActivation«: }) (let ## Implement »cfg.pools.*.autoApplyDuringBoot« and »cfg.pools.*.autoApplyOnActivation«:
@ -179,7 +179,12 @@ in let module = {
}; # these are sorted alphabetically, unless one gets "lifted up" by some other ending on it via its ».deps« field }; # these are sorted alphabetically, unless one gets "lifted up" by some other ending on it via its ».deps« field
}) ]); }) ])) (
# Disable this module in VMs without filesystems:
lib.mkIf (options.virtualisation?useDefaultFilesystems) { # (»nixos/modules/virtualisation/qemu-vm.nix« is imported, i.e. we are building a "vmVariant")
${prefix}.fs.zfs.enable = lib.mkIf config.virtualisation.useDefaultFilesystems (lib.mkVMOverride false);
}
) ];
}; verbose = { }; verbose = {

View File

@ -15,10 +15,10 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, ... }: let inherit (inputs.sel
in { in {
imports = [ imports = [
(lib.wip.overrideNixpkgsModule ({ inherit inputs; } // specialArgs) "profiles/qemu-guest.nix" (module: { (args@{ config, pkgs, lib, modulesPath, utils, ... }: {
options.profiles.qemu-guest.enable = (lib.mkEnableOption "qemu-guest profile"); options.profiles.qemu-guest.enable = (lib.mkEnableOption "qemu-guest profile");
config = lib.mkIf config.profiles.qemu-guest.enable module; config = lib.mkIf config.profiles.qemu-guest.enable (import "${modulesPath}/profiles/qemu-guest.nix" args);
})) })
# Could do this automatically for all files in the directory ... # Could do this automatically for all files in the directory ...
]; ];

158
modules/vm-exec.nix.md Normal file
View File

@ -0,0 +1,158 @@
/*
# Qemu Exec VM
This module configures a "VM Variant" of the system that allows executing (pretty) arbitrary commands in a very lightweight qemu VM, with full access to the host's Nix store, and in the context of the systems Kernel and it's modules.
This is, for example, used to install the system to images without requiring root access (on the host).
## Usage
```bash
nix run .../nixos-config'#'nixosConfigurations.${hostName}.config.system.build.vmExec -- [--quiet] [--initrd-console] [--shared=/host/path/to/shared] "bash commands to run in VM" [-- ...extra-qemu-options]
```
* `--initrd-console` shows NixOS'es stage 1 boot output on the console (otherwise it is silenced).
* `--quiet` aims to suppress the terminal (re-)setting and all non-command output. Note that this filters the VM output.
* `--shared=` specifies an optional path to a host path that is read-write mounted at `/tmp/shared` in the VM.
* The value of the first positional argument is executed as a bash script in the VM and (if nothing else goes wrong) its exit status becomes that of the overall command.
* Any other positional arguments (that aren't parsed as named arguments, so put them after the `--` marker) are passed verbatim to the `qemu` launch command. These could for example attach disk or network devices.
## Notes
* The host's `/nix/var/nix/db` is read-only shared into the VM and then overlayed with a tmpfs. Modifying the Nix DBs on the host may have funny effects, esp. when also doing writing operations in the VM.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, modulesPath, extendModules, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.virtualisation.vmVariantExec;
in {
options = { virtualisation.vmVariantExec = lib.mkOption {
description = lib.mdDoc ''Machine configuration to be added to the system's qemu exec VM.'';
inherit (extendModules { modules = [ "${modulesPath}/virtualisation/qemu-vm.nix" ]; }) type;
default = { }; visible = "shallow";
}; };
config = {
system.build.vmExec = (let vmPkgs = pkgs; in let
name = "run-${config.system.name}-vm-exec";
launch = "${cfg.system.build.vm}/bin/${cfg.system.build.vm.meta.mainProgram}";
pkgs = if cfg.virtualisation?host.pkgs then cfg.virtualisation.host.pkgs else vmPkgs;
in pkgs.runCommand "nixos-vm" {
preferLocalBuild = true; meta.mainProgram = name;
} ''
mkdir -p $out/bin
ln -s ${cfg.system.build.toplevel} $out/system
ln -s ${pkgs.writeShellScript name ''
${lib.wip.extractBashFunction (builtins.readFile lib.wip.setup-scripts.utils) "generic-arg-parse"}
generic-arg-parse "$@" ; set -- ; set -o pipefail -u #; set -x
script=''${argv[0]:?'The first positional argument must be the script to execute in the VM'} ; argv=( "''${argv[@]:1}" )
tmp=$( mktemp -d nix-vm.XXXXXXXXXX --tmpdir ) && trap "rm -rf '$tmp'" EXIT || exit
mkdir -p $tmp/{xchg,shared} && printf '%s\n' "$script" >$tmp/xchg/script && chmod +x $tmp/xchg/script || exit
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
${cfg.virtualisation.qemu.package}/bin/qemu-img create -f qcow2 $tmp/dummyImage 4M &>/dev/null # do this silently
export NIX_DISK_IMAGE=$tmp/dummyImage
export TMPDIR=$tmp USE_TMPDIR=1 SHARED_DIR=''${args[shared]:-$tmp/shared}
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 ; }
else
${launch} "''${argv[@]}" || exit
fi
if [[ -e $tmp/xchg/exit ]] ; then \exit "$( cat $tmp/xchg/exit )" ; fi
echo "Execution in VM failed!" 1>&2 ; \exit 1
''} $out/bin/${name}
'');
} // { virtualisation.vmVariantExec = lib.mkMerge [ ({
# Instead of tearing down the initrd environment, adjust some mounts and run the »command« in the initrd:
boot.initrd.postMountCommands = ''
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
chmod 1777 /tmp
# Nix want's to create lock files, even on read-only operations:
mkdir -p -m 755 /nix/var/nix/db.work /nix/var/nix/db.upper /nix/var/nix/db
mount -t overlay overlay -o lowerdir=/nix/var/nix/db.lower,workdir=/nix/var/nix/db.work,upperdir=/nix/var/nix/db.upper /nix/var/nix/db
# Nix insists on setting the ownership of »/nix/store« to »0:30000« (if run as root(?) and it is something else, e.g. when using »nix-user-chroot«):
mkdir -p -m 755 /nix/store.work /nix/store.upper /nix/store
mount -t overlay overlay -o lowerdir=/nix/store.lower,workdir=/nix/store.work,upperdir=/nix/store.upper /nix/store
# »/run/{booted,current}-system« is more for debugging than anything else, but changing »/lib/modules« makes modprobe use the full system's modules, instead of only the initrd ones:
toplevel=$(dirname $stage2Init)
ln -sfT $toplevel /run/current-system
ln -sfT $toplevel /run/booted-system
ln -sfT $toplevel/kernel-modules/lib/modules /lib/modules
# ALso mostly dor debugging shells:
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)
# »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
printf '%s\n' 'root:x:0:' 'nixbld:x:30000:' >/etc/group
export HOME=/root USER=root ; mkdir -p -m 700 $HOME
PATH=/run/current-system/sw/bin ; rm -f /bin /sbin ; unset LD_LIBRARY_PATH
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=$?
echo $exit >/tmp/xchg/exit
sync ; sync
echo 1 > /proc/sys/kernel/sysrq
echo o > /proc/sysrq-trigger
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«
}) ({
fileSystems = lib.mkVMOverride {
"/nix/var/nix/db.lower" = {
fsType = "9p"; device = "nix-var-nix-db"; neededForBoot = true;
options = [ "trans=virtio" "version=9p2000.L" "msize=65536" "ro" ];
};
"/nix/store".options = lib.mkAfter [ "ro" "msize=65536" ];
"/nix/store".mountPoint = "/nix/store.lower";
}; # mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=65536 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)
}) ({
fileSystems = lib.mkVMOverride { "/" = lib.mkForce {
fsType = "tmpfs"; device = "tmpfs"; neededForBoot = true;
options = [ "mode=1777" "noatime" "nosuid" "nodev" "size=50%" ];
}; };
virtualisation.diskSize = 4; #MB, not needed at all
# tag this to make clearer what's what
system.nixos.tags = [ "vmExec" ];
system.build.isVmExec = true;
}) ]; };
}

View File

@ -92,5 +92,13 @@ in {
platforms = lib.platforms.linux; platforms = lib.platforms.linux;
}; };
}; };
# e.g.: override a python package:
pythonPackagesExtensions = (prev.pythonPackagesExtensions or [ ]) ++ [ (final: prev: {
mox = prev.mox.overridePythonAttrs (old: {
disabled = false; # (with the way that "disabled" is currently being evaluated, this does not apply in time)
# (other attributes should work, though)
});
}) ];
} }
```` ````

View File

@ -0,0 +1,7 @@
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in lib.mapAttrs (name: path: (
pkgs.writeShellScriptBin name (
lib.wip.substituteImplicit { inherit pkgs; scripts = [ path ]; context = { inherit dirname inputs pkgs lib; }; }
)
)) (lib.wip.getFilesExt "sh(.md)?" dirname)

View File

@ -0,0 +1,18 @@
# 1: targetStore, 2?: flakePath
set -o pipefail -u
targetStore=${1:?} ; if [[ $targetStore != *://* ]] ; then targetStore='ssh://'$targetStore ; fi
flakePath=$( @{pkgs.coreutils}/bin/realpath "${2:-.}" ) || exit
# TODO: this only considers top-level inputs
# TODO: the names in lock.nodes.* do not necessarily match those in inputs.* (there is the lock.nodes.root mapping)
storePaths=( $( PATH=@{pkgs.git}/bin:$PATH @{pkgs.nix}/bin/nix --extra-experimental-features 'nix-command flakes' eval --impure --expr 'let
lock = builtins.fromJSON (builtins.readFile "'"$flakePath"'/flake.lock");
flake = builtins.getFlake "'"$flakePath"'"; inherit (flake) inputs;
in builtins.concatStringsSep " " ([ flake.outPath ] ++ (map (name: inputs.${name}.outPath) (
(builtins.filter (name: lock.nodes.${name}.original.type == "indirect") (builtins.attrNames inputs))
)))' --raw ) ) || exit
: ${storePaths[0]:?}
PATH=@{pkgs.openssh}/bin:@{pkgs.hostname-debian}/bin:@{pkgs.gnugrep}/bin:$PATH @{pkgs.nix}/bin/nix --extra-experimental-features 'nix-command flakes' copy --to "$targetStore" ${storePaths[@]} || exit ; echo ${storePaths[0]}
# ¿¿Why does something there call »hostname -I«, which is apparently only available in the debian version of hostname??

View File

@ -0,0 +1,51 @@
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index ae9be08c8d8..ce0d51d6b29 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -62,7 +62,7 @@ with lib;
Type `sudo systemctl start display-manager' to
start the graphical user interface.
- '';
+ '' + "\n";
# We run sshd by default. Login via root is only possible after adding a
# password via "passwd" or by adding a ssh key to /home/nixos/.ssh/authorized_keys.
diff --git a/nixos/modules/services/ttys/getty.nix b/nixos/modules/services/ttys/getty.nix
index 22ae9c27e5b..6eb96a6fc9f 100644
--- a/nixos/modules/services/ttys/getty.nix
+++ b/nixos/modules/services/ttys/getty.nix
@@ -102,7 +102,7 @@ in
# Note: this is set here rather than up there so that changing
# nixos.label would not rebuild manual pages
services.getty.greetingLine = mkDefault ''<<< Welcome to NixOS ${config.system.nixos.label} (\m) - \l >>>'';
- services.getty.helpLine = mkIf (config.documentation.nixos.enable && config.documentation.doc.enable) "\nRun 'nixos-help' for the NixOS manual.";
+ services.getty.helpLine = mkIf (config.documentation.nixos.enable && config.documentation.doc.enable) "\nRun 'nixos-help' for the NixOS manual.\n";
systemd.services."getty@" =
{ serviceConfig.ExecStart = [
@@ -152,7 +152,6 @@ in
${config.services.getty.greetingLine}
${config.services.getty.helpLine}
-
'';
};
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index 4963d9f3f9e..458871fe0e7 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -190,11 +190,7 @@ in
'';
# Some more help text.
- services.getty.helpLine =
- ''
-
- Log in as "root" with an empty password.
- '';
+ services.getty.helpLine = "\nLog in as \"root\" with an empty password.\n";
# Containers should be light-weight, so start sshd on demand.
services.openssh.enable = mkDefault true;

View File

@ -0,0 +1,13 @@
diff --git a/lib/modules.nix b/lib/modules.nix
index caabfee5710..bd39e8666eb 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -449,7 +449,7 @@ rec {
}
else
# shorthand syntax
- lib.throwIfNot (isAttrs m) "module ${file} (${key}) does not look like a module."
+ if ! (isAttrs m) then throw "module ${file} (${key}) does not look like a module." else
{ _file = toString m._file or file;
key = toString m.key or key;
disabledModules = m.disabledModules or [];