refactoring: create separate repo for "installer"

This commit is contained in:
Niklas Gollenstede 2023-06-16 02:14:51 +02:00
parent dd8fc696f4
commit 5c44084024
66 changed files with 873 additions and 2715 deletions

82
.vscode/settings.json vendored
View File

@ -1,7 +1,6 @@
{
"markdown.validate.ignoredLinks": [
"./modules/",
"./modules/fs/",
"./patches/",
"./example/",
],
@ -15,7 +14,6 @@
"ashift", // zfs
"askpass", // program
"attrset", "attrsets", // nix/abbr (attribute set)
"autoexpand", // zfs option
"autologin", // agetty
"autotrim", // zpool property
"binfmt", // abbr "binary format"
@ -25,16 +23,14 @@
"bootable", // word (~= able to boot)
"bootkey", // partition name
"bootloader", // word
"bootpart", // abbr (boot partition)
"bridgeadapter", // virtual box
"bridgedifs", // virtual box
"btrfs", // filesystem
"builtins", // nix
"cachefile", // zfs
"canmount", // zfs
"cdetect", // abbr
"checksumming", // word
"cmake", // program
"cmds", // abbr (commands)
"compress_chksum", // f2fs
"concat", // abbr
"controlvm", // virtual box
@ -44,75 +40,46 @@
"createrawvmdk", // virtual box
"createvm", // virtual box
"cryptsetup", // program
"ctxify", // abbr (~= add context)
"CWD", // abbr
"cykerway", // name
"dedup", // zfs
"deps", // abbr dependencies
"devs", // abbr (devices)
"diffutils", // package
"dir_nlink", // ext4 option
"dmask", // mount
"dnodesize", // zfs
"dontStrip", // nixos
"dontUnpack", // nixos
"dosfstools", // package
"dpkg", // file format
"draid", // zfs
"dropbear", // program
"dtoverlay", // option
"dtparam", // option
"e2fsprogs", // package
"eeprom", // abbr
"elif", // abbr (else if)
"encryptionroot", // zfs
"errexit", // bash
"expandsz", // zfs property
"extglob", // cli arg
"errtrace", // bash
"extlinux", // program
"extra_isize", // ext4 option
"fallocate", // program / function
"fdisk", // program
"fetchpatch", // nix
"fetchurl", // nix function
"filesystems", // plural
"findutils", // package
"firejail", // program
"firmwareLinuxNonfree", // nixos
"fmask", // mount
"foldl", // nix (fold left)
"foldr", // nix (fold right)
"fstrim", // program
"FUNCNAME", // bash var
"fuser", // optoin
"fw_printenv", // program
"fw_setenv", // program
"gcroots", // Nix
"gdisk", // program
"genkeytypes", // concat
"getsize64", // cli arg
"getty", // serice
"gids", // abbr/plural (group IDs)
"gnugrep", // package
"gnused", // package
"gollenstede", // name
"gpio", // abbr (general purpose IO)
"gptfdisk", // package
"hcloud", // program
"headlessly", // word
"hetzner", // comapny
"HISTCONTROL", // bash option
"hetzner", // name
"hostbus", // cli arg
"hostfwd", // cli arg
"hostiocache", // virtual box
"hostport", // cli arg
"hugepages", // linux
"ignoredups", // bash option
"ignorespace", // bash option
"inetutils", // package
"inodes", // plural
"internalcommands", // virtual box
"ints", // plural
"journalctl", // program
"keydev", // cli arg
"keyformat", // zfs
"keylocation", // zfs
@ -120,32 +87,23 @@
"kmod", // linux
"lazy_itable_init", // ext4 option
"lazytime", // f2fs
"leds", // plural
"libblockdev", // package
"libraspberrypi", // program
"libubootenv", // package
"libutil", // concat
"logbias", // zfs
"logind", // program
"losetup", // program / function
"lowerdir", // mount overlay option
"lsusb", // program / function
"luks", // linux
"macaddr", // cli arg
"metadata_csum", // ext4 option
"mkdir", // program / function
"mkenvimage", // program
"mktemp", // program / function
"mmap", // abbr "memory map"
"modifyvm", // virtual box
"mountpoint", // program / function
"msize", // option
"mtab", // linux
"namei", // program
"namespacing", // word
"netbootxyz", // option
"netdev", // cli arg
"netns", // linux
"niklas", // name
"nixbld", // nix
"nixos", // (duh)
@ -157,16 +115,13 @@
"nodiscard", // cli arg
"noexec", // mount option
"nofail", // cli arg
"nofile", // option (number of files)
"nographic", // cli arg
"noheadings", // cli arg
"nohibernate", // kernel param
"noprofile", // option
"nosuid", // mount option
"notrunc", // dd option
"nounset", // bash
"ntfs", // filesystem
"ondemand", // concat
"oneshot", // systemd
"optimise", // B/E
"ostype", // virtual box
@ -175,8 +130,6 @@
"partlabel", // linux
"partprobe", // program / function
"partuuid", // linux
"passthru", // nix
"pbkdf", // cli arg
"pflash", // cli arg
"pipefail", // bash
"pkgs", // nix
@ -185,51 +138,37 @@
"posix", // word
"posixacl", // zfs
"poweroff", // program / function
"poweron", // concat
"pwauth", // abbr (password authentication)
"preconfigured", // word
"qcow", // file type (qemu)
"raidz", // zfs
"ramdisk", // word
"ramfs", // linux
"raspberrypi", // abbr
"raspberrypifw", // package
"raspi3b", // cli arg
"rawdisk", // virtual box
"readlink", // command
"realpath", // program / function
"redistributable", // word
"reexec", // option
"refreservation", // zfs
"relatime", // mount option
"robotnix", // package
"rootfs", // abbr (root filesystem)
"rpiboot", // package
"rpicm4", // abbr (Raspberry PI Compute Module 4)
"rpool", // zfs
"rprivate", // linux
"sandboxing", // word
"sata", // storage protocol
"sbabic", // name
"screenshotpng", // virtual box
"setsid", // program / function
"setuid", // cli arg
"sgdisk", // program
"shopt", // bash option
"showvminfo", // virtual box
"sigs", // cli arg
"socat", // program / function
"specialisation", "specialisations", // nixos option
"startvm", // virtual box
"stdenv", // nix
"storageattach", // virtual box
"stty", // program / function
"sublist", // Nix
"swsuspend", // parameter
"syncoid", // program
"syslinux", // package
"sysrq-trigger", // linux
"sysrq", // linux
"temproot", // abbr (temporary root (FS))
"timefmt", // option
"timesync", // systemd
"TMPDIR", // env var
"tmpfiles", // nixos option
@ -238,28 +177,22 @@
"typecode", // cli arg
"uart", "uarts", // serial protocol
"uartmode", // virtual box
"uboot", // program
"udev", // program
"udevadm", // program
"udptunnel", // program
"UEFI", // abbr
"uids", // abbr/plural (group IDs)
"unencrypted", // ~= not encrypted / decrypted
"unpartitioned", // word
"upperdir", // mount overlay option
"upstreamed", // word
"urandom", // linux
"usbhid", // kmod
"vboxusers", // virtual box
"vcgencmd", // program
"vdev", "vdevs", // zfs
"vfat", // linux
"virt", // abbr (virtualization)
"virtfs", // qemu / filesystem
"virtio", // cli arg
"virtualisation", // british english
"vmdk", // file type (virtual disk format)
"wipefs", // program
"wiplib", // name / abbr (WIP library)
"workdir", // mount overlay option
"xattr", // zfs
"xchg", // abbr (exchange)
@ -269,6 +202,5 @@
"yubikey", // program
"YubiKeys", // plural
"zfsutil", // program / function
"zstd", "zstdcat", // program / function
],
}

109
README.md
View File

@ -1,88 +1,57 @@
# Work In Progress Nix(OS) Library
# Automated NixOS CLI Installer
The idea of this repo / flake is that whenever I have a Nix function, NixOS Module, nixpkgs package/overlay, related bash script, or combination of those that I need in more than one project, I first put it here so that it can be shared between them.
NixOS is traditionally either installed by creating and populating filesystems [by hand](https://nixos.org/manual/nixos/stable/index.html#sec-installation-manual-partitioning), or by scripts that each only support one or a limited set of filesystem setups (the graphical installer falls somewhere between the two).
The mounted filesystems and some drivers, etc, would then be captured in a `hardware-configuration.nix`.
Eventually I may decide to move parts of this into their own flake repository, but as long as they live here, APIs are not necessarily stable.
In contrast to that, this flake implements a very flexible, yet fully automated, NixOS installer (framework).
Hosts can define any number of [disks and partitions](./modules/setup/disks.nix.md) on them.
If the `fileSystems` use `partlabel`s to identify their devices, then they can be associated with their partitions even before they are formatted -- and can thus automatically be formatted during the installation.
ZFS [pools and datasets](./modules/setup/zfs.nix.md), and LUKS and ZFS [encryption](./modules/setup/keystore.nix.md) are also supported.
For setups with ephemeral `/` (root filesystem), [`modules/setup/temproot.nix.md`](./modules/setup/temproot.nix.md) provides various preconfigured setups.
This, together with convenient defaults for most of the options, means that
simple setups (see the `minimal` [example](./hosts/example.nix.md)) only require a handful of config lines, while complex multi-disk setups (see the `raidz` [example](./hosts/example.nix.md)) are just as possible.
The more interesting of the components currently in this repository are largely concerned with good structures for Nix flakes, in particular those defining NixOS configurations, and installing hosts from those configurations.
A set of composable [`setup-scripts`](./lib/setup-scripts/) can then [automatically](https://github.com/NiklasGollenstede/nix-functions/blob/master/lib/scripts.nix#substituteImplicit) grab this information and perform a completely automated installation.
The only thing that the scripts will interactively prompt for are external secrets (e.g., passwords), iff required by the new host.
When using [`mkSystemsFlake`](./lib/nixos.nix#mkSystemsFlake), the installation can be started with:
```bash
nix run .'#'hostname -- install-system /path/to/disk
```
Help output with information on available commands and flags is available via:
```bash
nix run .'#'hostname -- --help
```
[`config.installer.commands.*`](./modules/installer.nix.md) can be used to run host-specific commands at various points of the installation, and additional `config.installer.scripts` can [add or replace](./lib/setup-scripts/README.md) new and existing setup commands or functions.
<!-- This mechanism has been used to, for example, [automatically restore of ZFS backups](#TODO) during the installation, or to [automatically deploy](#TODO) locally built system images tp Hetzner VPSes. -->
## Repo Layout
## Repo Layout/Contents
This is a nix flake repository, so [`flake.nix`](./flake.nix) is the entry point and export mechanism for almost everything.
[`lib/`](./lib/) adds additional library functions as `.wip` to the default `nixpkgs.lib` and exports the whole thing as `lib`. Other folders in this repo may thus use them as `inputs.self.lib.wip.*`. \
[`lib/setup-scripts/`](./lib/setup-scripts/) contains bash scripts that integrate with the options defined in [`modules/fs/`](./modules/fs/) (esp. [`modules/fs/disks.nix.md`](./modules/fs/disks.nix.md)) and some default options to do flexible and fully automated installations of configured NixOS hosts.
[`lib/`](./lib/) defines new library functions which are exported as the `lib` flake output. Other Nix files in this repo use them as `inputs.self.lib`. \
[`setup-scripts`](./lib/setup-scripts/) contains the implementation for the default setup (system installation and maintenance) commands.
[`modules/`](./modules/) contains NixOS configuration modules. Added options' names start with `wip.` (or a custom prefix, see [Namespacing](#namespacing-in-nixos)).
The modules are inactive by default, and are, where possible, designed to be independent from each other and the other things in this repo. Some though do have dependencies on added or modified packages, or other modules in the same directory.
[`modules/default.nix`](./modules/default.nix) exports an attr set of the modules defined in the individual files, which is also what is exported as `flake#outputs.nixosModules` and merged as `flake#outputs.nixosModule`.
[`modules/`](./modules/) contains NixOS configuration modules.
[`bootloader/extlinux`](./modules/bootloader/extlinux.nix.md) enables `extlinux` as alternative bootloader for legacy BIOS environments, because GRUB refuses top be installed to loop-mounted images.
The modules in [`setup`](./modules/setup/) allow defining a NixOS system's disk and filesystem setup in sufficient detail that a fully automatic installation is possible.
The [`installer`](./modules/installer.nix.md) module composes the [`setup-scripts`](./lib/setup-scripts/) and the host's `config` into its individual installer.
[`overlays/`](./overlays/) contains nixpkgs overlays. Some modify packages from `nixpkgs`, others add packages not in there (yet).
[`overlays/default.nix`](./overlays/default.nix) exports an attr set of the overlays defined in the individual files, which is also what is exported as `flake#outputs.overlays` and merged as `flake#outputs.overlay`. Additionally, the added or modified packages are exported as `flake#outputs.packages.<arch>.*`.
There is currently only [one overlay](./overlays/gptfdisk.nix.md) that applies [a patch](./patches/gptfdisk-move-secondary-table.patch) to `sgdisk` (it allows moving the backup GPT table, see [gptfdisk #32](https://sourceforge.net/p/gptfdisk/code/merge-requests/32/)).
[`patches/`](./patches/) contains patches which are either applied to the flake's inputs in [`flake.nix`](./flake.nix) or to packages in one of the [`overlays/`](./overlays/).
[`hosts/example`](./hosts/example.nix.md) provides some NixOS host definitions that demonstrate different types of disk setups.
[`hosts/`](./hosts/) contains the main NixOS config modules for each host. Generally, there is one file for each host, but the [flake](./flake.nix) can be instructed to reuse the config for multiple hosts (in which case the module should probably interpret the `name` argument passed to it).
Any `wip.preface.*` options have to be set in the first sub-module in these files (`## Hardware` section). \
This flake only defines a single [`example`](./hosts/example.nix.md) host meant to demonstrate how other flakes can use the (NixOS) flake library framework.
[`example/`](./example/) contains an example of adjusting the [installation](./example/install.sh.md) script for the hosts, and this flake's [default config](./example/defaultConfig/) (see [Namespacing](#namespacing-in-nixos)).
[`example/`](./example/) contains examples for customizing the [installation](./example/install.sh.md) script for the hosts, and this flake's [default config](./example/defaultConfig/flake.nix).
## Namespacing in NixOS
## License
One of the weak points of NixOS is namespacing. NixOS is traditionally based on the `nixpkgs` monorepo.
All files in this repository ([`nixos-installer`](https://github.com/NiklasGollenstede/nixos-installer)), except `./LICENSE`, are authored by the authors of this repository, and are copyright 2022 - present Niklas Gollenstede.
The `pkgs` package set is intentionally a global namespace, so that different parts of the system by default use the same instance of each respective package (unless there is a specific reason not to).
The caller to the top-level function constructing a NixOS system can provide `lib` as a set of Nix library functions. This library set is provided as global argument to all imported modules. `nixpkgs` has its default `lib` set, which its modules depend on.
If a flake exports `nixosModules` to be used by another flake to construct systems, then those modules either need to restrict themselves to the default `lib` (in the expectation that that is what will be passed) or instruct the caller to attach some additional functions (exported together with the modules) to `lib`. The former leads to code duplication within the modules, the latter is an additional requirement on the caller, and since `lib` is global, naming conflicts in the `lib` required by different modules are quite possible. The same problem applies to the strategy of supplying additional global arguments to the modules.
Since a nix flake exports instantiated Nix language constructs, not source code, it is possible to define the modules in their source code files wrapped in an outer function, which gets called by the exporting flake before exporting. Consequently, it can supply arguments which are under control of the module author, providing a library set tailored to and exposed exclusively to the local modules, thus completely avoiding naming conflicts.
NixOS modules however define their configuration options in a hierarchical, but global, namespace, and some of those options are necessarily meant to be accessed from modules external to the defining flake.
Usually, for any given module, an importing flake would only have the option to either include a module or not. If two modules define options of conflicting names, then they can't be imported at the same time, even if they could otherwise coexist.
The only workaround (that I could come up with) is to have a flake-level option that allows to change the names of the options defined in the modules exported by that flake, for example by changing their first hierarchical label.
Since flakes are purely functional, the only way to provide configuration to a flake as a whole (as opposed to exporting parts of the flake as functions, which would break the convention on flake exports) is via the flakes `inputs`, and those inputs must be flakes themselves.
The inputs have defaults defined by the flake itself, but can be overridden by the importing flake.
A flake using the modules exported by this flake may thus accept the default that all options are defined under the prefix `wip.`, or it may override its `config` input by a flake of the same shape as [`example/defaultConfig/`](./example/defaultConfig/) but with a different `prefix`.
As a local experiment, the result of running this in a `nix repl` is sufficient:
```nix
:b (import <nixpkgs> { }).writeTextDir "flake.nix" ''
{ outputs = { ... }: {
prefix = "<str>";
}; }
''
```
## Other Concepts
### `.xx.md` files
Often, the concept expressed by a source code file is at least as important as the concrete implementation of it.
`nix` unfortunately isn't super readable and also does not have documentation tooling support nearly on par with languages like TypeScript.
Embedding the source code "file" within a MarkDown file emphasizes the importance of textual expressions of the motivation and context of each piece of source code, and should thus incentivize writing sufficient documentation.
Having the documentation right next to the code should also help against documentation rot.
Technically, Nix (and most other code files) don't need to have any specific file extension. By embedding the MarkDown header in a block comment, the file can still be a valid source code file, while the MarDown header ending in a typed code block ensures proper syntax highlighting of the source code in editors or online repos.
## Notepad
### `nix repl`
```nix
pkgs = import <nixpkgs> { }
:lf . # load CWD's flake's outputs as variables
pkgs = nixosConfigurations.target.pkgs
```
### TODOs
See [`patches/README.md#license`](./patches/README.md#license) for the licensing of the included [patches](./patches/).
All other parts of this software may be used under the terms of the MIT license, as detailed in [`./LICENSE`](./LICENSE).
This license applies to the files in this repository only.
Any external packages are built from sources that have their own licenses, which should be the ones indicated in the package's metadata.

View File

@ -1,5 +1,20 @@
# The default »config« of <github:NiklasGollenstede/nix-wiplib>.
# (Not a convention-conformant flake, just a workaround to supply configuration to a flake as a flake input.)
# The default »config« flake input for this repo. It influences the exports made by this repo's main flake.
# To customize these options, copy the directory containing this file into a calling flake, and set »inputs.<this-repo>.inputs.config.url = "path:./rel/path/to/copied/dir"«.
{ outputs = { ... }: {
prefix = "wip"; # The prefix to define NixOS configuration options as.
# Moving from a monorepo (`nixpkgs`) to compositions of independent repositories, it is likely that different things will end up with the same name.
# The hierarchical structure and input/output sematic of Nix flakes can avoid most naming conflicts.
#
# NixOS modules, however, define their configuration options in a hierarchical, but global, namespace, and some of those options are necessarily meant to be accessed from modules external to the defining flake.
# Usually, for any given module, an importing flake would only have the option to either include a module or not. If two modules define options of conflicting names, then they can't be imported at the same time, even if they could otherwise coexist.
#
# The only workaround (that I could come up with) is to have a flake-level option that allows to change the names of the options defined in the modules exported by that flake.
# To rename the options exported by this flake's modules, change the values of this attrset:
rename = {
installer = "installer"; # config.${installer}
setup = "setup"; # config.${setup}
preface = "preface"; # config.${preface}
extlinux = "extlinux"; # config.boot.loader.${extlinux}
preMountCommands = "preMountCommands"; # config.fileSystems.*.${preMountCommands}
};
}; }

View File

@ -1,22 +1,40 @@
#!/usr/bin/env bash
: << '```bash'
# System Installer Script
# Installer Script Overrides (Example)
This is a minimal example for an adjusted NixOS system installation using the functions defined in [`../lib/setup-scripts/`](../lib/setup-scripts/).
See its [README](../lib/setup-scripts/README.md) for more documentation.
This is an example on how to customize the default installation process.
The [`config.installer.commands.*`](../modules/installer.nix.md) can be used for some per-host customization, but for further reaching changes that are supposed to affect all hosts in a configuration, it may be necessary or more appropriate to extend/override the [default installer functions](../lib/setup-scripts/).
This file would need to be included in the configuration like this:
```nix
{ config.installer.scripts.my-customizations = { path = .../installer.sh.md; order = 1500; }; }
```
## (Example) Implementation
## Implementation
```bash
# Replace the entry point with the same function:
function install-system {( # 1: diskPaths
trap - EXIT # start with empty traps for sub-shell
prepare-installer "$@" || exit
do-disk-setup "$1" || exit
install-system-to $mnt || exit
## Replace/Extend a Command or Function
# For example, »nixos-install-cmd $mnt $topLevel« gets called to perform the last step(s) of the installation, after the »/nix/store« contents has been copied to the new filesystems.
# Usually it just installs the bootloader, but we can hook into the function to have it do additional stuff:
copy-function nixos-install-cmd nixos-install-cmd-default # (if we still want to call the previous implementation)
function nixos-install-cmd {( # 1: mnt, 2: topLevel
# ... do things beforehand ...
nixos-install-cmd-default "$@"
# ... do things afterwards ...
)}
# This schema works with any of the existing commands or functions they use.
# ... could also replace any other function(s) ...
## Add a New COMMAND
# Any bash function defined in any of the setup scripts can be called as »nix run .#$hostname -- COMMAND«, but a proper COMMAND should be documented as such:
declare-command my-thing withThese coolArguments << 'EOD'
This does my thing with these cool arguments -- duh!
EOD
declare-flag my-thing laser-color COLOR "Color of the laser used for my thing."
function run-qemu { # 1: withThese, 2: coolArguments
local withThese=$1
local coolThings=$2
echo "I am playing $withThese to shoot $coolArguments with ${args[laser-color]:-red} lasers!"
}
# $ nix run .#example -- my-thing --laser-color=powerful 'friends' -- 'nothing, cuz that would be irresponsible,'

View File

@ -1 +0,0 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+y3pbsUopXWSWVz+sowoMPTWv+u9Qj9aEl20NUN1LrKxduUv/fijmOyui92ZdTYJEu1oa5+V5jbxxlqNDn51yuwXXCxnIwFgh/aSl34Mc86HrjH73kZonya26jfCBE/7Mn9rppUmpkTt0Dk13Y1gnKp0OvuukEQ+Fa5ZxPLtyZ9d3zYDKIBbwNhISOHlllj8jgEMgGNNDGS7EdFh9AEnKG9d8s4+zTlHEXTom0srr4GBrRcG8qlV6DEcHB/aS7hhI5lA79H9AFWd1PjTV7ZUvX9sLsfRitcmQy2psicDxlagA15Lm/pLuf11t+IIO6bv9EG1cCAvkrGqnGqHLCPFYIW0rKyxD2IRq1ZG4+sbyQlgJiACw1WPiJkOXK88hmjlvwKGx4i8bk2bkXgcmxEHtd0rl+zsSMaZnNltaaGae7DVPKEYhn/sx+hzPpdpz7nhNs/OmN1Y61Zi8J8NHyBKWJ+lQSpV7AY8f2VNKvTFPdXzZmTYd4xVd7saGCa9235oqHX54rZ2zXZaj24zncnxhsvvKkLHeeYbr8knSZNDVfqCCzrm6FTV8aQ5M+QJwfnjVW+TQ/2hEnM1Jb4qbAylJfGY+LHZC9tysRyMwStvnB2+td4HX4hjO75CWbDsW6RLsXQjuzMNAwcGhftA9rnV8azIVX9PD4FYSadPptwuOsw== gpg_rsa.niklas@gollenstede.net

Binary file not shown.

View File

@ -1,30 +1,27 @@
{ description = (
"Work In Progress: a collection of Nix things that are used in more than one project, but aren't refined enough to be standalone libraries/modules/... (yet)."
# This flake file defines the inputs (other than except some files/archives fetched by hardcoded hash) and exports all results produced by this repository.
# It should always pass »nix flake check« and »nix flake show --allow-import-from-derivation«, which means inputs and outputs comply with the flake convention.
"Fully automated NixOS CLI installer"
); inputs = {
# To update »./flake.lock«: $ nix flake update
nixpkgs = { url = "github:NixOS/nixpkgs/nixos-22.11"; };
nixos-hardware = { url = "github:NixOS/nixos-hardware/master"; };
config = { type = "github"; owner = "NiklasGollenstede"; repo = "nix-wiplib"; dir = "example/defaultConfig"; rev = "5e9cc7ce3440be9ce6aeeaedcc70db9c80489c5f"; }; # Use some previous commit's »./example/defaultConfig/flake.nix« as the default config for this flake.
}; outputs = inputs: let patches = {
nixpkgs = [ # Can define a list of patches for each input here:
# { url = "https://github.com/NixOS/nixpkgs/pull/###.diff"; sha256 = inputs.nixpkgs.lib.fakeSha256; } # Path from URL.
# ./patches/nixpkgs-fix-systemd-boot-install.patch # Local path file. (use long native / direct path to ensure it only changes if the content does)
# ./patches/nixpkgs-test.patch # After »nix build«, check »result/inputs/nixpkgs/patched!« to see that these patches were applied.
];
}; in (import "${./.}/lib/flakes.nix" "${./.}/lib" inputs).patchFlakeInputsAndImportRepo inputs patches ./. (inputs@{ self, nixpkgs, ... }: repo@{ overlays, lib, ... }: let
nixpkgs = { url = "github:NixOS/nixpkgs/nixos-23.05"; };
functions = { url = "github:NiklasGollenstede/nix-functions"; inputs.nixpkgs.follows = "nixpkgs"; };
config.url = "path:./example/defaultConfig";
}; outputs = inputs@{ self, ... }: inputs.functions.lib.importRepo inputs ./. (repo@{ overlays, ... }: let
lib = repo.lib.__internal__;
in [ # Run »nix flake show --allow-import-from-derivation« to see what this merges to:
## Exports (things to reuse in other flakes):
repo # lib.* nixosModules.* overlays.*
(lib.wip.mkSystemsFlake { inherit inputs; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems
(lib.wip.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: { # packages.*-linux.* defaultPackage.*-linux
packages = builtins.removeAttrs (lib.wip.getModifiedPackages (lib.wip.importPkgs inputs { system = localSystem; }) overlays) [ "libblockdev" ];
defaultPackage = self.packages.${localSystem}.all-systems;
}))
{ patches = (lib.wip.importWrapped inputs "${self}/patches").result; } # patches.*
{ patches = (lib.fun.importWrapped inputs "${self}/patches").result; } # patches.*
## Examples:
# The example host definitions from ./hosts/, plus their installers (apps):
(lib.self.mkSystemsFlake { inherit inputs; }) # nixosConfigurations.* apps.*-linux.* devShells.*-linux.* packages.*-linux.all-systems
# The same cross-compiled from aarch64 (just to show how that works):
(lib.self.mkSystemsFlake { inherit inputs; buildPlatform = "aarch64-linux"; renameOutputs = name: "arm:${name}"; }) # nixosConfigurations.arm:* apps.*-linux.arm:* devShells.*-linux.arm:* packages.*-linux.arm:all-systems
# Any packages touched by the ./overlays/:
(lib.fun.forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: let # packages.*-linux.*
packages = lib.fun.getModifiedPackages (lib.fun.importPkgs inputs { system = localSystem; }) overlays;
in { packages = packages // { default = self.packages.${localSystem}.all-systems; }; }))
]); }

View File

@ -7,53 +7,49 @@ Just to provide an example of what a host configuration using this set of librar
## Installation
To prepare a virtual machine disk, as `sudo` user with `nix` installed, run in `..`:
To install the system to a (set of) virtual machine disk images, with `$hostname` as any of the `instances` below, run in `..`:
```bash
nix run '.#example' -- sudo install-system /home/$(id -un)/vm/disks/example/
( sudo chown $(id -un): /home/$(id -un)/vm/disks/example/* )
nix run '.#example-raidz' -- sudo install-system /tmp/nixos-example-raidz/
nix run .'#'$hostname -- install-system /tmp/$hostname/
```
Then to boot the system in a qemu VM with KVM:
```bash
nix run '.#example' -- sudo run-qemu /home/$(id -un)/vm/disks/example/
nix run .'#'$hostname -- run-qemu /tmp/$hostname/
```
Or as user with vBox access, run this and use the UI or the printed commands:
```bash
nix run '.#example' -- register-vbox /home/$(id -un)/vm/disks/example/primary.img
```
Alternative to running with `sudo` (if `nix` is installed for root), the above commands can also be run as `root` without the `sudo` argument.
See `nix run .#$hostname -- --help` for options and more commands.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS config flake input:
dirname: inputs: { config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
#suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »wip.preface.instances«
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
in { imports = [ ({ ## Hardware
wip.preface.instances = [ "example-explicit" "example" "example-minimal" "example-raidz" "test-zfs-hibernate" ];
dirname: inputs: { config, pkgs, lib, name, ... }: let lib = inputs.self.lib.__internal__; in let
#suffix = builtins.head (builtins.match ''example-(.*)'' name); # make differences in config based on this when using »preface.instances«
hash = builtins.substring 0 8 (builtins.hashString "sha256" name);
in { preface = { # (any »preface« options have to be defined here)
instances = [ "example-explicit" "example" "example-minimal" "example-raidz" ]; # Generate multiple variants of this host, with these »name«s.
}; imports = [ ({ ## Hardware
wip.preface.hardware = "x86_64"; system.stateVersion = "22.05";
nixpkgs.hostPlatform = "x86_64-linux"; system.stateVersion = "22.05";
## What follows is a whole bunch of boilerplate-ish stuff, most of which multiple hosts would have in common and which would thus be moved to one or more modules:
wip.bootloader.extlinux.enable = true;
boot.loader.extlinux.enable = true;
# Example of adding and/or overwriting setup/maintenance functions:
#wip.setup.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1000; };
#installer.scripts.install-overwrite = { path = ../example/install.sh.md; order = 1500; };
}) (lib.mkIf (name == "example-explicit") { ## Minimal explicit FS setup
# Declare a boot and system partition. Though not required for EFI, make the boot part visible to boot loaders supporting only MBR.
wip.fs.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };
wip.fs.disks.partitions."system-${hash}" = { type = "8300"; size = null; order = 500; };
wip.fs.disks.devices = { primary = { mbrParts = "1"; extraFDiskCommands = ''
t;1;c # type ; part1 ; W95 FAT32 (LBA)
a;1 # active/boot ; part1
# Declare a boot and system partition.
setup.disks.partitions."boot-${hash}" = { type = "ef00"; size = "64M"; index = 1; order = 1500; };
setup.disks.partitions."system-${hash}" = { type = "8300"; size = null; order = 500; };
# Though not required for EFI, make the boot part visible to boot loaders supporting only MBR.
setup.disks.devices = { primary = { mbrParts = "1"; extraFDiskCommands = ''
t;1;c # set type ; part1 ; W95 FAT32 (LBA)
a;1 # set as active/bootable ; part1
''; }; };
wip.fs.boot.enable = false;
setup.bootpart.enable = false; # (enabled by the bootloader)
# 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" ]; };
@ -62,71 +58,70 @@ in { imports = [ ({ ## Hardware
fileSystems."/nix/store" = { options = ["bind,ro"]; device = "/system/nix/store"; neededForBoot = true; };
}) (lib.mkIf (name == "example" || name == "test-zfs-hibernate") { ## More complex but automatic FS setup
}) (lib.mkIf (name == "example") { ## More complex but automatic FS setup
#wip.fs.disks.devices.primary.size = "16G"; # (default)
wip.fs.boot.enable = true; wip.fs.boot.size = "512M";
#setup.disks.devices.primary.size = "16G"; # (default)
setup.bootpart.enable = true; setup.bootpart.size = "512M";
wip.fs.keystore.enable = true;
wip.fs.temproot.enable = true;
setup.keystore.enable = true;
setup.temproot.enable = true;
wip.fs.temproot.swap.size = "2G";
wip.fs.temproot.swap.asPartition = true;
wip.fs.temproot.swap.encrypted = true;
setup.temproot.swap.size = "2G";
setup.temproot.swap.asPartition = true;
setup.temproot.swap.encrypted = true;
wip.fs.temproot.temp.type = "tmpfs";
setup.temproot.temp.type = "tmpfs";
wip.fs.temproot.local.type = "bind";
wip.fs.temproot.local.bind.base = "f2fs-encrypted"; # creates partition and FS
#wip.fs.keystore.keys."luks/local-${hash}/0" = "random"; # (implied by the »-encrypted« suffix above)
#wip.fs.disks.partitions."local-${hash}".size = "50%"; # (default)
setup.temproot.local.type = "bind";
setup.temproot.local.bind.base = "f2fs-encrypted"; # creates partition and FS
#setup.keystore.keys."luks/local-${hash}/0" = "random"; # (implied by the »-encrypted« suffix above)
#setup.disks.partitions."local-${hash}".size = "50%"; # (default)
wip.fs.temproot.remote.type = "zfs";
wip.fs.keystore.keys."luks/rpool-${hash}/0" = "random";
#wip.fs.keystore.keys."zfs/rpool-${hash}/remote" = "random"; # (default)
#wip.fs.zfs.pools."rpool-${hash}".vdevArgs = [ "rpool-${hash}" ]; # (default)
#wip.fs.disks.partitions."rpool-${hash}" = { type = "bf00"; size = null; order = 500; }; # (default)
setup.temproot.remote.type = "zfs";
setup.keystore.keys."luks/rpool-${hash}/0" = "random";
#setup.keystore.keys."zfs/rpool-${hash}/remote" = "random"; # (default)
#setup.zfs.pools."rpool-${hash}".vdevArgs = [ "rpool-${hash}" ]; # (default)
#setup.disks.partitions."rpool-${hash}" = { type = "bf00"; size = null; order = 500; }; # (default)
wip.fs.temproot.local.mounts."/var/log" = lib.mkForce null; # example: don't keep logs
setup.temproot.local.mounts."/var/log" = lib.mkForce null; # example: don't keep logs
}) (lib.mkIf (name == "example-minimal") { ## 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";
setup.bootpart.enable = true; # (also set by »boot.loader.extlinux.enable«)
setup.temproot.enable = true;
setup.temproot.temp.type = "tmpfs"; # (default)
setup.temproot.local.type = "bind"; # (default)
setup.temproot.local.bind.base = "f2fs";
setup.temproot.remote.type = "none";
}) (lib.mkIf (name == "example-raidz") { ## Multi-disk ZFS setup
wip.bootloader.extlinux.enable = lib.mkForce false; # use UEFI boot this time
boot.loader.extlinux.enable = lib.mkForce false; # use UEFI boot this time
boot.loader.systemd-boot.enable = true; boot.loader.grub.enable = false;
wip.fs.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; });
wip.fs.boot.enable = true; wip.fs.boot.size = "512M";
setup.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; });
setup.bootpart.enable = true; setup.bootpart.size = "512M";
wip.fs.keystore.enable = true;
wip.fs.temproot.enable = true;
setup.keystore.enable = true;
setup.temproot.enable = true;
wip.fs.temproot.temp.type = "zfs";
wip.fs.temproot.local.type = "zfs";
setup.temproot.temp.type = "zfs";
setup.temproot.local.type = "zfs";
wip.fs.temproot.remote.type = "zfs";
wip.fs.zfs.pools."rpool-${hash}".vdevArgs = [ "raidz1" "rpool-rz1-${hash}" "rpool-rz2-${hash}" "rpool-rz3-${hash}" "log" "rpool-zil-${hash}" "cache" "rpool-arc-${hash}" ];
wip.fs.disks.partitions."rpool-rz1-${hash}" = { type = "bf00"; disk = "raidz1"; };
wip.fs.disks.partitions."rpool-rz2-${hash}" = { type = "bf00"; disk = "raidz2"; };
wip.fs.disks.partitions."rpool-rz3-${hash}" = { type = "bf00"; disk = "raidz3"; };
wip.fs.disks.partitions."rpool-zil-${hash}" = { type = "bf00"; size = "2G"; };
wip.fs.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; };
setup.temproot.remote.type = "zfs";
setup.zfs.pools."rpool-${hash}".vdevArgs = [ "raidz1" "rpool-rz1-${hash}" "rpool-rz2-${hash}" "rpool-rz3-${hash}" "log" "rpool-zil-${hash}" "cache" "rpool-arc-${hash}" ];
setup.disks.partitions."rpool-rz1-${hash}" = { type = "bf00"; disk = "raidz1"; };
setup.disks.partitions."rpool-rz2-${hash}" = { type = "bf00"; disk = "raidz2"; };
setup.disks.partitions."rpool-rz3-${hash}" = { type = "bf00"; disk = "raidz3"; };
setup.disks.partitions."rpool-zil-${hash}" = { type = "bf00"; size = "2G"; };
setup.disks.partitions."rpool-arc-${hash}" = { type = "bf00"; };
}) ({ ## Base Config
# Some base config:
wip.base.enable = true;
documentation.enable = false; # sometimes takes quite long to build
@ -138,39 +133,8 @@ in { imports = [ ({ ## Hardware
services.getty.autologinUser = "root"; users.users.root.password = "root";
boot.kernelParams = [ /* "console=tty1" */ "console=ttyS0" "boot.shell_on_fail" ]; wip.base.panic_on_fail = false;
wip.services.dropbear.enable = true;
wip.services.dropbear.rootKeys = ''${lib.readFile "${dirname}/../example/ssh-login.pub"}'';
wip.services.dropbear.socketActivation = true;
#wip.fs.disks.devices.primary.gptOffset = 64;
#wip.fs.disks.devices.primary.size = "250059096K"; # 256GB Intel H10
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
}) (lib.mkIf (name == "test-zfs-hibernate") {
# This was an attempt to reliably get ZFS to corrupt when importing a ZFS pool before resuming from hibernation in initrd. It isn't reproducible, though: https://github.com/NixOS/nixpkgs/pull/208037#issuecomment-1368240321
wip.fs.temproot.temp.type = lib.mkForce "zfs";
wip.fs.temproot.local.type = lib.mkForce "zfs";
wip.fs.keystore.keys."luks/rpool-${hash}/0" = lib.mkForce null;
wip.fs.disks.devices.mirror.size = "16G";
wip.fs.disks.partitions."mirror-${hash}" = { type = "bf00"; disk = "mirror"; };
environment.systemPackages = [ (pkgs.writeShellScriptBin "test-zfs-hibernate" ''
set -ex
</dev/urandom head -c 10G >/tmp/dump
sync ; echo 3 > /proc/sys/vm/drop_caches ; sleep 5
zpool attach rpool-${hash} /dev/disk/by-partlabel/rpool-${hash} /dev/disk/by-partlabel/mirror-${hash}
sleep 2 # the above command should still be in progress
: before ; date
systemctl hibernate
: hibernating ; date
sleep 3 ; : awake ; date
'') ];
boot.zfs = if (builtins.substring 0 5 inputs.nixpkgs.lib.version) == "22.05" then { } else { allowHibernation = true; };
boot.kernelParams = [ /* "console=tty1" */ "console=ttyS0" "boot.shell_on_fail" ];
boot.zfs.allowHibernation = lib.mkForce false; # Ugh: https://github.com/NixOS/nixpkgs/commit/c70f0473153c63ad1cf6fbea19f290db6b15291f
}) ]; }

View File

@ -1,38 +0,0 @@
/*
# 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,6 +1,4 @@
dirname: inputs@{ self, nixpkgs, ...}: let
#fix = f: let x = f x; in x;
#categories = fix (wip: (import "${dirname}/imports.nix" dirname inputs).importAll (inputs // { self = inputs.self // { lib = nixpkgs.lib // { inherit wip; }; }; })) dirname;
categories = (import "${dirname}/imports.nix" dirname inputs).importAll inputs dirname;
wip = (builtins.foldl' (a: b: a // (if builtins.isAttrs b then b else { })) { } (builtins.attrValues (builtins.removeAttrs categories [ "setup-scripts" ]))) // categories;
in nixpkgs.lib // { wip = wip // { prefix = inputs.config.prefix; }; }
dirname: inputs@{ nixpkgs, functions, ...}: let
categories = functions.lib.importAll inputs dirname;
self = (builtins.foldl' (a: b: a // (if builtins.isAttrs b then b else { })) { } (builtins.attrValues (builtins.removeAttrs categories [ "setup-scripts" ]))) // categories;
in self // { __internal__ = nixpkgs.lib // { self = self; fun = functions.lib; }; }

View File

@ -1,303 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) namesToAttrs mapMerge mapMergeUnique mergeAttrsUnique flipNames;
inherit (import "${dirname}/imports.nix" dirname inputs) getNixFiles importWrapped getOverlaysFromInputs getModulesFromInputs;
inherit (import "${dirname}/scripts.nix" dirname inputs) substituteImplicit extractBashFunction;
setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs);
prefix = inputs.config.prefix;
inherit (import "${dirname}/misc.nix" dirname inputs) trace;
in rec {
# Simplified implementation of »flake-utils.lib.eachSystem«.
forEachSystem = systems: getSystemOutputs: flipNames (namesToAttrs getSystemOutputs systems);
# Sooner or later this should be implemented in nix itself, for now require »inputs.nixpkgs« and a system that can run »x86_64-linux« (native or through qemu).
patchFlakeInputs = inputs: patches: outputs: let
inherit ((import inputs.nixpkgs { overlays = [ ]; config = { }; system = "x86_64-linux"; }).pkgs) applyPatches fetchpatch;
in outputs (builtins.mapAttrs (name: input: if name != "self" && patches?${name} && patches.${name} != [ ] then (let
patched = applyPatches {
name = "${name}-patched"; src = "${input.sourceInfo or input}";
patches = map (patch: if patch ? url then fetchpatch patch else patch) patches.${name};
};
sourceInfo = (builtins.removeAttrs (input.sourceInfo or input) [ "narHash"]) // patched; # (keeps (short)rev, which is not really correct)
dir = if input?sourceInfo.outPath && lib.hasPrefix input.outPath input.sourceInfo.outPath then lib.removePrefix input.sourceInfo.outPath input.outPath else ""; # this should work starting with nix version 2.14 (before, they are the same path)
in (
# sourceInfo = { lastModified; lastModifiedDate; narHash; outPath; rev?; shortRev?; }
# A non-flake has only the attrs of »sourceInfo«.
# 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.
# Since nix v2.14, the direct »outPath« has the relative location of the »dir« containing the »flake.nix« as suffix (if not "").
if (!input?sourceInfo) then sourceInfo else (let
outputs = (import "${patched.outPath}${dir}/flake.nix").outputs ({ self = sourceInfo // outputs; } // input.inputs);
in outputs // sourceInfo // { outPath = "${patched.outPath}${dir}"; inherit (input) inputs; inherit outputs; inherit sourceInfo; })
)) else input) inputs);
# Generates implicit flake outputs by importing conventional paths in the local repo. E.g.:
# outputs = inputs@{ self, nixpkgs, wiplib, ... }: wiplib.lib.wip.importRepo inputs ./. (repo@{ overlays, lib, ... }: let ... in [ repo ... ])
importRepo = inputs: flakePath': outputs: let
flakePath = builtins.path { path = flakePath'; name = "source"; }; # Referring to the current flake directory as »./.« is quite intuitive (and »inputs.self.outPath« causes infinite recursion), but without this it adds another hash to the path (because it copies it). For flakes with »dir != ""«, this includes only the ».« directory, making references to »./..« invalid, but ensuring that »./flake.nix« exists (there), and the below default paths are relative to that (and not whatever nix thought is the root of the repo).
# TODO: should _not_ do the above if it is not a direct store path
in let result = (outputs (
(let it = importWrapped inputs "${flakePath}/lib"; in if it.exists then {
lib = it.result;
} else { }) // (let it = importWrapped inputs "${flakePath}/overlays"; in if it.exists then {
overlays = { default = final: prev: builtins.foldl' (prev: overlay: prev // (overlay final prev)) prev (builtins.attrValues it.result); } // it.result;
} else { }) // (let it = importWrapped inputs "${flakePath}/modules"; in if it.exists then {
nixosModules = { default = { imports = builtins.attrValues it.result; }; } // it.result;
} else { })
)); in if (builtins.isList result) then mergeOutputs result else result;
# Combines »patchFlakeInputs« and »importRepo« in a single call. E.g.:
# outputs = inputs: let patches = {
# nixpkgs = [
# # remote: { url = "https://github.com/NixOS/nixpkgs/pull/###.diff"; sha256 = inputs.nixpkgs.lib.fakeSha256; }
# # local: ./overlays/patches/nixpkgs-###.patch # (use long native path to having the path change if any of the other files in ./. change)
# ]; # ...
# }; in inputs.wiplib.lib.wip.patchFlakeInputsAndImportRepo inputs patches ./. (inputs@{ self, nixpkgs, ... }: repo@{ nixosModules, overlays, lib, ... }: let ... in [ repo ... ])
patchFlakeInputsAndImportRepo = inputs: patches: flakePath: outputs: (
patchFlakeInputs inputs patches (inputs: importRepo inputs flakePath (outputs (inputs // {
self = inputs.self // { outPath = builtins.path { path = flakePath; name = "source"; }; }; # If the »flake.nix is in a sub dir of a repo, "${inputs.self}" would otherwise refer to the parent. (?)
})))
);
# Merges a list of flake output attribute sets.
mergeOutputs = outputList: lib.zipAttrsWith (type: values: (
if ((lib.length values) == 1) then (lib.head values)
else if (lib.all lib.isAttrs values) then (lib.zipAttrsWith (system: values: mergeAttrsUnique values) values)
else throw "Outputs.${type} has multiple values which are not all attribute sets, can't merge."
)) outputList;
# Given a path to a host config file, returns some properties defined in its first inline module (to be used where accessing them via »nodes.${name}.config...« isn't possible).
getSystemPreface = inputs: entryPath: args: let
imported = (importWrapped inputs entryPath).required ({ config = null; pkgs = null; lib = null; name = null; nodes = null; extraModules = null; } // args);
module = builtins.elemAt imported.imports 0; props = module.${prefix}.preface;
in if (
imported?imports && (builtins.isList imported.imports) && (imported.imports != [ ]) && module?${prefix}.preface && props?hardware
) then (props) else throw "File ${entryPath} must fulfill the structure: dirname: inputs: { ... }: { imports = [ { ${prefix}.preface = { hardware = str; ... } } ]; }";
# 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.
mkNixosConfiguration = args@{ name,
entryPath ? null, config ? null, preface ? null,
inputs ? { }, overlays ? (getOverlaysFromInputs inputs), modules ? (getModulesFromInputs inputs),
peers ? { }, nixosSystem ? inputs.nixpkgs.lib.nixosSystem, localSystem ? null,
renameOutputs ? (name: null), # If not supplied (explicitly or as »false« by »mkSystemsFlake«), assume the system is not exported.
... }: let
preface = args.preface or (getSystemPreface inputs entryPath (specialArgs // { inherit name; }));
targetSystem = "${preface.hardware}-linux"; buildSystem = if localSystem != null then localSystem else targetSystem;
specialArgs = (args.specialArgs or { }) // {
nodes = peers; # NixOPS
};
outputName = if renameOutputs != false then renameOutputs name else name;
in let system = { inherit preface; } // (nixosSystem {
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.
(args.config or (importWrapped inputs entryPath).module)
{ _module.args.name = lib.mkOverride 99 name; } # (specialisations can somehow end up with the name »configuration«, which is very incorrect)
{ networking.hostName = name; }
]; _file = "${dirname}/flakes.nix#modules"; } ];
extraModules = modules ++ [ { imports = [ ({ # These are passed as »extraModules« module argument and can thus conveniently be reused when defining containers and such (Therefore define as much stuff as possible here).
# TODO: or should these be set as »baseModules«? Does that implicitly pass these into any derived configs?
}) ({ config, ... }: {
nixpkgs = { inherit overlays; }
// (if buildSystem != targetSystem then { localSystem.system = buildSystem; crossSystem.system = targetSystem; } else { system = targetSystem; });
_module.args = builtins.removeAttrs specialArgs [ "name" ]; # (pass the args here, so that they also apply to any other evaluation using »extraModules«)
${prefix}.base.includeInputs = lib.mkDefault (if config.boot.isContainer then inputs // {
self = null; # avoid changing (and thus restarting) the containers on every trivial change
} else inputs);
system.nixos.revision = lib.mkIf (inputs?nixpkgs.rev) inputs.nixpkgs.rev; # (evaluating the default value fails under some circumstances)
}) ({
options.${prefix}.preface.hardware = lib.mkOption { description = "The name of the system's CPU instruction set (the first part of what is often referred to as »system«)."; type = lib.types.str; readOnly = true; };
}) ({
options.${prefix}.preface.instances = lib.mkOption { description = "List of host names to instantiate this host config for, instead of just for the file name."; type = lib.types.listOf lib.types.str; readOnly = true; };
config.${prefix} = if preface?instances then { } else { preface.instances = [ name ]; };
}) ({
options.${prefix}.setup.scripts = lib.mkOption {
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 = {
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; };
text = lib.mkOption { description = "Script text to process."; type = lib.types.str; default = builtins.readFile config.path; };
order = lib.mkOption { description = "Parsing order of the scripts. Higher orders will be parsed later, and can thus overwrite earlier definitions."; type = lib.types.int; default = 1000; };
}; })));
apply = lib.filterAttrs (k: v: v != null);
}; config.${prefix}.setup.scripts = lib.mapAttrs (name: path: lib.mkOptionDefault { inherit path; }) (setup-scripts);
}) ({ config, options, pkgs, ... }: {
options.${prefix}.setup.appliedScripts = lib.mkOption {
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 outputName; } // context; }; # inherit (builtins) trace;
};
}) ]; _file = "${dirname}/flakes.nix#extraModules"; } ];
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;
# Given either a list (or attr set) of »files« (paths to ».nix« or ».nix.md« files for dirs with »default.nix« files in them) or a »dir« path (and optionally a list of base names to »exclude« from it), this builds the NixOS configuration for each host (per file) in the context of all configs provided.
# If »files« is an attr set, exactly one host with the attribute's name as hostname is built for each attribute. Otherwise the default is to build for one host per configuration file, named as the file name without extension or the sub-directory name. Setting »${prefix}.preface.instances« can override this to build the same configuration for those multiple names instead (the specific »name« is passed as additional »specialArgs« to the modules and can thus be used to adjust the config per instance).
# All other arguments are as specified by »mkSystemsFlake« and are passed to »mkNixosConfiguration«.
mkNixosConfigurations = args: let # { files, dir, exclude, ... }
files = args.files or (builtins.removeAttrs (getNixFiles args.dir) (args.exclude or [ ]));
files' = if builtins.isAttrs files then files else (builtins.listToAttrs (map (entryPath: let
stripped = builtins.match ''^(.*)[.]nix[.]md$'' (builtins.baseNameOf entryPath);
name = builtins.unsafeDiscardStringContext (if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath));
in { inherit name; value = entryPath; }) files));
configs = mapMergeUnique (name: entryPath: (let
preface = (getSystemPreface inputs entryPath { });
in (mapMergeUnique (name: {
"${name}" = mkNixosConfiguration ((
builtins.removeAttrs args [ "files" "dir" "exclude" ]
) // {
inherit name entryPath; peers = configs;
});
}) (if !(args?files && builtins.isAttrs files) && preface?instances then preface.instances else [ name ])))) (files');
withId = lib.filterAttrs (name: node: node.preface?id) configs;
ids = mapMerge (name: node: { "${toString node.preface.id}" = name; }) withId;
duplicate = builtins.removeAttrs withId (builtins.attrValues ids);
in if duplicate != { } then (
throw "»${prefix}.preface.id«s are not unique! The following hosts share their IDs with some other host: ${builtins.concatStringsSep ", " (builtins.attrNames duplicate)}"
) else configs;
# Builds a system of NixOS hosts and exports them, plus »apps« and »devShells« to manage them, as flake outputs.
# All arguments are optional, as long as the default can be derived from the other arguments as passed.
mkSystemsFlake = args@{
# An attrset of imported Nix flakes, for example the argument(s) passed to the flake »outputs« function. All other arguments are optional (and have reasonable defaults) if this is provided and contains »self« and the standard »nixpkgs«. This is also the second argument passed to the individual host's top level config files.
inputs ? { },
# Arguments »{ files, dir, exclude, }« to »mkNixosConfigurations«, see there for details. May also be a list of those attrsets, in which case those multiple sets of hosts will be built separately by »mkNixosConfigurations«, allowing for separate sets of »peers« passed to »mkNixosConfiguration«. Each call will receive all other arguments, and the resulting sets of hosts will be merged.
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«).
overlays ? (getOverlaysFromInputs overlayInputs),
# (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
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«).
modules ? (getModulesFromInputs moduleInputs),
# (Subset of) »inputs« that »modules« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
moduleInputs ? inputs,
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
specialArgs ? { },
# The »nixosSystem« function defined in »<nixpkgs>/flake.nix«, or equivalent.
nixosSystem ? inputs.nixpkgs.lib.nixosSystem,
# If provided, then cross compilation is enabled for all hosts whose target architecture is different from this. Since cross compilation currently fails for (some stuff in) NixOS, better don't set »localSystem«. Without it, building for other platforms works fine (just slowly) if »boot.binfmt.emulatedSystems« on the building system is configured for the respective target(s).
localSystem ? null,
## 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 ? false,
... }: let
otherArgs = args // { inherit inputs overlays modules specialArgs nixosSystem localSystem renameOutputs; };
nixosConfigurations = if builtins.isList systems then mergeAttrsUnique (map (systems: mkNixosConfigurations (otherArgs // systems)) systems) else mkNixosConfigurations (otherArgs // systems);
in let outputs = {
inherit nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: let
pkgs = (import inputs.nixpkgs { inherit overlays; system = localSystem; });
tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites));
in rec {
apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = writeSystemScripts { inherit name system pkgs; }; program = "${derivation}"; }) nixosConfigurations;
# dummy that just pulls in all system builds
packages.all-systems = pkgs.runCommandLocal "all-systems" { } ''
${''
mkdir -p $out/systems
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${system.config.system.build.toplevel} $out/systems/${if renameOutputs == false then name else renameOutputs name}") nixosConfigurations)}
''}
${''
mkdir -p $out/scripts
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${outputs.apps.${localSystem}.${name}.program} $out/scripts/${if renameOutputs == false then name else renameOutputs name}") nixosConfigurations)}
''}
${lib.optionalString (inputs != { }) ''
mkdir -p $out/inputs
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: { outPath, ... }: "ln -sT ${outPath} $out/inputs/${name}") inputs)}
''}
'';
checks.all-systems = packages.all-systems;
})); in if renameOutputs == false then outputs else {
nixosConfigurations = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (localSystem: {
apps = mapMerge (k: v: { ${renameOutputs k} = v; }) outputs.apps.${localSystem};
packages.${renameOutputs "all-systems"} = outputs.packages.${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 positional argument is `eval`ed 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
shift ; 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
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS
cat $0) -i -s ':' "$@"
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.'} || exit
argv=( "''${argv[@]:1}" ) ; set -- "''${argv[@]}" ; eval "$command" || exit
else
entry=''${argv[0]:?} || exit
argv=( "''${argv[@]:1}" ) ; "$entry" "''${argv[@]}" || exit
fi
'';
}

View File

@ -1,152 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) mapMergeUnique mergeAttrsRecursive endsWith;
inherit (import "${dirname}/misc.nix" dirname inputs) trace;
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.
getNixFiles = dir: mapMergeUnique (name: type: if (type == "directory") then (
if (builtins.pathExists "${dir}/${name}/default.nix") then { ${name} = "${dir}/${name}/default.nix"; } else { }
) else (
let
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
in if (match != null) then {
${builtins.head match} = "${dir}/${name}";
} else { }
)) (builtins.readDir dir);
getNixFilesRecursive = dir: let
list = prefix: dir: mapMergeUnique (name: type: if (type == "directory") then (
list "${prefix}${name}/" "${dir}/${name}"
) else (let
match = builtins.match ''^(.*)[.]nix([.]md)?$'' name;
in if (match != null) then {
"${prefix}${builtins.head match}" = "${dir}/${name}";
} else { })) (builtins.readDir dir);
in list "" dir;
## Decides whether a thing is probably a NixOS configuration module or not.
# Probably because almost everything could be a module declaration (any attribute set or function returning one is potentially a module).
# Per convention, modules (at least those declared stand-alone in a file) are declared as functions taking at least the named arguments »config«, »pkgs«, and »lib«. Once entered into the module system, to remember where they came from, modules get wrapped in an attrset »{ _file = "<path>"; imports = [ <actual_module> ]; }«.
isProbablyModule = thing: let args = builtins.functionArgs thing; in (
(builtins.isFunction thing) && (builtins.isAttrs (thing args)) && (builtins.isBool (args.config or null)) && (builtins.isBool (args.lib or null)) && (builtins.isBool (args.pkgs or null))
) || (
(builtins.isAttrs thing) && ((builtins.attrNames thing) == [ "_file" "imports" ]) && ((builtins.isString thing._file) || (builtins.isPath thing._file)) && (builtins.isList thing.imports)
);
## Decides whether a thing could be a NixPkgs overlay.
# Any function with two (usually unnamed) arguments returning an attrset could be an overlay, so that's rather vague.
couldBeOverlay = thing: let result1 = thing (builtins.functionArgs thing); result2 = result1 (builtins.functionArgs result1); in builtins.isFunction thing && builtins.isFunction result1 && builtins.isAttrs result2;
# Builds an attrset that, for each folder (containing a »default.nix«) or ».nix« or ».nix.md« file (other than »./default.nix«) in this folder, as the name of that folder or the name of the file without extension(s), exports the result of importing that file/folder.
importAll = inputs: dir: builtins.mapAttrs (name: path: import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs) (builtins.removeAttrs (getNixFiles dir) [ "default" ]);
# Import a Nix file that expects the standard `dirname: inputs: ` arguments, providing some additional information and error handling.
importWrapped = inputs: path: rec {
# Whether the file is imported by an explicit full path (or one omitting ».nix« or »/default.nix«):
isExplicit = (builtins.match ''^(.*)[.]nix([.]md)?$'' path) != null;
# Whether the import path _implicitly_ refers to the »/default.nix« in a directory:
isImplicitDir = !isExplicit && builtins.pathExists "${path}/default.nix";
# The resolved path that will be imported:
fullPath = if isImplicitDir then "${path}/default.nix" else if isExplicit then path else "${path}.nix";
# The imported nix value:
result = import fullPath (if isImplicitDir then path else builtins.dirOf path) inputs;
# Whether the import path points to an existing file:
exists = isImplicitDir || (builtins.pathExists (if isExplicit then path else "${path}.nix"));
# Return »null« if not ».exists«:
optional = if exists then result else null;
# Throw if not ».exists«:
required = if exists then result else throw (if isExplicit then "File ${path} does not exist" else "Neither ${path}/default.nix nor ${path}.nix exist");
# ».result« interpreted as NixOS module, wrapped to preserve the import path:
module = { _file = fullPath; imports = [ required ]; };
};
## Returns an attrset that, for each file in »dir« (except ...), imports that file and exposes only if the result passes »filter«. If provided, the imported value is »wrapped« after filtering.
# If a file/folder' import that is rejected by »filter« is an attrset (for example because it results from a call to this function), then all attributes whose values pass »filter« are prefixed with the file/folders name plus a slash and merged into the overall attrset.
# Example: Given a file tree like this, where each »default.nix« contains only a call to this function with the containing directory as »dir«, and every other file contains a definition of something accepted by the »filter«:
# ├── default.nix
# ├── a.nix.md
# ├── b.nix
# └── c
# ├── default.nix
# ├── d.nix
# └── e.nix.md
# The top level »default.nix« returns:
# { "a" = <filtered>; "b" = <filtered>; "c/d" = <filtered>; "c/e" = <filtered>; }
importFilteredFlattened = dir: inputs: { except ? [ ], filter ? (thing: true), wrap ? (path: thing: thing), }: let
files = builtins.removeAttrs (getNixFiles dir) except;
in mapMergeUnique (name: path: let
thing = import path (if endsWith "/default.nix" path then "${dir}/${name}" else dir) inputs;
in if (filter thing) then (
{ ${name} = wrap path thing; }
) else (if (builtins.isAttrs thing) then (
mapMergeUnique (name': thing': if (filter thing') then (
{ "${name}/${name'}" = thing'; }
) else { }) thing
) else { })) files;
# Used in a »default.nix« and called with the »dir« it is in, imports all modules in that directory as attribute set. See »importFilteredFlattened« and »isProbablyModule« for details.
importModules = inputs: dir: opts: importFilteredFlattened dir inputs ({ except = [ "default" ]; } // opts // { filter = isProbablyModule; wrap = path: module: { _file = path; imports = [ module ]; }; });
# Used in a »default.nix« and called with the »dir« it is in, imports all overlays in that directory as attribute set. See »importFilteredFlattened« and »couldBeOverlay« for details.
importOverlays = inputs: dir: opts: importFilteredFlattened dir inputs ({ except = [ "default" ]; } // opts // { filter = couldBeOverlay; });
# Imports »inputs.nixpkgs« and instantiates it with all ».overlay(s)« provided by »inputs.*«.
importPkgs = inputs: args: import inputs.nixpkgs ({
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);
## 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.
# (But this only works for top-level / non-scoped packages.)
getModifiedPackages = pkgs: overlays: let
getNames = overlay: builtins.attrNames (overlay { } { });
names = if overlays?default then getNames overlays.default else builtins.concatLists (map getNames (builtins.attrValues overlays));
in mapMergeUnique (name: if lib.isDerivation pkgs.${name} then { ${name} = pkgs.${name}; } else { }) names;
## Given a path to a module in »nixpkgs/nixos/modules/«, when placed in another module's »imports«, this adds an option »disableModule.${modulePath}« that defaults to being false, but when explicitly set to »true«, disables all »config« values set by the module.
# Every module should, but not all modules do, provide such an option themselves.
# 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
# * makes the disabling an option, i.e. it can be changed dynamically based on other config values
# NOTE: This can only be used once per module import graph (~= NixOS configuration) and »modulePath«!
makeNixpkgsModuleConfigOptional = modulePath: extraOriginalModuleArgs: args@{ config, pkgs, lib, modulesPath, utils, ... }: let
fullPath = "${modulesPath}/${modulePath}";
module = import fullPath (args // extraOriginalModuleArgs);
in { _file = fullPath; imports = [
{ options.disableModule.${modulePath} = lib.mkOption { description = "Disable the nixpkgs module ${modulePath}"; type = lib.types.bool; default = false; }; }
(if module?config then (
module // { config = lib.mkIf (!config.disableModule.${modulePath}) module.config; }
) else (
{ config = lib.mkIf (!config.disableModule.${modulePath}) module; }
))
{ 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 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.
# 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.
# 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.
overrideNixpkgsModule = modulePath: extraOriginalModuleArgs: override: args@{ config, pkgs, lib, modulesPath, utils, ... }: let
fullPath = "${modulesPath}/${modulePath}";
module = import fullPath (args // extraOriginalModuleArgs);
overrides = 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 ]; }
]; };
}

View File

@ -1,26 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) startsWith;
in rec {
## Logic Flow
notNull = value: value != null;
ifNull = value: default: (if value == null then default else value);
withDefault = default: value: (if value == null then default else value);
passNull = mayNull: expression: (if mayNull == null then null else expression);
## Misc
# Creates a package for `config.systemd.packages` that adds an `override.conf` to the specified `unit` (which is the only way to modify a single service template instance).
mkSystemdOverride = pkgs: unit: text: (pkgs.runCommandNoCC unit { preferLocalBuild = true; allowSubstitutes = false; } ''
mkdir -p $out/${lib.escapeShellArg "/etc/systemd/system/${unit}.d/"}
<<<${lib.escapeShellArg text} cat >$out/${lib.escapeShellArg "/etc/systemd/system/${unit}.d/override.conf"}
'');
# Given a message and any value, traces both the message and the value, and returns the value.
trace = message: value: (builtins.trace (message +": "+ (lib.generators.toPretty { } value)) value);
}

271
lib/nixos.nix Normal file
View File

@ -0,0 +1,271 @@
dirname: inputs@{ self, nixpkgs, functions, ...}: let
inherit (nixpkgs) lib;
inherit (functions.lib) extractBashFunction forEachSystem getModulesFromInputs getNixFiles getOverlaysFromInputs importWrapped mapMerge mapMergeUnique mergeAttrsUnique substituteImplicit;
setup-scripts = (import "${dirname}/setup-scripts" "${dirname}/setup-scripts" inputs);
inherit (inputs.config.rename) installer; preface' = inputs.config.rename.preface;
getModuleConfig = module: inputs: args: if builtins.isFunction module then (
getModuleConfig (module args) inputs args
) else if (builtins.isPath module) || (builtins.isString module) then (
getModuleConfig (importWrapped inputs module).required inputs args
) else if module?config then module.config else if module?_file && module?imports then (
getModuleConfig (builtins.head module.imports) inputs args
) else module;
getPreface = inputs: moduleArgs: mainModule: name: let
args = { config = null; pkgs = null; lib = null; name = null; nodes = null; extraModules = null; } // { inherit inputs; } // moduleArgs // { name = name; };
config = getModuleConfig mainModule inputs args;
in config.${preface'} or { };
in rec {
# Builds the System Configuration for a single host.
mkNixosConfiguration = {
mainModule, name,
# See »mkSystemsFlake« for documentation of the following arguments:
inputs ? { }, modules ? (getModulesFromInputs inputs), overlays ? (getOverlaysFromInputs inputs),
extraModules ? [ ], moduleArgs ? { },
nixosSystem ? inputs.nixpkgs.lib.nixosSystem,
buildPlatform ? null,
}: nixosSystem {
#system = null; # (This actually does nothing more than setting »config.nixpkgs.system« (which is the same as »config.nixpkgs.buildPlatform.system«) and can be null/unset here.)
modules = [ { imports = [ # Anything specific to only this evaluation of the module tree should go here.
(if (builtins.isPath mainModule) || (builtins.isString mainModule) then (importWrapped inputs mainModule).module else mainModule)
{ _module.args.name = lib.mkOverride 99 name; } # (specialisations can somehow end up with the name »configuration«, which is very incorrect)
{ networking.hostName = name; }
]; _file = "${dirname}/nixos.nix#modules"; } ];
extraModules = modules ++ extraModules ++ [ ({ config, ... }: { imports = [ ({
# These are passed as »extraModules« module argument and can thus be reused when defining containers and such (so define as much stuff as possible here).
# There is, unfortunately, no way to directly pass modules into all containers. Each container will need to be defined with »config.containers."${name}".config.imports = extraModules«.
# (One could do that automatically by defining »options.security.containers = lib.mkOption { type = lib.types.submodule (cfg: { options.config = lib.mkOption { apply = _:_.extendModules { modules = extraModules; }; }); }«.)
nixpkgs = { overlays = overlays; } // (lib.optionalAttrs (buildPlatform != null) { inherit buildPlatform; });
_module.args = { inherit inputs; } // moduleArgs; # (pass the args here, so that they also apply to any other evaluation using »extraModules«)
system.nixos.revision = lib.mkIf (inputs?nixpkgs.rev) inputs.nixpkgs.rev; # (evaluating the default value fails under some circumstances)
}) ]; _file = "${dirname}/nixos.nix#mkNixosConfiguration-extraModule"; }) ];
specialArgs = { inherit inputs; };
# (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.)
};
# Given either a list (or attr set) of »files« (paths to ».nix« or ».nix.md« files for dirs with »default.nix« files in them) or a »dir« path (and optionally a list of base names to »exclude« from it), this builds the NixOS configuration for each host (per file) in the context of all configs provided.
# If »files« is an attr set, exactly one host with the attribute's name as hostname is built for each attribute. Otherwise the default is to build for one host per configuration file, named as the file name without extension or the sub-directory name. Setting »${preface'}.instances« can override this to build the same configuration for those multiple names instead (the specific »name« is passed as additional »moduleArgs« to the modules and can thus be used to adjust the config per instance).
# All other arguments are as specified by »mkSystemsFlake« and are passed to »mkNixosConfiguration«.
mkNixosConfigurations = args: let # { files, dir, exclude, ... }
files = args.files or (builtins.removeAttrs (getNixFiles args.dir) (args.exclude or [ ]));
files' = if builtins.isAttrs files then files else (builtins.listToAttrs (map (entryPath: let
stripped = builtins.match ''^(.*)[.]nix[.]md$'' (builtins.baseNameOf entryPath);
name = builtins.unsafeDiscardStringContext (if stripped != null then (builtins.elemAt stripped 0) else (builtins.baseNameOf entryPath));
in { inherit name; value = entryPath; }) files));
moduleArgs = (args.moduleArgs or { }) // { nodes = configs; };
configs = mapMergeUnique (prelimName: mainModule: (let
instances = let
preface = getPreface inputs moduleArgs mainModule null; # (we don't yet know the final name)
in if !(args?files && builtins.isAttrs files) && preface?instances then preface.instances else [ prelimName ];
in (mapMergeUnique (name: { "${name}" = let
preface = getPreface inputs moduleArgs mainModule name; # (call again, with name)
in { inherit preface; } // (mkNixosConfiguration ((
builtins.removeAttrs args [ "files" "dir" "exclude" ]
) // {
inherit name mainModule moduleArgs; extraModules = (args.extraModules or [ ]) ++ [ { imports = [ ({
options.${preface'} = {
instances = lib.mkOption { description = "List of host names to instantiate this host config for, instead of just for the file name."; type = lib.types.listOf lib.types.str; readOnly = true; } // (lib.optionalAttrs (!preface?instances) { default = instances; });
id = lib.mkOption { description = "This system's ID. If set, »mkSystemsFlake« will ensure that the ID is unique among all »moduleArgs.nodes«."; type = lib.types.nullOr (lib.types.either lib.types.int lib.types.str); readOnly = true; apply = id: if id == null then null else toString id; } // (lib.optionalAttrs (!preface?id) { default = null; });
};
}) ]; _file = "${dirname}/nixos.nix#mkNixosConfigurations-extraModule"; } ];
})); }) instances))) (files');
duplicate = let
getId = node: name: let id = node.preface.id or null; in if id == null then null else toString id;
withId = lib.filterAttrs (name: node: (getId node name) != null) configs;
ids = mapMerge (name: node: { "${getId node name}" = name; }) withId;
in builtins.removeAttrs withId (builtins.attrValues ids);
in if duplicate != { } then (
throw "»${preface'}.id«s are not unique! The following hosts share their IDs with some other host: ${builtins.concatStringsSep ", " (builtins.attrNames duplicate)}"
) else configs;
# Builds a system of NixOS hosts and exports them, plus »apps« and »devShells« to manage them, as flake outputs.
# All arguments are optional, as long as the default can be derived from the other arguments as passed.
mkSystemsFlake = args@{
# An attrset of imported Nix flakes, for example the argument(s) passed to the flake »outputs« function. All other arguments are optional (and have reasonable defaults) if this is provided and contains »self« and the standard »nixpkgs«. This is also the second argument passed to the individual host's top level config files.
inputs ? { },
# Arguments »{ files, dir, exclude, }« to »mkNixosConfigurations«, see there for details. May also be a list of those attrsets, in which case those multiple sets of hosts will be built separately by »mkNixosConfigurations«, allowing for separate sets of »peers« passed to »mkNixosConfiguration«. Each call will receive all other arguments, and the resulting sets of hosts will be merged.
systems ? ({ dir = "${inputs.self}/hosts"; exclude = [ ]; }),
# 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 ? (getModulesFromInputs moduleInputs),
# (Subset of) »inputs« that »modules« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
moduleInputs ? inputs,
# List of additional modules to import for all hosts.
extraModules ? [ ],
# List of overlays to set as »config.nixpkgs.overlays«. Defaults to ».overlays.default« of all »overlayInputs«/»inputs« (incl. »inputs.self«).
overlays ? (getOverlaysFromInputs overlayInputs),
# (Subset of) »inputs« that »overlays« will be used from. Example: »{ inherit (inputs) self flakeA flakeB; }«.
overlayInputs ? inputs,
# Additional arguments passed to each module evaluated for the host config (if that module is defined as a function).
moduleArgs ? { },
# The »nixosSystem« function defined in »<nixpkgs>/flake.nix«, or equivalent.
nixosSystem ? inputs.nixpkgs.lib.nixosSystem,
# If provided, this will be set as »config.nixpkgs.buildPlatform« for all hosts, which in turn enables cross-compilation for all hosts whose »config.nixpkgs.hostPlatform« (the architecture they will run on) does not expand to the same value. Without this, building for other platforms may still work (slowly) if »boot.binfmt.emulatedSystems« on the building system is configured for the respective target(s).
buildPlatform ? null,
## 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 (by then merging the results):
renameOutputs ? false,
... }: let
getName = if renameOutputs == false then (name: name) else renameOutputs;
otherArgs = (builtins.removeAttrs args [ "renameOutputs" ]) // {
inherit inputs modules overlays moduleArgs nixosSystem buildPlatform;
extraModules = (args.extraModules or [ ]) ++ [ { imports = [ ({ name, ... }/**/: {
${installer}.outputName = getName name;
}) ]; _file = "${dirname}/nixos.nix#mkSystemsFlake-extraModule"; } ];
};
nixosConfigurations = if builtins.isList systems then mergeAttrsUnique (map (systems: mkNixosConfigurations (otherArgs // systems)) systems) else mkNixosConfigurations (otherArgs // systems);
in let outputs = {
inherit nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (buildSystem: let
pkgs = (import inputs.nixpkgs { inherit overlays; system = buildSystem; });
tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites));
in rec {
apps = lib.mapAttrs (name: system: rec { type = "app"; derivation = writeSystemScripts { inherit name pkgs system; }; program = "${derivation}"; }) nixosConfigurations;
# dummy that just pulls in all system builds
packages.all-systems = pkgs.runCommandLocal "all-systems" { } ''
${''
mkdir -p $out/systems
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${system.config.system.build.toplevel} $out/systems/${getName name}") nixosConfigurations)}
''}
${''
mkdir -p $out/scripts
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: system: "ln -sT ${apps.${name}.program} $out/scripts/${getName name}") nixosConfigurations)}
''}
${lib.optionalString (inputs != { }) ''
mkdir -p $out/inputs
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: { outPath, ... }: "ln -sT ${outPath} $out/inputs/${name}") inputs)}
''}
'';
checks.all-systems = packages.all-systems;
})); in if renameOutputs == false then outputs else {
nixosConfigurations = mapMergeUnique (k: v: { ${renameOutputs k} = v; }) outputs.nixosConfigurations;
} // (forEachSystem [ "aarch64-linux" "x86_64-linux" ] (buildSystem: {
apps = mapMergeUnique (k: v: { ${renameOutputs k} = v; }) outputs.apps.${buildSystem};
packages.${renameOutputs "all-systems"} = outputs.packages.${buildSystem}.all-systems;
checks.${renameOutputs "all-systems"} = outputs.checks.${buildSystem}.all-systems;
}));
# This makes the »./setup-scripts/*« callable from the command line:
writeSystemScripts = {
system, # The NiOS definition of the system that the scripts are supposed to manage.
name ? system._module.args.name, # The system's name.
pkgs, # Package set for the host calling these scripts, which is not necessarily the same as »system«'s.
}: let
description = ''
Call per-host setup and maintenance commands. Most importantly, »install-system«.
'';
ownPath = if exported then "nix run REPO#${system.config.${installer}.outputName} --" else "$0";
usageLine = ''
Usage:
%s [sudo] [bash] [--FLAG[=value]]... [--] [COMMAND [ARG]...]
${lib.optionalString exported ''
Where »REPO« is the path to a flake repo using »mkSystemsFlake« for it's »apps« output.
''} If the first argument (after the first »--«) 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 »COMMAND«s (bash functions and exported Nix values used by them) sourced.
If a »FLAG« »--command« is supplied, then the first positional argument (»COMMAND«) is »eval«ed as bash instructions, otherwise the first argument should be one of the »COMMAND«s below, which will be called with the positional CLI »ARG«s as arguments.
»FLAG«s may be set to customize the behavior of »COMMAND« or any sub-commands it or »SCRIPT« call.
»COMMAND« should be one of:%s
»FLAG«s may be any of:
''; # printf string that gets passed the flake path and the COMMANDs list, and that is followed by the FLAGs list
notesAndExamples = ''
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)
# Now run any of the »COMMAND«s above, or inspect/use the exported Nix variables (»declare -p config_<TAB><TAB>«).
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 .#other-host -- sudo
'';
exported = system.options.${installer}.outputName.isDefined;
tools = lib.unique (map (p: p.outPath) (lib.filter lib.isDerivation pkgs.stdenv.allowedRequisites));
esc = lib.escapeShellArg;
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 != ${pkgs.bashInteractive}/bin/bash ]] ; then
shift ; 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
# add active »hostName« to shell prompt
PS1=''${PS1/\\$/\\[\\e[93m\\](${name})\\[\\e[97m\\]\\$}
''}EOS
cat $0) -i -s ':' "$@"
fi
# provide installer tools (not necessarily for system.pkgs.config.hostPlatform)
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
args[trace]=1
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
declare -g -A allowedArgs=( ) allowedArgCtx=( ) ; function declare-flag { # 1: context, 2: name, 3?: value, 4: description
if [[ ''${allowedArgCtx[$2]:-} && ''${allowedArgCtx[$2]:-} != "$1" ]] ; then echo "Flag $2 was declared in conflicting contexts ''${allowedArgCtx[$2]} and $1" >&2 ; \exit 1 ; fi
allowedArgCtx[$2]=$1
local name=--$2 ; if [[ $3 ]]; then name+='='$3 ; fi ; allowedArgs[$name]="($1) $4"
}
declare-flag global command "" 'Interpret the first positional argument as bash script (instead of the name of a single command) and »eval« it (with access to all commands and internal functions and variables).'
declare-flag global debug "" 'Hook into any »|| exit« / »|| return« statements and open a shell if they are triggered by an error. Implies »--trace«.'
declare-flag global trace "" "Turn on bash's »errtrace« option before running »COMMAND«."
declare-flag global quiet "" "Try to suppress all non-error output. May also swallow some error related output."
declare -g -A allowedCommands=( ) ; function declare-command { allowedCommands[$@]=$(< /dev/stdin) ; }
${system.config.${installer}.build.scripts { native = pkgs; }}
if [[ ''${args[help]:-} ]] ; then (
functionDoc= ; while IFS= read -u3 -r name ; do
functionDoc+=$'\n\n '"$name"$'\n '"''${allowedCommands[$name]//$'\n'/$'\n '}" #$'\n\n'
done 3< <( printf '%s\n' "''${!allowedCommands[@]}" | LC_ALL=C sort )
generic-arg-help "${ownPath}" "$functionDoc" ${esc description} ${esc notesAndExamples} ${esc usageLine} || exit
) ; \exit 0 ; fi
# generic-arg-verify || \exit 3
# either call »argv[0]« with the remaining parameters as arguments, or if »$1« is »-c« eval »$2«.
if [[ ''${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.'} || exit
argv=( "''${argv[@]:1}" ) ; set -- "''${argv[@]}" ; eval "$command" || exit
else
entry=''${argv[0]:?} || exit
argv=( "''${argv[@]:1}" ) ; "$entry" "''${argv[@]}" || exit
fi
'';
}

View File

@ -1,114 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
inherit (import "${dirname}/vars.nix" dirname inputs) extractLineAnchored;
in rec {
# Turns an attr set into a bash dictionary (associative array) declaration, e.g.:
# bashSnippet = "declare -A dict=(\n${asBashDict { } { foo = "42"; bar = "baz"; }})"
asBashDict = { mkName ? (n: v: n), mkValue ? (n: v: v), indent ? " ", ... }: attrs: (
builtins.concatStringsSep "" (lib.mapAttrsToList (name: value: (
let key = mkName name value; in if key == null then "" else
"${indent}[${lib.escapeShellArg key}]=${lib.escapeShellArg (mkValue name value)}\n"
)) attrs)
);
# This function allows using nix values in bash scripts without passing an explicit and manually curated list of values to the script.
# Given a path list of bash script »sources« and an attrset »context«, this function parses the scripts for the literal sequence »@{« followed by a lookup path of period-joined words, resolves that attribute path against »context«, declares a variable with that value, and swaps out the »@{« plus path for a »${« use of the declared variable. The returned script sources the variable definitions and all translated »sources« in order.
# The lookup path may end in »!« plus the name of a function and optionally string arguments separated by ».«s, in which case the function is taken from »helpers//self.lib.wip//(pkgs.lib or lib)//pkgs//builtins« and called with the string args and the resolved value as last arg; the return value then replaces the resolved value. Examples: »!attrNames«, »!toJSON«, »!catAttrs«, »!hashString.sha256«.
# The names of the declared values are the lookup paths, with ».« and »!« replaced by »_« and »__«.
# The symbol immediately following the lookup path (/builtin name) can be »}« or any other symbol that bash variable substitutions allow after the variable name (like »:«, »/«), eliminating the need to assign to a local variable to do things like replacements, fallbacks or substrings.
# If the lookup path does not exist in »context«, then the value will be considered the same as »null«, and a value of »null« will result in a bash variable that is not defined (which can then be handled in the bash script).
# Other scalars (bool, float, int, path) will be passed to »builtins.toString«. Anything that has an ».outPath« that is a string will be passed as that ».outPath«.
# Lists will be declared as bash arrays, attribute sets will be declared as associative arrays using »asBashDict«.
# Bash does not support any nested data structures. Lists or attrsets in within lists or attrsets are therefore (recursively) encoded and escaped as strings, such that calling »eval« on them is safe if (but only if) they are known to be encoded from nested lists/attrsets. Example: »eval 'declare -A fs='"@{config.fileSystems['/']}" ; root=${fs[device]}«.
# Any other value (functions), and things that »builtins.toString« doesn't like, will throw here.
substituteImplicit = args@{
scripts, # List of paths to scripts to process and then source in the returned script. Each script may also be an attrset »{ name; text; }« instead of a path.
context, # The root attrset for the resolution of substitutions.
pkgs, # Instantiated »nixpkgs«, as fallback location for helpers, and to grab »writeScript« etc from.
helpers ? { }, # Attrset of (highest priority) helper functions.
trace ? (m: v: v), # Function that gets called with the names and values as they are processed. Pass »builtins.trace« for debugging, esp. when evaluating one of the accessed values fails.
}: let
scripts = map (source: rec {
text = if builtins.isAttrs source then source.text else builtins.readFile source; name = if builtins.isAttrs source then source.name else builtins.baseNameOf source;
parsed = builtins.split ''@\{([#!]?)([a-zA-Z][a-zA-Z0-9_.-]*[a-zA-Z0-9](![a-zA-Z][a-zA-Z0-9_.-]*[a-zA-Z0-9])?)([:*@\[#%/^,\}])'' text; # (first part of a bash parameter expansion, with »@« instead of »$«)
}) args.scripts;
decls = lib.unique (map (match: builtins.elemAt match 1) (builtins.filter builtins.isList (builtins.concatMap (script: script.parsed) scripts)));
vars = pkgs.writeText "vars" ("#!/usr/bin/env bash\n" + (lib.concatMapStringsSep "\n" (decl: let
call = let split = builtins.split "!" decl; in if (builtins.length split) == 1 then null else builtins.elemAt split 2;
path = (builtins.filter builtins.isString (builtins.split "[.]" (if call == null then decl else builtins.substring 0 ((builtins.stringLength decl) - (builtins.stringLength call) - 1) decl)));
resolved = lib.attrByPath path null context;
applied = if call == null || resolved == null then resolved else (let
split = builtins.filter builtins.isString (builtins.split "[.]" call); name = builtins.head split; args = builtins.tail split;
func = builtins.foldl' (func: arg: func arg) (helpers.${name} or self.lib.wip.${name} or (pkgs.lib or lib).${name} or pkgs.${name} or builtins.${name}) args;
in func resolved);
value = if builtins.isString (applied.outPath or null) then applied.outPath else if (
(builtins.isBool applied) || (builtins.isFloat applied) || (builtins.isInt applied) || (builtins.isPath applied)
) then builtins.toString applied else applied;
name = trace "substituteImplicit »${decl}« =>" (builtins.replaceStrings [ "." "!" "-" ] [ "_" "1" "0" ] decl);
toStringRecursive = value: if builtins.isString (value.outPath or null) then (
value.outPath
) else if builtins.isAttrs value then (
"(\n${asBashDict { mkName = name: value: if value == null then null else name; mkValue = name: toStringRecursive; } value})"
) else if (builtins.isList value) then (
"( ${lib.escapeShellArgs (map toStringRecursive value)} )"
) else (toString value);
in (let final = (
if (value == null) then "#${name}=null"
else if (builtins.isString value) then "${name}=${lib.escapeShellArg value}"
else if (builtins.isList value) then "${name}=${toStringRecursive value}"
else if (builtins.isAttrs value) then "declare -A ${name}=${toStringRecursive value}"
else throw "Can't use value of unsupported type ${builtins.typeOf} as substitution for ${decl}" # builtins.isFunction
); in trace final final)) decls));
in ''
source ${vars}
${lib.concatMapStringsSep "\n" (script: "source ${pkgs.writeScript script.name (
lib.concatMapStringsSep "" (seg: if builtins.isString seg then seg else (
"$"+"{"+(builtins.head seg)+(builtins.replaceStrings [ "." "!" "-" ] [ "_" "1" "0" ] (builtins.elemAt seg 1))+(toString (builtins.elemAt seg 3))
)) script.parsed
)}") scripts}
'';
## Given a bash »script« as string and a function »name«, this finds and extracts the definition of that function in and from the script.
# The function definition has to start at the beginning of a line and must ends the next »}« of »)}« at the beginning of a line that is followed by nothing but a comment on that line.
extractBashFunction = script: name: let
inherit (extractLineAnchored ''${name}[ ]*[(][ ]*[)]|function[ ]+${name}[ ]'' true false script) line after;
body = builtins.split "(\n[)]?[}])([ ]*[#][^\n]*)?\n" after;
in if (builtins.length body) < 3 then null else line + (builtins.head body) + (builtins.head (builtins.elemAt body 1));
# Used as a »system.activationScripts« snippet, this performs substitutions on a »text« before writing it to »path«.
# For each name-value pair in »substitutes«, all verbatim occurrences of the attribute name in »text« are replaced by the content of the file with path of the attribute value.
# Since this happens one by one in no defined order, the attribute values should be chosen such that they don't appear in any of the files that are substituted in.
# If a file that is supposed to be substituted in is missing, then »placeholder« is inserted instead, and the activation snipped reports a failure.
# If »enable« is false, then the file at »path« is »rm«ed instead.
writeSubstitutedFile = { enable ? true, path, text, substitutes, placeholder ? "", perms ? "440", owner ? "root", group ? "root", }: let
hash = builtins.hashString "sha256" text;
esc = lib.escapeShellArg;
in { "write ${path}" = if enable then ''
text=$(cat << '#${hash}'
${text}
#${hash}
)
${builtins.concatStringsSep "\n" (lib.mapAttrsToList (name: file: "text=\"\${text//${esc name}/$( if ! cat ${esc file} ; then printf %s ${esc placeholder} ; false ; fi )}\"") substitutes)}
install -m ${esc (toString perms)} -o ${esc (toString owner)} -g ${esc (toString group)} /dev/null ${esc path}
<<<"$text" cat >${esc path}
'' else ''rm ${esc path} || true''; };
# Wraps a (bash) script into a "package", making »deps« available on the script's path.
wrap-script = args@{ pkgs, src, deps, ... }: let
name = args.name or (builtins.baseNameOf (builtins.unsafeDiscardStringContext "${src}"));
in pkgs.runCommandLocal name {
script = src; nativeBuildInputs = [ pkgs.buildPackages.makeWrapper ];
} ''makeWrapper $script $out/bin/${name} --prefix PATH : ${lib.makeBinPath deps}'';
# Simplifies a path (or any other string) such that it can be used as a systemd unit name.
escapeUnitName = name: lib.concatMapStringsSep "" (s: if builtins.isList s then "-" else s) (builtins.split "[^a-zA-Z0-9_.\\-]+" name); # from nixos/modules/services/backup/syncoid.nix
pathToName = path: (builtins.replaceStrings [ "/" ":" ] [ "-" "-" ] path);
# Given a »path«, returns the list of all its parents, starting with »path« itself and ending with only its first segment.
# Examples: "/a/b/c" -> [ "/a/b/c" "/a/b" "/a" ] ; "x/y" -> [ "x/y" "x" ]
parentPaths = path: let
absolute = if lib.hasPrefix "/" path then 1 else 0; prefix = if absolute == 1 then "/" else "";
split = builtins.filter builtins.isString (builtins.split ''/'' (builtins.substring (absolute) ((builtins.stringLength path) - absolute - (if lib.hasSuffix "/" path then 1 else 0)) path));
in map (length: prefix + (builtins.concatStringsSep "/" (lib.take length split))) (lib.reverseList ((lib.range 1 (builtins.length split))));
}

View File

@ -3,42 +3,22 @@
This is a library of bash functions, mostly for NixOS system installation.
The (paths to these) scripts are meant to be (and by default are) set as `config.wip.setup.scripts.*` (see [`../flakes.nix`](../flakes.nix)), which makes their functions available in the per-host [`devShells`/`apps`](../flakes.nix#mkSystemsFlake).
The (paths to these) scripts are meant to be (and are by default when importing [`../../modules/installer.nix.md`](../../modules/installer.nix.md)) set as `config.installer.scripts.*`.
[`mkSystemsFlake`](../nixos.nix#mkSystemsFlake) then makes their functions available in the per-host `devShells`/`apps`.
Host-specific nix variables are available to the bash functions as `@{...}` through [`substituteImplicit`](../scripts.nix#substituteImplicit) with the respective host as root context.
Any script passed later in `scripts` can overwrite the functions of these (earlier) default scripts.
With the functions from here, [a simple three-liner](./install.sh) is enough to do a completely automated NixOS installation:
```bash
function install-system {( # 1: diskPaths
prepare-installer "$@" || exit
do-disk-setup "${argv[0]}" || exit
install-system-to $mnt || exit
)}
Any script passed later in `scripts` [can override](../../example/install.sh.md#implementation) the functions of these (earlier) default scripts, e.g.:
```nix
{ config.installer.scripts.override = { path = .../override.sh; order = 1500; }; }
```
# `install-system` Documentation
For repositories that use the `lib.wip.mkSystemsFlake` Nix function in their `flake.nix`, the above bash function performs the automated installation of any `nixosConfigurations.$HOST`s (where the host's configurations would usually be placed in the `/hosts/` directory of the repository) to the local disk(s) (or image file(s)) `$DISK`.
On a NixOS host or with a Nix multi-user installation, this can be run by root as: `#` `nix run .#"$HOST" -- install-system "$DISK"`.
Doing an installation on non-NixOS (but Linux), where nix isn't installed for root, the process is a bit of a hack, but works as well.
In this case, all `nix` commands will be run as `$SUDO_USER`, but this script and some other user-owned (or user-generated) code will (need to) be run as root.
If that is acceptable, run with `sudo` as first argument: `$` `nix run .#"$HOST" -- sudo install-system "$DISK"` (And then maybe `sudo bash -c 'chown $SUDO_USER: '"$DISK"` afterwards.)
If `$DISK` points to something in `/dev/`, then it is directly formatted and written to as block device, otherwise `$DISK` is (re-)created as raw image and then used as loop device.
For hosts that install to multiple disks, pass a `:`-separated list of `<disk-name>=<path>` pairs (the name may be omitted only for the "`default`" disk).
Once done, the disk can be transferred -- or the image be copied -- to the final system, and should boot there.
If the host's hardware target allows, a resulting image can also be passed to [`register-vbox`](./maintenance.sh#register-vbox) to create a bootable VirtualBox instance for the current user, or to [`run-qemu`](./maintenance.sh#run-qemu) to start it in a qemu VM.
The "Installation" section of each host's documentation should contain host specific details, if any.
See `nix run .#$HOST -- --help` to see how to use the result.
## Development Notes
* The functions are designed to be (and by default are) executed with the bash options `pipefail` and `nounset` (`-u`) set.
* When the functions are executed, `generic-arg-parse` has already been called on the CLI arguments, and the parsed result can be accessed as `"${args[<name>]:-}"` for named arguments and `"${argv[<index>]}"` for positional arguments (except the first one, which has been removed and used as the command or name of the entry function to run).
* When adding functions that are meant to be called as top-level `COMMAND`s, make sure to document them by calling `declare-command`. See esp. [`maintenance.sh`](./maintenance.sh) for examples. Similarly, use `declare-flag` to add new flags to the `--help` output.
* Do not use `set -e`. It has some unexpected and unpredictable behavior, and *does not* actually provide the expected semantic of "exit the shell if a command fails (exits != 0)". For example, the internal exit behavior of commands in a function depends on *how the function is called*.
* If the `--debug` flag is passed, then `return` and `exit` are aliased to open a shell when `$?` is not zero. This effectively turns any `|| return` / `|| exit` into break-on-error point.
* The aliasing does not work if an explicit code is provided to `return` or `exit`. In these cases, or where the breakpoint behavior is not desired, use `\return` or `\exit` (since the `\` suppresses the alias expansion).

View File

@ -8,13 +8,13 @@
## Outputs nothing (/ an empty key), causing that ZFS dataset to be unencrypted, even if it's parent is encrypted.
function gen-key-unencrypted {( set -eu # 1: usage
:
: # TODO: write-secret does not allow empty secrets anymore (might want to change that back)
)}
## Uses the hostname as a trivial key.
function gen-key-hostname {( set -eu # 1: usage
usage=$1
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf '»trivial« key mode is only available for the keystore itself.\n' 1>&2 ; \exit 1 ; fi
if [[ ! "$usage" =~ ^(luks/keystore-@{config.networking.hostName!hashString.sha256:0:8}/.*)$ ]] ; then printf 'The trivial »hostname« key mode is only available for the keystore itself.\n' 1>&2 ; \exit 1 ; fi
printf %s "@{config.networking.hostName}"
)}

View File

@ -1,14 +1,13 @@
dirname: inputs: let
inherit (inputs.config) prefix;
inherit (import "${dirname}/../imports.nix" dirname inputs) getFilesExt;
inherit (inputs.config.rename) setup installer;
replacePrefix = if prefix == "wip" then (x: x) else (builtins.mapAttrs (name: path: (
doRenames = if setup == "setup" && installer == "installer" then (x: x) else (builtins.mapAttrs (name: path: (
builtins.toFile name (builtins.replaceStrings
[ "@{config.wip." "@{#config.wip." "@{!config.wip." ]
[ "@{config.${prefix}." "@{#config.${prefix}." "@{!config.${prefix}." ]
[ "@{config.setup." "@{#config.setup." "@{!config.setup." "@{config.installer." "@{#config.installer." "@{!config.installer." ]
[ "@{config.${setup}." "@{#config.${setup}." "@{!config.${setup}." "@{config.${installer}." "@{#config.${installer}." "@{!config.${installer}." ]
(builtins.readFile path)
)
)));
in replacePrefix (getFilesExt "sh(.md)?" dirname)
in doRenames (inputs.functions.lib.getFilesExt "sh(.md)?" dirname)

View File

@ -14,16 +14,16 @@ function do-disk-setup { # 1: diskPaths
partition-disks || return
create-luks-layers && open-luks-layers || return # other block layers would go here too (but figuring out their dependencies would be difficult)
run-hook-script 'Post Partitioning' @{config.wip.fs.disks.postPartitionCommands!writeText.postPartitionCommands} || return
run-hook-script 'Post Partitioning' @{config.installer.commands.postPartition!writeText.postPartitionCommands} || return
format-partitions || return
if [[ $(LC_ALL=C type -t create-zpools) == function ]] ; then create-zpools $mnt || return ; fi
run-hook-script 'Post Formatting' @{config.wip.fs.disks.postFormatCommands!writeText.postFormatCommands} || return
run-hook-script 'Post Formatting' @{config.installer.commands.postFormat!writeText.postFormatCommands} || return
fix-grub-install || return
prepend_trap "unmount-system $mnt" EXIT && mount-system $mnt || return
run-hook-script 'Post Mounting' @{config.wip.fs.disks.postMountCommands!writeText.postMountCommands} || return
run-hook-script 'Post Mounting' @{config.installer.commands.postMount!writeText.postMountCommands} || return
}
# Notes on segmentation and alignment:
@ -34,14 +34,15 @@ function do-disk-setup { # 1: diskPaths
# * (source: https://lwn.net/Articles/428584/)
# * So alignment at the default »align=8MiB« actually seems a decent choice.
declare-flag install-system image-owner "" "When using image files, »chown« them to this »owner[:group]« before the installation."
## Parses and expands »diskPaths« to ensure that a disk or image exists for each »config.wip.fs.disks.devices«, creates and loop-mounts images for non-/dev/ paths, and checks whether physical device sizes match.
## Parses and expands »diskPaths« to ensure that a disk or image exists for each »config.setup.disks.devices«, creates and loop-mounts images for non-/dev/ paths, and checks whether physical device sizes match.
function ensure-disks { # 1: diskPaths, 2?: skipLosetup
declare -g -A blockDevs=( ) # this ends up in the caller's scope
if [[ $1 == */ ]] ; then
mkdir -p "$1"
for name in "@{!config.wip.fs.disks.devices[@]}" ; do blockDevs[$name]=${1}${name}.img ; done
for name in "@{!config.setup.disks.devices[@]}" ; do blockDevs[$name]=${1}${name}.img ; done
else
local path ; for path in ${1//:/ } ; do
local name=${path/=*/} ; if [[ $name != "$path" ]] ; then path=${path/$name=/} ; else name=primary ; fi
@ -50,10 +51,10 @@ function ensure-disks { # 1: diskPaths, 2?: skipLosetup
done
fi
local name ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do
if [[ ! @{config.wip.fs.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
local name ; for name in "@{!config.setup.disks.devices[@]}" ; do
if [[ ! @{config.setup.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
if [[ ! ${blockDevs[$name]:-} ]] ; then echo "Path for block device $name not provided" 1>&2 ; \return 1 ; fi
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
if [[ ${blockDevs[$name]} != /dev/* ]] ; then
local outFile=${blockDevs[$name]} &&
install -m 640 -T /dev/null "$outFile" && truncate -s "${disk[size]}" "$outFile" || return
@ -72,36 +73,36 @@ function ensure-disks { # 1: diskPaths, 2?: skipLosetup
}
## Partitions the »blockDevs« (matching »config.wip.fs.disks.devices«) to ensure that all specified »config.wip.fs.disks.partitions« exist.
## Partitions the »blockDevs« (matching »config.setup.disks.devices«) to ensure that all specified »config.setup.disks.partitions« exist.
# Tries to abort if any partition already exists on the host.
function partition-disks {
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
for partDecl in "@{config.setup.disks.partitionList[@]}" ; do
eval 'local -A part='"$partDecl"
if [[ -e /dev/disk/by-partlabel/"${part[name]}" ]] && ! is-partition-on-disks /dev/disk/by-partlabel/"${part[name]}" "${blockDevs[@]}" ; then echo "Partition /dev/disk/by-partlabel/${part[name]} already exists on this host and does not reside on one of the target disks ${blockDevs[@]}. Refusing to create another partition with the same partlabel!" 1>&2 ; \return 1 ; fi
done
for name in "@{!config.wip.fs.disks.devices[@]}" ; do
if [[ ! @{config.wip.fs.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
for name in "@{!config.setup.disks.devices[@]}" ; do
if [[ ! @{config.setup.disks.devices!catAttrSets.partitionDuringInstallation[$name]} ]] ; then continue ; fi
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
if [[ ${disk[serial]:-} ]] ; then
actual=$( @{native.systemd}/bin/udevadm info --query=property --name="$blockDev" | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' )
if [[ ${disk[serial]} != "$actual" ]] ; then echo "Block device $blockDev's serial ($actual) does not match the serial (${disk[serial]}) declared for ${disk[name]}" 1>&2 ; \return 1 ; fi
fi
# can (and probably should) restore the backup:
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.wip.fs.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent ) || return
( PATH=@{native.gptfdisk}/bin ; ${_set_x:-:} ; sgdisk --zap-all --load-backup=@{config.setup.disks.partitioning}/"${disk[name]}".backup ${disk[allowLarger]:+--move-second-header} "${blockDevs[${disk[name]}]}" >$beLoud 2>$beSilent ) || return
#partition-disk "${disk[name]}" "${blockDevs[${disk[name]}]}" || return
done
@{native.parted}/bin/partprobe "${blockDevs[@]}" &>$beLoud || return
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
# ensure that filesystem creation does not complain about the devices already being occupied by a previous filesystem
local toWipe=( ) ; for part in "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do [[ ! -e "$part" ]] || toWipe+=( "$part" ) ; done
local toWipe=( ) ; for part in "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do [[ ! -e "$part" ]] || toWipe+=( "$part" ) ; done
@{native.util-linux}/bin/wipefs --all "${toWipe[@]}" >$beLoud 2>$beSilent || return
#</dev/zero head -c 4096 | tee "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >/dev/null
#for part in "@{config.wip.fs.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do @{native.util-linux}/bin/blkdiscard -f "$part" || return ; done
#</dev/zero head -c 4096 | tee "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" >/dev/null
#for part in "@{config.setup.disks.partitions!attrNames[@]/#/'/dev/disk/by-partlabel/'}" ; do @{native.util-linux}/bin/blkdiscard -f "$part" || return ; done
}
## Given a declared disk device's »name« and a path to an actual »blockDev« (or image) file, partitions the device as declared in the config.
@ -109,7 +110,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
local name=$1 ; local blockDev=$2
local beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
local beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
eval 'local -A disk='"@{config.wip.fs.disks.devices[$name]}"
eval 'local -A disk='"@{config.setup.disks.devices[$name]}"
local devSize=${3:-$( @{native.util-linux}/bin/blockdev --getsize64 "$blockDev" )}
local -a sgdisk=( --zap-all ) # delete existing part tables
@ -119,7 +120,7 @@ function partition-disk { # 1: name, 2: blockDev, 3?: devSize
fi
sgdisk+=( --disk-guid="${disk[guid]}" )
for partDecl in "@{config.wip.fs.disks.partitionList[@]}" ; do
for partDecl in "@{config.setup.disks.partitionList[@]}" ; do
eval 'local -A part='"$partDecl"
if [[ ${part[disk]} != "${disk[name]}" ]] ; then continue ; fi
if [[ ${part[size]:-} =~ ^[0-9]+%$ ]] ; then
@ -206,7 +207,7 @@ function fix-grub-install {
if [[ ! @{config.fileSystems[$mount]:-} ]] ; then continue ; fi
device=$( eval 'declare -A fs='"@{config.fileSystems[$mount]}" ; echo "${fs[device]}" )
label=${device/\/dev\/disk\/by-partlabel\//}
if [[ $label == "$device" || $label == *' '* || ' '@{config.wip.fs.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; \return 1 ; fi
if [[ $label == "$device" || $label == *' '* || ' '@{config.setup.disks.partitions!attrNames[@]}' ' != *' '$label' '* ]] ; then echo "" 1>&2 ; \return 1 ; fi
bootLoop=$( @{native.util-linux}/bin/losetup --show -f /dev/disk/by-partlabel/$label ) || return ; prepend_trap "@{native.util-linux}/bin/losetup -d $bootLoop" EXIT
ln -sfT ${bootLoop/\/dev/..\/..} /dev/disk/by-partlabel/$label || return
done

View File

@ -3,7 +3,28 @@
# NixOS Installation
##
## Entry point to the installation, see »./README.md«.
declare-command install-system diskPaths << 'EOD'
This command installs a NixOS system to local disks or image files.
It gets all the information it needs from the system's NixOS configuration -- except for the path(s) of the target disk(s) / image file(s).
If »diskPaths« points to something in »/dev/«, then it is directly formatted and written to as block device, otherwise »diskPaths« is (re-)created as raw image and then used as loop device.
For hosts that install to multiple disks, pass a :-separated list of »<disk-name>=<path>« pairs (the name may be omitted only for the "default" disk).
Since the installation needs to format and mount (image files as) disks, it needs some way of elevating permissions. It can:
* be run as »root«, requiring Nix to be installed system-wide / for root,
* be run with the »sudo« argument (see »--help« output; this runs »nix« commands as the original user, and the rest as root),
* or automatically perform the installation in a qemu VM (see »--vm« flag).
Installing inside the VM is safer (will definitely only write wi the supplied »diskPaths«), more secure (executes the VM), and does not require privilege elevation, but does currently only work for the same ISA, is significantly slower (painfully slow without KVM), and may break custom »*Commands« hooks (esp. those passing in secrets).
Without VM, installations across different ISAs (e.g. from an x64 desktop to a Raspberry Pi microSD) works if the installing host is NixOS and sets »boot.binfmt.emulatedSystems« for the target systems ISA, or on other Linux with a matching »binfmt_misc« registration with the preload (F) flag.
Once done, the disk(s) can be transferred -- or the image(s) be copied -- to the final system, and should boot there.
If the target host's hardware target allows, a resulting image can also be passed to the »register-vbox« command to create a bootable VirtualBox instance for the current user, or to »run-qemu« to start it in a qemu VM.
What the installation does is defined solely by the target host's NixOS configuration.
The "Installation" section of each host's documentation should contain host specific details, if any.
Various »FLAG«s below affect how the installation is performed (in VM, verbosity, debugging, ...).
EOD
function install-system {( # 1: diskPaths
trap - EXIT # start with empty traps for sub-shell
prepare-installer "$@" || exit
@ -11,6 +32,13 @@ function install-system {( # 1: diskPaths
install-system-to $mnt || exit
)}
declare-flag install-system vm "" "Perform the system installation in a qemu VM instead of on the host itself. This is implied when not running as »root« (or with the »sudo« option).
The VM boots the target system's kernel (or a slight modification of it, if the system kernel is not bootable in qemu) and performs the installation at the end of the first boot stage (instead of mounting the root filesystem and starting systemd).
The target disks or images are passed into the VM as block devices (and are the only devices available there). The host's »/nix/« folder is passed as a read-only network share. This makes the installation safe and secure, but also slower (network share), and may cause problems with custom install commands.
The calling user should have access to KVM, or the installation will be very very slow.
See also the »--no-vm« and »--vm-shared=« flags."
declare-flag install-system no-vm "" "Never perform the installation in a VM. Fail if not executed as »root«."
## Does some argument validation, performs some sanity checks, includes a hack to make installation work when nix isn't installed for root, and runs the installation in qemu (if requested).
function prepare-installer { # 1: diskPaths
@ -30,7 +58,7 @@ function prepare-installer { # 1: diskPaths
local luksName ; for luksName in "@{!config.boot.initrd.luks.devices!catAttrSets.device[@]}" ; do
if [[ -e "/dev/mapper/$luksName" ]] ; then echo "LUKS device mapping »$luksName« is already open. Close it before running the installer." 1>&2 ; \return 1 ; fi
done
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
local poolName ; for poolName in "@{!config.setup.zfs.pools[@]}" ; do
if @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then echo "ZFS pool »$poolName« is already imported. Export the pool before running the installer." 1>&2 ; \return 1 ; fi
done
@ -44,6 +72,8 @@ function prepare-installer { # 1: diskPaths
}
declare-flag install-system vm-shared "" "When installing inside the VM, specifies a host path that is read-write mounted at »/tmp/shared« inside the VM."
## Re-executes the current system's installation in a qemu VM.
function reexec-in-qemu {
@ -51,7 +81,7 @@ function reexec-in-qemu {
# (not sure whether this works for block devices)
ensure-disks "$1" 1 || return
qemu=( -m 2048 ) ; declare -A qemuDevs=( )
qemu=( -m 3072 ) ; declare -A qemuDevs=( )
local index=2 ; local name ; for name in "${!blockDevs[@]}" ; do
#if [[ ${blockDevs[$name]} != /dev/* ]] ; then
qemu+=( # not sure how correct the interpretations of the command are
@ -69,10 +99,10 @@ function reexec-in-qemu {
devSpec= ; for name in "${!qemuDevs[@]}" ; do devSpec+="$name"="${qemuDevs[$name]}": ; done
newArgs+=( ${devSpec%:} ) ; shift ; (( $# == 0 )) || args+=( "$@" ) # (( ${#argv[@]} > 1 )) && args+=( "${argv[@]:1}" )
#local output=@{inputs.self}'#'nixosConfigurations.@{outputName:?}.config.system.build.vmExec
#local output=@{inputs.self}'#'nixosConfigurations.@{config.installer.outputName:?}.config.system.build.vmExec
local output=@{config.system.build.vmExec.drvPath!unsafeDiscardStringContext} # this is more accurate, but also means another system needs to get evaluated every time
local scripts=$0 ; if [[ @{pkgs.system} != "@{native.system}" ]] ; then
scripts=$( build-lazy @{inputs.self}'#'apps.@{pkgs.system}.@{outputName:?}.derivation ) || return
scripts=$( build-lazy @{inputs.self}'#'apps.@{pkgs.system}.@{config.installer.outputName:?}.derivation ) || return
fi
local command="$scripts install-system $( printf '%q ' "${newArgs[@]}" ) || exit"
@ -93,11 +123,15 @@ function nixos-install-cmd {( # 1: mnt, 2: topLevel
LC_ALL=C PATH=$PATH:@{native.util-linux}/bin @{native.nixos-install-tools}/bin/nixos-enter --silent --root "$1" -c "${_set_x:-:} ; @{config.system.build.installBootLoader} $2" || exit
)}
declare-flag install-system toplevel "" "Optional replacement for the actual »config.system.build.toplevel«."
declare-flag install-system no-inspect "" "Do not inspect the (successfully) installed system before unmounting its filesystems."
declare-flag install-system inspect-cmd "script" "Instead of opening an interactive shell for the post-installation inspection, »eval« this script."
## Copies the system's dependencies to the disks mounted at »$mnt« and installs the bootloader. If »$inspect« is set, a root shell will be opened in »$mnt« afterwards.
# »$topLevel« may point to an alternative top-level dependency to install.
function install-system-to {( set -u # 1: mnt
mnt=$1 ; topLevel=${2:-}
function install-system-to {( set -u # 1: mnt, 2?: topLevel
targetSystem=${args[toplevel]:-@{config.system.build.toplevel}}
mnt=$1 ; topLevel=${2:-$targetSystem}
beLoud=/dev/null ; if [[ ${args[debug]:-} ]] ; then beLoud=/dev/stdout ; fi
beSilent=/dev/stderr ; if [[ ${args[quiet]:-} ]] ; then beSilent=/dev/null ; fi
trap - EXIT # start with empty traps for sub-shell
@ -120,14 +154,14 @@ function install-system-to {( set -u # 1: mnt
fi
# Support cross architecture installation (not sure if this is actually required)
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{config.wip.preface.hardware}"-linux ]] ; then
mkdir -p $mnt/run/binfmt || exit ; [[ ! -e /run/binfmt/"@{config.wip.preface.hardware}"-linux ]] || cp -a {,$mnt}/run/binfmt/"@{config.wip.preface.hardware}"-linux || exit # On NixOS, this is a symlink or wrapper script, pointing to the store.
if [[ $(cat /run/current-system/system 2>/dev/null || echo "x86_64-linux") != "@{pkgs.system}" ]] ; then
mkdir -p $mnt/run/binfmt || exit ; [[ ! -e /run/binfmt/"@{pkgs.system}" ]] || cp -a {,$mnt}/run/binfmt/"@{pkgs.system}" || exit # On NixOS, this is a symlink or wrapper script, pointing to the store.
# Ubuntu (20.04, by default) uses a statically linked, already loaded qemu binary (F-flag), which therefore does not need to be reference-able from within the chroot.
fi
# Copy system closure to new nix store:
if [[ ${SUDO_USER:-} ]] ; then chown -R $SUDO_USER: $mnt/nix/store $mnt/nix/var || exit ; fi
cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt ${topLevel:-$targetSystem} )
cmd=( nix --extra-experimental-features nix-command --offline copy --no-check-sigs --to $mnt "$topLevel" )
if [[ ${args[quiet]:-} ]] ; then
"${cmd[@]}" --quiet >/dev/null 2> >( grep -Pe '^error:' || true ) || exit
elif [[ ${args[quiet]:-} ]] ; then
@ -148,9 +182,9 @@ function install-system-to {( set -u # 1: mnt
# Run the main install command (primarily for the bootloader):
@{native.util-linux}/bin/mount -o bind,ro /nix/store $mnt/nix/store || exit ; prepend_trap '! @{native.util-linux}/bin/mountpoint -q $mnt/nix/store || @{native.util-linux}/bin/umount -l $mnt/nix/store' EXIT || exit # all the things required to _run_ the system are copied, but (may) need some more things to initially install it and/or enter the chroot (like qemu, see above)
run-hook-script 'Pre Installation' @{config.wip.fs.disks.preInstallCommands!writeText.preInstallCommands} || exit
code=0 ; nixos-install-cmd $mnt "${topLevel:-$targetSystem}" >$beLoud 2>$beSilent || code=$?
run-hook-script 'Post Installation' @{config.wip.fs.disks.postInstallCommands!writeText.postInstallCommands} || exit
run-hook-script 'Pre Installation' @{config.installer.commands.preInstall!writeText.preInstallCommands} || exit
code=0 ; nixos-install-cmd $mnt "$topLevel" >$beLoud 2>$beSilent || code=$?
run-hook-script 'Post Installation' @{config.installer.commands.postInstall!writeText.postInstallCommands} || exit
# Done!
if [[ ${args[no-inspect]:-} ]] ; then

View File

@ -12,7 +12,7 @@ function prompt-for-user-passwords { # (void)
}
## Mounts a ramfs as the host's keystore and populates it with keys as requested by »config.wip.fs.keystore.keys«.
## Mounts a ramfs as the host's keystore and populates it with keys as requested by »config.setup.keystore.keys«.
# Depending on the specified key types/sources, this may prompt for user input.
function populate-keystore { # (void)
local keystore=/run/keystore-@{config.networking.hostName!hashString.sha256:0:8}
@ -21,9 +21,9 @@ function populate-keystore { # (void)
@{native.util-linux}/bin/mount ramfs -t ramfs $keystore && prepend_trap "@{native.util-linux}/bin/umount $keystore" EXIT || return
local -A methods=( ) ; local -A options=( )
local usage ; for usage in "@{!config.wip.fs.keystore.keys[@]}" ; do
methods[$usage]=@{config.wip.fs.keystore.keys[$usage]%%=*}
options[$usage]=@{config.wip.fs.keystore.keys[$usage]:$(( ${#methods[$usage]} + 1 ))}
local usage ; for usage in "@{!config.setup.keystore.keys[@]}" ; do
methods[$usage]=@{config.setup.keystore.keys[$usage]%%=*}
options[$usage]=@{config.setup.keystore.keys[$usage]:$(( ${#methods[$usage]} + 1 ))}
done
local usage ; for usage in "${!methods[@]}" ; do

View File

@ -3,8 +3,10 @@
# NixOS Maintenance
##
## On the host and for the user it is called by, creates/registers a VirtualBox VM meant to run the shells target host. Requires the path to the target host's »diskImage« as the result of running the install script. The image file may not be deleted or moved. If »bridgeTo« is set (to a host interface name, e.g. as »eth0«), it is added as bridged network "Adapter 2" (which some hosts need).
function register-vbox {( # 1: diskImages, 2?: bridgeTo
declare-command register-vbox diskImages bridgeTo? << 'EOD'
On the host and for the user it is called by, creates/registers a VirtualBox VM meant to run the shells target host. Requires the path to the target host's »diskImage« as the result of running the install script. The image file may not be deleted or moved. If »bridgeTo« is set (to a host interface name, e.g. as »eth0«), it is added as bridged network "Adapter 2" (which some hosts need).
EOD
function register-vbox {(
diskImages=$1 ; bridgeTo=${2:-}
vmName="nixos-@{config.networking.hostName}"
VBoxManage=$( PATH=$hostPath which VBoxManage ) || exit # The host is supposed to run these anyway, and »pkgs.virtualbox« is marked broken on »aarch64«.
@ -45,10 +47,27 @@ function register-vbox {( # 1: diskImages, 2?: bridgeTo
echo " ssh $(@{native.inetutils}/bin/hostname) VBoxManage controlvm $vmName screenshotpng /dev/stdout | display"
)}
## Runs a host in QEMU, taking the same disk specification as the installer. It infers a number of options from he target system's configuration.
# Currently, this only works for x64 (on x64) ...
function run-qemu { # 1: diskImages, ...: qemuArgs
if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{outputName:-@{config.system.name}}/ ; fi
declare-command run-qemu diskImages qemuArgs... << 'EOD'
Runs a host in a QEMU VM, directly from its bootable disks, without requiring any change in it's configuration.
This function infers many qemu options from the target system's configuration and the current host system.
»diskImages« may be passed in the same format as to the installer. Any image files passed are ensured to be loop-mounted. »root« may also pass device paths.
EOD
declare-flag run-qemu dry-run "" "Instead of running the (main) qemu (and install) command, only print it."
declare-flag run-qemu efi "" "Treat the target system as EFI system, even if not recognized as such automatically."
declare-flag run-qemu efi-vars "path" "For »--efi« systems, path to a file storing the EFI variables. The default is in »XDG_RUNTIME_DIR«, i.e. it does not persist across host reboots."
declare-flag run-qemu graphic "" "Open a graphical window even of the target system logs to serial and not (explicitly) TTY1."
declare-flag run-qemu install "[always]" "If any of the guest system's disk images does not exist, perform the its installation before starting the VM. If set to »always«, always install before starting the VM. With this flag set, »diskImages« defaults to paths in »/tmp/."
declare-flag run-qemu mem "num" "VM RAM in MiB (»qemu -m«)."
declare-flag run-qemu nat-fw "forwards" "Port forwards to the guest's NATed NIC. E.g: »--nat-fw=:8000-:8000,:8001-:8001,127.0.0.1:2022-:22«."
declare-flag run-qemu no-kvm "" "Do not rey to use (or complain about the unavailability of) KVM."
declare-flag run-qemu no-nat "" "Do not provide a NATed NIC to the guest."
declare-flag run-qemu no-serial "" "Do not connect the calling terminal to a serial adapter the guest can log to and open a terminal on the guests serial, as would be the default if the guests logs to ttyS0."
declare-flag run-qemu share "decls" "Host dirs to make available as network shares for the guest, as space separated list of »name:host-path,options. E.g. »--share='foo:/home/user/foo,readonly=on bar:/tmp/bar«. In the VM hte share can be mounted with: »$ mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 -o ro foo /foo«."
declare-flag run-qemu smp "num" "Number of guest CPU cores."
declare-flag run-qemu usb-port "path" "A physical USB port (or hub) to pass to the guest (e.g. a YubiKey for unlocking). Specified as »<bus>-<port>«, where bus and port refer to the physical USB port »/sys/bus/usb/devices/<bus>-<port>« (see »lsusb -tvv«). E.g.: »--usb-port=3-1.1.1.4«."
declare-flag run-qemu virtio-blk "" "Pass the system's disks/images as virtio disks, instead of using AHCI+IDE. Default iff »boot.initrd.availableKernelModules« includes »virtio_blk« (because it requires that driver)."
function run-qemu {
if [[ ${args[install]:-} && ! ${argv[0]:-} ]] ; then argv[0]=/tmp/nixos-vm/@{config.installer.outputName:-@{config.system.name}}/ ; fi
diskImages=${argv[0]:?} ; argv=( "${argv[@]:1}" )
local qemu=( )
@ -66,7 +85,7 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
qemu+=( -machine accel=tcg ) # this may suppress warnings that qemu is using tcg (slow) instead of kvm
fi
else
qemu=( $( build-lazy @{native.qemu_full.drvPath!unsafeDiscardStringContext} )/bin/qemu-system-@{config.wip.preface.hardware} ) || return
qemu=( $( build-lazy @{native.qemu_full.drvPath!unsafeDiscardStringContext} )/bin/qemu-system-@{pkgs.system%%-linux} ) || return
fi
if [[ @{pkgs.system} == aarch64-* ]] ; then
qemu+=( -machine type=virt ) # aarch64 has no default, but this seems good
@ -80,17 +99,17 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
#qemu+=( -bios ${ovmf}/FV/OVMF.fd ) # This works, but is a legacy fallback that stores the EFI vars in /NvVars on the EFI partition (which is really bad).
local fwName=OVMF ; if [[ @{pkgs.system} == aarch64-* ]] ; then fwName=AAVMF ; fi # fwName=QEMU
qemu+=( -drive file=${ovmf}/FV/${fwName}_CODE.fd,if=pflash,format=raw,unit=0,readonly=on )
local efiVars=${args[efi-vars]:-"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qemu-@{outputName:-@{config.system.name}}-VARS.fd"}
local efiVars=${args[efi-vars]:-"${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qemu-@{config.installer.outputName:-@{config.system.name}}-VARS.fd"}
qemu+=( -drive file="$efiVars",if=pflash,format=raw,unit=1 )
if [[ ! -e "$efiVars" ]] ; then mkdir -pm700 "$( dirname "$efiVars" )" ; cat ${ovmf}/FV/${fwName}_VARS.fd >"$efiVars" || return ; fi
# https://lists.gnu.org/archive/html/qemu-discuss/2018-04/msg00045.html
fi
# if [[ @{config.wip.preface.hardware} == aarch64 ]] ; then
# if [[ @{pkgs.system} == aarch64-* ]] ; then
# qemu+=( -kernel @{config.system.build.kernel}/Image -initrd @{config.system.build.initialRamdisk}/initrd -append "$(echo -n "@{config.boot.kernelParams[@]}")" )
# fi
if [[ $diskImages == */ ]] ; then
disks=( ${diskImages}primary.img ) ; for name in "@{!config.wip.fs.disks.devices[@]}" ; do if [[ $name != primary ]] ; then disks+=( ${diskImages}${name}.img ) ; fi ; done
disks=( ${diskImages}primary.img ) ; for name in "@{!config.setup.disks.devices[@]}" ; do if [[ $name != primary ]] ; then disks+=( ${diskImages}${name}.img ) ; fi ; done
else disks=( ${diskImages//:/ } ) ; fi
[[ ' '"@{boot.initrd.availableKernelModules[@]}"' ' != *' 'virtio_blk' '* ]] || args[virtio-blk]=1
@ -155,9 +174,11 @@ function run-qemu { # 1: diskImages, ...: qemuArgs
# https://askubuntu.com/questions/54814/how-can-i-ctrl-alt-f-to-get-to-a-tty-in-a-qemu-session
}
## Creates a random static key on a new key partition on the GPT partitioned »$blockDev«. The drive can then be used as headless but removable disk unlock method.
# To create/clear the GPT: $ sgdisk --zap-all "$blockDev"
function add-bootkey-to-keydev { # 1: blockDev, 2?: hostHash
declare-command add-bootkey-to-keydev blockDev << 'EOD'
Creates a random static key on a new key partition on the GPT partitioned »$blockDev«. The drive can then be used as headless but removable disk unlock method.
To create/clear the GPT beforehand, run: $ sgdisk --zap-all "$blockDev"
EOD
function add-bootkey-to-keydev {
local blockDev=$1 ; local hostHash=${2:-@{config.networking.hostName!hashString.sha256}}
local bootkeyPartlabel=bootkey-${hostHash:0:8}
@{native.gptfdisk}/bin/sgdisk --new=0:0:+1 --change-name=0:"$bootkeyPartlabel" --typecode=0:0000 "$blockDev" || exit # create new 1 sector (512b) partition
@ -165,26 +186,29 @@ function add-bootkey-to-keydev { # 1: blockDev, 2?: hostHash
</dev/urandom tr -dc 0-9a-f | head -c 512 >/dev/disk/by-partlabel/"$bootkeyPartlabel" || exit
}
## Tries to open and mount the systems keystore from its LUKS partition. If successful, adds the traps to close it when the parent shell exits.
# For the exit traps to trigger on exit from the calling script / shell, this can't run in a sub shell (and therefore can't be called from a pipeline).
# See »open-system«'s implementation for some example calls to this function.
function mount-keystore-luks { # ...: cryptsetupOptions
declare-command mount-keystore-luks cryptsetupOptions... << 'EOD'
Tries to open and mount the systems keystore from its LUKS partition. If successful, this also adds the traps to close the keystore when the parent shell exits (so this is not useful as a standalone »COMMAND«, use the »bash« or »--command« options).
For the exit traps to trigger on exit from the calling script / shell, this can't run in a sub shell (and therefore can't be called from a pipeline).
See »open-system«'s implementation for some example calls to this function.
EOD
function mount-keystore-luks {
local keystore=keystore-@{config.networking.hostName!hashString.sha256:0:8}
mkdir -p -- /run/$keystore && prepend_trap "[[ ! -e /run/$keystore ]] || rmdir /run/$keystore" EXIT || return
@{native.cryptsetup}/bin/cryptsetup open "$@" /dev/disk/by-partlabel/$keystore $keystore && prepend_trap "@{native.cryptsetup}/bin/cryptsetup close $keystore" EXIT || return
@{native.util-linux}/bin/mount -o nodev,umask=0077,fmask=0077,dmask=0077,ro /dev/mapper/$keystore /run/$keystore && prepend_trap "@{native.util-linux}/bin/umount /run/$keystore" EXIT || return
}
## Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
# For any steps taken, it also adds the steps to undo them on exit from the calling shell (so don't call this from a sub-shell that exits too early).
# »diskImages« may be passed in the same format as to the installer. If so, any image files are ensured to be loop-mounted.
# Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
# $ source ${config_wip_fs_disks_initSystemCommands1writeText_initSystemCommands}
# $ source ${config_wip_fs_disks_restoreSystemCommands1writeText_restoreSystemCommands}
# $ install-system-to $mnt
# $ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt
# $ nixos-enter --root $mnt
function open-system { # 1?: diskImages
declare-command open-system diskImages << 'EOD'
Performs any steps necessary to mount the target system at »/tmp/nixos-install-@{config.networking.hostName}« on the current host.
For any steps taken, it also adds the steps to undo them on exit from the calling shell (so this is not useful as a standalone »COMMAND«, use the »bash« or »--command« options, and don't call this from a sub-shell that exits too early).
»diskImages« may be passed in the same format as to the installer. Any image files passed are ensured to be loop-mounted. »root« may also pass device paths.
Perfect to inspect/update/amend/repair a system's installation afterwards, e.g.:
$ install-system-to $mnt
$ nixos-install --system ${config_system_build_toplevel} --no-root-passwd --no-channel-copy --root $mnt
$ nixos-enter --root $mnt
EOD
function open-system {
local diskImages=${1:-} # If »diskImages« were specified and they point at files that aren't loop-mounted yet, then loop-mount them now:
local images=$( @{native.util-linux}/bin/losetup --list --all --raw --noheadings --output BACK-FILE )
local decl ; for decl in ${diskImages//:/ } ; do
@ -195,7 +219,7 @@ function open-system { # 1?: diskImages
done
@{native.systemd}/bin/udevadm settle -t 15 || true # sometimes partitions aren't quite made available yet
if [[ @{config.wip.fs.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
if [[ @{config.setup.keystore.enable} && ! -e /dev/mapper/keystore-@{config.networking.hostName!hashString.sha256:0:8} ]] ; then # Try a bunch of approaches for opening the keystore:
mount-keystore-luks --key-file=<( printf %s "@{config.networking.hostName}" ) || return
mount-keystore-luks --key-file=/dev/disk/by-partlabel/bootkey-@{config.networking.hostName!hashString.sha256:0:8} || return
mount-keystore-luks --key-file=<( read -s -p PIN: pin && echo ' touch!' >&2 && @{native.yubikey-personalization}/bin/ykchalresp -2 "$pin" ) || return
@ -208,8 +232,8 @@ function open-system { # 1?: diskImages
open-luks-layers || return # Load crypt layers and zfs pools:
if [[ $( LC_ALL=C type -t ensure-datasets ) == 'function' ]] ; then
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if [[ ! @{config.wip.fs.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
local poolName ; for poolName in "@{!config.setup.zfs.pools[@]}" ; do
if [[ ! @{config.setup.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
if ! @{native.zfs}/bin/zfs get -o value -H name "$poolName" &>/dev/null ; then
@{native.zfs}/bin/zpool import -f -N -R "$mnt" "$poolName" && prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
fi

View File

@ -24,12 +24,12 @@ function generic-arg-parse { # ...
# Uses »allowedArgs« for the list of the named arguments (the values are the descriptions).
# »name« should be the program name/path (usually »$0«), »args« the form/names of any positional arguments expected (e.g. »SOURCE... DEST«) and is included in the "Usage" description,
# »description« the introductory text shown before the "Usage", and »suffix« any text printed after the argument list.
function generic-arg-help { # 1: name, 2?: args, 3?: description, 4?: suffix
function generic-arg-help { # 1: name, 2?: args, 3?: description, 4?: suffix, 5?: usageLine
if [[ ! ${args[help]:-} ]] ; then : ${allowedArgs[help]:=1} ; \return 0 ; fi
[[ ! ${3:-} ]] || echo "$3"
printf 'Usage:\n %s [ARG[=value]]... [--] %s\n\nWhere »ARG« may be any of:\n' "$1" "${2:-}"
printf "${5:-'Usage:\n %s [FLAG[=value]]... [--] %s\n\nWhere »FLAG« may be any of:\n'}" "$1" "${2:-}"
local name ; while IFS= read -u3 -r name ; do
printf ' %s\n %s\n' "$name" "${allowedArgs[$name]}"
printf ' %s\n %s\n' "$name" "${allowedArgs[$name]//$'\n'/$'\n '}"
done 3< <( printf '%s\n' "${!allowedArgs[@]}" | LC_ALL=C sort )
printf ' %s\n %s\n' "--help" "Do nothing but print this message and exit with success."
[[ ! ${4:-} ]] || echo "$4"
@ -92,6 +92,8 @@ function prompt-new-password {( set -u # 1: usage
printf %s "$password1" || exit
)}
declare-flag install-system inspectScripts "" "When running installation hooks (»...*Commands« composed as Nix strings) print out and pause before each command. This works ... semi-well."
## Runs an installer hook script, optionally stepping through the script.
function run-hook-script {( # 1: title, 2: scriptPath
trap - EXIT # start with empty traps for sub-shell

View File

@ -1,19 +1,23 @@
## Creates all of the system's ZFS pools that are »createDuringInstallation«, plus their datasets.
function create-zpools { # 1: mnt
local poolName ; for poolName in "@{!config.wip.fs.zfs.pools[@]}" ; do
if [[ ! @{config.wip.fs.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
local poolName ; for poolName in "@{!config.setup.zfs.pools[@]}" ; do
if [[ ! @{config.setup.zfs.pools!catAttrSets.createDuringInstallation[$poolName]} ]] ; then continue ; fi
create-zpool "$1" "$poolName"
done
}
## Creates a single of the system's ZFS pools and its datasets.
function create-zpool { # 1: mnt, 2: poolName
declare-command create-zpool mnt poolName << 'EOD'
Creates a single of the system's ZFS pools, and its datasets. Can be called manually to create pools that were added to the configuration, or to create those declared with »createDuringInstallation = false«. Expects the backing device(-partition)s to exist as declared for the pool.
EOD
declare-flag install-system zpool-force "" "(create-zpool) When creating ZFS storage pools, pass the »-f« (force) option. This may be required when installing to disks that are currently part of a pool, or ZFS refuses do reuse them."
function create-zpool {
local mnt=$1 ; local poolName=$2
eval 'local -A pool='"@{config.wip.fs.zfs.pools[$poolName]}"
eval 'local -A pool='"@{config.setup.zfs.pools[$poolName]}"
eval 'local -a vdevs='"${pool[vdevArgs]}"
eval 'local -A poolProps='"${pool[props]}"
eval 'local -A dataset='"@{config.wip.fs.zfs.datasets[${pool[name]}]}"
eval 'local -A dataset='"@{config.setup.zfs.datasets[${pool[name]}]}"
eval 'local -A dataProps='"${dataset[props]}"
local dummy ; get-zfs-crypt-props "${dataset[name]}" dataProps dummy dummy
local -a zpoolCreate=( ) ; keySrc=/dev/null
@ -33,18 +37,20 @@ function create-zpool { # 1: mnt, 2: poolName
done
@{native.kmod}/bin/modprobe zfs || true
<$keySrc @{native.xxd}/bin/xxd -l 32 -c 64 -p | ( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zpool create ${args[zpool-force]:+-f} "${zpoolCreate[@]}" -R "$mnt" "${pool[name]}" "${vdevs[@]}" ) || return
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
if [[ $keySrc == /dev/urandom ]] ; then @{native.zfs}/bin/zfs unload-key "$poolName" &>/dev/null ; fi
prepend_trap "@{native.zfs}/bin/zpool export '$poolName'" EXIT || return
ensure-datasets $mnt '^'"$poolName"'($|[/])' || return
if [[ ${args[debug]:-} ]] ; then @{native.zfs}/bin/zfs list -o name,canmount,mounted,mountpoint,keystatus,encryptionroot -r "$poolName" ; fi
}
## Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined).
# The pool(s) must exist, be imported with root prefix »$mnt«, and (if datasets are to be created or encryption roots to be inherited) the system's keystore must be open (see »mount-keystore-luks«) or the keys be loaded.
# »keystatus« and »mounted« of existing datasets should remain unchanged, newly crated datasets will not be mounted but have their keys loaded.
function ensure-datasets { # 1: mnt, 2?: filterExp
if (( @{#config.wip.fs.zfs.datasets[@]} == 0 )) ; then \return ; fi
declare-command ensure-datasets mnt filterExp? << 'EOD'
Ensures that the system's datasets exist and have the defined properties (but not that they don't have properties that aren't defined).
The pool(s) must exist, be imported with root prefix »$mnt«, and (if datasets are to be created or encryption roots to be inherited) the system's keystore must be open (see »mount-keystore-luks«) or the keys must be loaded.
»keystatus« and »mounted« of existing datasets should remain unchanged by this function, newly crated datasets will not be mounted but have their keys loaded.
EOD
function ensure-datasets {
if (( @{#config.setup.zfs.datasets[@]} == 0 )) ; then \return ; fi
local mnt=$1 ; while [[ "$mnt" == */ ]] ; do mnt=${mnt:0:(-1)} ; done # (remove any tailing slashes)
local filterExp=${2:-'^'}
local tmpMnt=$(mktemp -d) ; trap "rmdir $tmpMnt" EXIT
@ -53,7 +59,7 @@ function ensure-datasets { # 1: mnt, 2?: filterExp
local name ; while IFS= read -u3 -r -d $'\0' name ; do
if [[ ! $name =~ $filterExp ]] ; then printf 'Skipping dataset »%s« since it does not match »%s«\n' "$name" "$filterExp" >&2 ; continue ; fi
eval 'local -A dataset='"@{config.wip.fs.zfs.datasets[$name]}"
eval 'local -A dataset='"@{config.setup.zfs.datasets[$name]}"
eval 'local -A props='"${dataset[props]}"
local explicitKeylocation=${props[keylocation]:-} cryptKey cryptRoot
@ -127,10 +133,10 @@ function ensure-datasets { # 1: mnt, 2?: filterExp
# »zfs allow $dataset« seems to be the only way to view permissions, and that is not very parsable -.-
( PATH=@{native.zfs}/bin ; ${_set_x:-:} ; zfs allow -$who "${allows[$who]}" "${dataset[name]}" >&2 ) || return
done
done 3< <( printf '%s\0' "@{!config.wip.fs.zfs.datasets[@]}" | LC_ALL=C sort -z )
done 3< <( printf '%s\0' "@{!config.setup.zfs.datasets[@]}" | LC_ALL=C sort -z )
}
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.wip.fs.keystore.keys."zfs/..."«).
## Given the name (»datasetPath«) of a ZFS dataset, this deducts crypto-related options from the declared keys (»config.setup.keystore.keys."zfs/..."«).
function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_cryptKey, 4?: name_cryptRoot
local hash=@{config.networking.hostName!hashString.sha256:0:8}
local keystore=/run/keystore-$hash
@ -141,8 +147,8 @@ function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_c
} ; local key=${pool/-$hash'/'/}$path # strip hash from pool name
__cryptKey='' ; __cryptRoot=''
if [[ @{config.wip.fs.keystore.keys[zfs/$name]:-} ]] ; then
if [[ @{config.wip.fs.keystore.keys[zfs/$name]} == unencrypted ]] ; then
if [[ @{config.setup.keystore.keys[zfs/$name]:-} ]] ; then
if [[ @{config.setup.keystore.keys[zfs/$name]} == unencrypted ]] ; then
__cryptProps[encryption]=off # empty key to disable encryption
else
__cryptProps[encryption]=aes-256-gcm ; __cryptProps[keyformat]=hex ; __cryptProps[keylocation]=file://"$keystore"/zfs/"$name".key
@ -151,8 +157,8 @@ function get-zfs-crypt-props { # 1: datasetPath, 2?: name_cryptProps, 3?: name_c
else
while true ; do
name=$(dirname $name) ; if [[ $name == . ]] ; then break ; fi
if [[ @{config.wip.fs.keystore.keys[zfs/$name]:-} ]] ; then
if [[ @{config.wip.fs.keystore.keys[zfs/$name]} != unencrypted ]] ; then
if [[ @{config.setup.keystore.keys[zfs/$name]:-} ]] ; then
if [[ @{config.setup.keystore.keys[zfs/$name]} != unencrypted ]] ; then
__cryptKey=$keystore/zfs/$name.key ; __cryptRoot=$name
fi ; break
fi

View File

@ -1,140 +0,0 @@
dirname: { self, nixpkgs, ...}: let
inherit (nixpkgs) lib;
in rec {
## Data Structures
## Given a function mapping a name to its value and a list of names, generate that mapping as attribute set. (This is the same as »lib.attrsets.genAttrs« with swapped arguments.)
namesToAttrs = toValue: names: builtins.listToAttrs (map (name: { inherit name; value = toValue name; }) names);
# Given a function and a list, calls the function for each list element, and returns the merge of all attr sets returned by the function
# attrs = mapMerge (value: { "${newKey}" = newValue; }) list
# attrs = mapMerge (key: value: { "${newKey}" = newValue; }) attrs
mapMerge = toAttr: listOrAttrs: mergeAttrs (if builtins.isAttrs listOrAttrs then lib.mapAttrsToList toAttr listOrAttrs else map toAttr listOrAttrs);
mapMergeUnique = toAttr: listOrAttrs: mergeAttrsUnique (if builtins.isAttrs listOrAttrs then lib.mapAttrsToList toAttr listOrAttrs else map toAttr listOrAttrs);
# Given a list of attribute sets, returns the merged set of all contained attributes, with those in elements with higher indices taking precedence.
mergeAttrs = attrsList: builtins.foldl' (a: b: a // b) { } attrsList;
# Given a list of attribute sets, returns the merged set of all contained attributes. Throws if the same attribute name occurs more than once.
mergeAttrsUnique = attrsList: let
merged = mergeAttrs attrsList;
names = builtins.concatLists (map builtins.attrNames attrsList);
duplicates = builtins.filter (a: (lib.count (b: a == b) names) >= 2) (builtins.attrNames merged);
in (
if (builtins.length (builtins.attrNames merged)) == (builtins.length names) then merged
else throw "Duplicate key(s) in attribute merge set: ${builtins.concatStringsSep ", " duplicates}"
);
mergeAttrsRecursive = attrsList: let # slightly adjusted from https://stackoverflow.com/a/54505212
merge = attrPath: lib.zipAttrsWith (name: values:
if builtins.length values == 1
then builtins.head values
else if builtins.all builtins.isList values
then lib.unique (builtins.concatLists values)
else if builtins.all builtins.isAttrs values
then merge (attrPath ++ [ name ]) values
else builtins.elemAt values (builtins.length values - 1)
);
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 [ ];
repeat = count: element: builtins.genList (i: element) count;
# Given an attribute set of attribute sets (»{ ${l1name}.${l2name} = value; }«), flips the positions of the first and second name level, producing »{ ${l3name}.${l1name} = value; }«. The set of »l2name«s does not need to be the same for each »l1name«.
flipNames = attrs: let
l1names = builtins.attrNames attrs;
l2names = builtins.concatMap builtins.attrNames (builtins.attrValues attrs);
in namesToAttrs (l2name: (
mapMerge (l1name: if attrs.${l1name}?${l2name} then { ${l1name} = attrs.${l1name}.${l2name}; } else { }) l1names
)) l2names;
# Like »builtins.catAttrs«, just for attribute sets instead of lists: Given an attribute set of attribute sets (»{ ${l1name}.${l2name} = value; }«) and the »name« of a second-level attribute, this returns the attribute set mapping directly from the first level's names to the second-level's values (»{ ${l1name} = value; }«), omitting any first-level attributes that lack the requested second-level attribute.
catAttrSets = name: attrs: (builtins.mapAttrs (_: value: value.${name}) (lib.filterAttrs (_: value: value?${name}) attrs));
## String Manipulation
# Given a regular expression with capture groups and a list of strings, returns the flattened list of all the matched capture groups of the strings matched in their entirety by the regular expression.
mapMatching = exp: strings: (builtins.filter (v: v != null) (builtins.concatLists (builtins.filter (v: v != null) (map (string: (builtins.match exp string)) strings))));
# Given a regular expression and a list of strings, returns the list of all the strings matched in their entirety by the regular expression.
filterMatching = exp: strings: (builtins.filter (matches exp) strings);
filterMismatching = exp: strings: (builtins.filter (string: !(matches exp string)) strings);
matches = exp: string: builtins.match exp string != null;
extractChars = exp: string: let match = (builtins.match "^.*(${exp}).*$" string); in if match == null then null else builtins.head match;
# If »exp« (which mustn't match across »\n«) matches (a part of) exactly one line in »text«, return that »line« including tailing »\n«, plus the text part »before« and »after«, the text »without« the line, and any »captures« made by »exp«. If »text« does not end in a »\n«, then one will be added (since this function operates on lines).
# The »*Anchored« version allows the expression to require to match from the »start« and/or to the »end« of its line, by passing the respective bool(s) as »true«.
extractLineAnchored = exp: start: end: text: let
exp' = "(${if start then "^|\n" else ""})(${if start then "" else "[^\n]*"}(${exp})${if end then "" else "[^\n]*"}\n)"; # First capture group is the optional start anchor, the second one the line itself.
text' = (builtins.unsafeDiscardStringContext (if (lastChar text) == "\n" then text else text + "\n")); # Ensure tailing newline and drop context (since it needs to be added again anyway).
split = builtins.split exp' text';
get = builtins.elemAt split; matches = get 1;
ctxify = str: lib.addContextFrom text str;
in if builtins.length split != 3 then null else rec { # length < 3 => no match ; length < 3 => multiple matches
before = ctxify ((get 0) + (builtins.head matches));
line = ctxify (builtins.elemAt matches 1);
captures = map ctxify (lib.sublist 3 (builtins.length matches) matches);
after = ctxify (get 2);
without = ctxify (before + after);
}; # (The string context stuff is actually required, but why? Shouldn't »builtins.split« propagate the context?)
extractLine = exp: text: extractLineAnchored exp false false text;
# Given a string, returns its first/last char (or last utf-8(?) byte?).
firstChar = string: builtins.substring (0) 1 string;
lastChar = string: builtins.substring (builtins.stringLength string - 1) 1 string;
startsWith = prefix: string: let length = builtins.stringLength prefix; in (builtins.substring (0) (length) string) == prefix;
endsWith = suffix: string: let length = builtins.stringLength suffix; in (builtins.substring (builtins.stringLength string - length) (length) string) == suffix;
removeTailingNewline = string: if lastChar string == "\n" then builtins.substring 0 (builtins.stringLength string - 1) string else string;
## Reproducibly generates a GUID by sha256-hashing a prefixed name. The result looks like a RFC 4122 GUID "generated by [SHA1] hashing a namespace identifier and name".
# E.g.: sha256guid "gpt-disk:primary:${hostname}" => "xxxxxxxx-xxxx-5xxx-8xxx-xxxxxxxxxxxx"
sha256guid = name: let
hash = builtins.hashString "sha256" "nixos-guid:${name}";
s = from: to: builtins.substring from (to - from) hash;
in "${s 0 8}-${s 8 12}-5${s 13 16}-8${s 17 20}-${s 20 32}";
## Generate an entry for »systemd.tmpfiles.rules« with named attributes and proper escaping.
# This behaves according to the man page, but contrary to what the man page says/implies:
# * a single »%« is actually interpreted verbatim, as long as it is not followed by a letter (I guess a "specifier" is a »%« followed by a letter or another »%«),
# * »\xDD« escapes don't work in the »type« field (e.g. error: "Unknown modifiers in command 'x66+'" for "\x66+", which should be interpreted as "f+"),
# * none of the »\« I tried (»\n«, »\t«, »\xDD«) worked in the »path« (it simply removed the »\«); Only »\\« correctly results in »\«.
# => I assume the "Fields may contain C-style escapes" isn't technically incorrect, but the implied "... and they are interpreted as such" actually only applies to the »argument« field. The man page also doesn't actually say what consequence quoting has (I assume it prevents splitting at " ", but anything else?).
mkTmpfile = {
type ? "d", # One of [ "f" "f+" "w" "w+" "d" "D" "e" "v" "q" "Q" "p" "p+" "L" "L+" "c" "c+" "b" "b+" "C" "x" "X" "r" "R" "z" "Z" "t" "T" "h" "H" "a" "a+" "A" "A+" ].
path, pathSubstitute ? false, # String starting with "/" or (if »pathSubstitute == true«) also "%"
mode ? "-", # 4 digit octal works. Can be prefixed with "~" (mask with existing mode) or ":" (keep existing mode).
user ? "-", group ? user,
age ? "-",
argument ? "", argumentSubstitute ? false, # Depends on type.
}: let
# »systemd/src/basic/string-util.h« defines »WHITESPACE = " \t\n\r"«. »toJSON« escapes all of these except for " ", but that only matters if it is the first char of the (unquoted) argument.
esc = s: if s == "-" then s else builtins.toJSON s; noSub = builtins.replaceStrings [ "%" ] [ "%%" ];
argument' = builtins.substring 1 ((builtins.stringLength (esc argument)) - 2) (esc argument);
argument'' = if builtins.substring 0 1 argument' != " " then argument'
else ''\x20${builtins.substring 1 ((builtins.stringLength argument') - 1) 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 = [
(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''
];
*/
## Math
pow = (let pow = b: e: if e == 1 then b else if e == 0 then 1 else b * pow b (e - 1); in pow); # (how is this not an operator or builtin?)
toBinString = int: builtins.concatStringsSep "" (map builtins.toString (lib.toBaseDigits 2 int));
parseSizeSuffix = decl: let
match = builtins.match ''^([0-9]+)(K|M|G|T|P)?(i)?(B)?$'' decl;
num = lib.toInt (builtins.head match); unit = builtins.elemAt match 1;
exponent = if unit == null then 0 else { K = 1; M = 2; G = 3; T = 4; P = 5; }.${unit};
base = if (builtins.elemAt match 3) == null || (builtins.elemAt match 2) != null then 1024 else 1000;
in if builtins.isInt decl then decl else if match != null then num * (pow base exponent) else throw "${decl} is not a number followed by a size suffix";
}

View File

@ -1,357 +0,0 @@
/*
# 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/loginKey.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 ""}
keys=${keysOutPath} ; rm -rf "$keys" && mkdir -p "$keys" && chmod 750 "$keys" || exit
for ketName in hostKey loginKey ; do
${pkgs.openssh}/bin/ssh-keygen -q -N "" -t ed25519 -f "$keys"/$ketName -C $ketName || exit
done
SUDO_USER= ${lib.wip.writeSystemScripts { inherit system pkgs; }} deploy-system-to-hetzner-vps --inspect-cmd='
keys='$( printf %q "$keys" )' ; if [[ ''${args[no-vm]:-} ]] ; then keys=/tmp/shared ; fi # "no-vm" is set inside the VM
mkdir -p $mnt/local/etc/ssh/ || exit
cp -aT "$keys"/loginKey.pub $mnt/local/etc/ssh/loginKey.pub || exit
cp -aT "$keys"/hostKey $mnt/local/etc/ssh/ssh_host_ed25519_key || exit
cp -aT "$keys"/hostKey.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"} ${lib.optionalString ignoreKill "--vps-keep-on-build-failure"} ${lib.optionalString suppressCreateEmail "--vps-suppress-create-email"} "$@" -- ${esc name} ${esc serverType} || exit # --parallel-build-deploy
rm "$keys"/hostKey || exit # don't need this anymore
ip=$( ${hcloud} server ip ${esc name} ) ; echo "$ip" >"$keys"/ip
printf "%s %s\n" "$ip" "$( cat "$keys"/hostKey.pub )" >"$keys"/known_hosts
printf '%s\n' '#!${pkgs.bash}' 'exec ${sshCmd} "$@"' >"$keys"/ssh ; chmod 555 "$keys"/ssh
echo ${remoteStore.urlArg} >"$keys"/store ; echo ${remoteStore.builderArg} >"$keys"/builder
printf '%s\n' '#!${pkgs.bash}' 'exec nix ${lib.concatStringsSep " " remoteStore.builderArgs} "$@"' >"$keys"/remote ; chmod 555 "$keys"/remote
'';
sshCmd = ''${pkgs.openssh}/bin/ssh -oUserKnownHostsFile=${keysOutPath}/known_hosts -i ${keysOutPath}/loginKey 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}'/loginKey&base64-ssh-public-host-key='$( cat ${keysOutPath}/hostKey.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}/loginKey" # 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}/hostKey.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}" ''
${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

@ -25,7 +25,7 @@ TODO: documentation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
dirname: inputs: { config, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.${TODO: name};
in {

View File

@ -1,202 +0,0 @@
/*
# System Defaults
Things that really should be (more like) this by default.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.base;
outputName = specialArgs.outputName or null;
in {
options.${prefix} = { base = {
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 = { }; };
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 = 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; };
}; };
imports = lib.optional ((builtins.substring 0 5 inputs.nixpkgs.lib.version) <= "22.05") (lib.wip.overrideNixpkgsModule "misc/extra-arguments.nix" { } (old: { config._module.args.utils = old._module.args.utils // {
escapeSystemdPath = s: let n = builtins.replaceStrings [ "/" "-" " " ] [ "-" "\\x2d" "\\x20" ] (lib.removePrefix "/" s); in if lib.hasPrefix "." n then "\\x2e" (lib.substring 1 (lib.stringLength (n - 1)) n) else n; # (a better implementation has been merged in 22.11)
}; }));
config = let
in lib.mkIf cfg.enable (lib.mkMerge [ ({
users.mutableUsers = false; users.allowNoPasswordLogin = true; # Don't babysit. Can roll back or redeploy.
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName));
environment.etc."machine-id".text = lib.mkDefault (builtins.substring 0 32 (builtins.hashString "sha256" "${config.networking.hostName}:machine-id")); # this works, but it "should be considered "confidential", and must not be exposed in untrusted environments" (not sure _why_ though)
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
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 ''
ln -sT ${builtins.unsafeDiscardStringContext config.system.build.bootStage1} $out/boot-stage-1.sh # (this is super annoying to locate otherwise)
''); # (to deactivate this, set »system.extraSystemBuilderCmds = lib.mkAfter "rm -f $out/boot-stage-1.sh";«)
system.activationScripts.diff-systems = { text = ''
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 "";«)
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:
boot.kernelParams = [ "panic=10" ] ++ (lib.optional cfg.panic_on_fail "boot.panic_on_fail"); # Reboot on kernel panic (showing the printed messages for 10s), panic if boot fails.
# might additionally want to do this: https://stackoverflow.com/questions/62083796/automatic-reboot-on-systemd-emergency-mode
systemd.extraConfig = "StatusUnitFormat=name"; # Show unit names instead of descriptions during boot.
}) (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:
system.extraSystemBuilderCmds = ''
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):
args: (builtins.getFlake ${builtins.toJSON cfg.includeInputs.self}).nixosConfigurations.${outputName}.pkgs
''} $out/pkgs # (nixpkgs with overlays)
''; # (use this indirection so that all open shells update automatically)
nix.nixPath = [ "nixpkgs=/run/current-system/pkgs" ]; # this intentionally replaces the defaults: nixpkgs is here, /etc/nixos/flake.nix is implicit, channels are impure
}) (lib.mkIf (cfg.includeInputs != { }) { # flake things
# "input" to the system build is definitely also a nix version that works with flakes:
nix.settings.experimental-features = [ "nix-command" "flakes" ]; # apparently, even nix 2.8 (in nixos-22.05) needs this
environment.systemPackages = [ pkgs.git ]; # necessary as external dependency when working with flakes
# »inputs.self« does not have a name (that is known here), so just register it as »/etc/nixos/« system config:
environment.etc.nixos = lib.mkIf (cfg.includeInputs?self) (lib.mkDefault { source = "/run/current-system/config"; }); # (use this indirection to prevent every change in the config to necessarily also change »/etc«)
system.extraSystemBuilderCmds = lib.mkIf (cfg.includeInputs?self) ''
ln -sT ${cfg.includeInputs.self} $out/config # (build input for reference)
'';
# Add all inputs to the flake registry:
nix.registry = lib.mapAttrs (name: input: lib.mkDefault { flake = input; }) (builtins.removeAttrs cfg.includeInputs [ "self" ]);
}) (lib.mkIf (cfg.autoUpgrade) {
nix.gc = { # gc everything older than 30 days, before updating
automatic = lib.mkDefault true; # let's hold back on this for a while
options = lib.mkDefault "--delete-older-than 30d";
dates = lib.mkDefault "Sun *-*-* 03:15:00";
};
nix.settings = { keep-outputs = true; keep-derivations = true; }; # don't GC build-time dependencies
system.autoUpgrade = {
enable = lib.mkDefault true;
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).)
dates = "05:40"; randomizedDelaySec = "30min";
allowReboot = lib.mkDefault false;
};
}) (lib.mkIf (cfg.bashInit) {
# (almost) Free Convenience:
environment.shellAliases = {
"with" = pkgs.writeShellScript "with" ''
help='Synopsys: With the Nix packages »PKGS« (as attribute path read from the imported »nixpkgs« specified on the »NIX_PATH«), run »CMD« with »ARGS«, or »bash --login« if no »CMD« is supplied. In the second form, »CMD« is the same as the last »PKGS« entry.
Usage: with [-h] PKGS... [-- [CMD [ARGS...]]]
with [-h] PKGS... [. [ARGS...]]'
pkgs=( ) ; while (( "$#" > 0 )) ; do {
if [[ $1 == -h ]] ; then echo "$help" ; exit 0 ; fi
if [[ $1 == -- ]] ; then shift ; break ; fi
if [[ $1 == . ]] ; then
shift ; (( ''${#pkgs[@]} == 0 )) || set -- "''${pkgs[-1]}" "$@" ; break
fi
pkgs+=( "$1" )
} ; shift ; done
if (( ''${#pkgs[@]} == 0 )) ; then echo "$help" 1>&2 ; exit 1 ; fi
if (( "$#" == 0 )) ; then set -- bash --login ; fi
nix-shell --run "$( printf ' %q' "$@" )" -p "''${pkgs[@]}"
#function run { bash -xc "$( printf ' %q' "$@" )" ; }
''; # »with« doesn't seem to be a common linux command yet, and it makes sense here: with package(s) => do stuff
ls = "ls --color=auto"; # (default)
l = "ls -alhF"; # (added F)
ll = "ls -alF"; # (added aF)
lt = "tree -a -p -g -u -s -D -F --timefmt '%Y-%m-%d %H:%M:%S'"; # ll like tree
lp = pkgs.writeShellScript "lp" ''abs="$(cd "$(dirname "$1")" ; pwd)"/"$(basename "$1")" ; ${pkgs.util-linux}/bin/namei -lx "$abs"''; # similar to »ll -d« on all path element from »$1« to »/«
ips = "ip -c -br addr"; # colorized listing of all interface's IPs
mounts = pkgs.writeShellScript "mounts" ''${pkgs.util-linux}/bin/mount | ${pkgs.gnugrep}/bin/grep -vPe '/.zfs/snapshot/| on /var/lib/docker/|^/var/lib/snapd/snaps/' | LC_ALL=C ${pkgs.coreutils}/bin/sort -k3 | ${pkgs.util-linux}/bin/column -t -N Device/Source,on,Mountpoint,type,Type,Options -H on,type -W Device/Source,Mountpoint,Options''; # the output of »mount«, cleaned up and formatted as a sorted table
netns-exec = pkgs.writeShellScript "netns-exec" ''ns=$1 ; shift ; /run/wrappers/bin/firejail --noprofile --quiet --netns="$ns" -- "$@"''; # execute a command in a different netns (like »ip netns exec«), without requiring root permissions (but does require »config.programs.firejail.enable=true«)
nixos-list-generations = "nix-env --list-generations --profile /nix/var/nix/profiles/system";
sc = "systemctl";
scs = "systemctl status";
scc = "systemctl cat";
scu = "systemctl start"; # up
scd = "systemctl stop"; # down
scr = "systemctl restart";
scf = "systemctl list-units --failed";
scj = "journalctl -b -f -u";
};
programs.bash.promptInit = ''
# Provide a nice prompt if the terminal supports it.
if [ "''${TERM:-}" != "dumb" ] ; then
if [[ "$UID" == '0' ]] ; then if [[ ! "''${SUDO_USER:-}" ]] ; then # direct root: red username + green hostname
PS1='\[\e[0m\]\[\e[48;5;234m\]\[\e[96m\]$(printf "%-+ 4d" $?)\[\e[93m\][\D{%Y-%m-%d %H:%M:%S}] \[\e[91m\]\u\[\e[97m\]@\[\e[92m\]\h\[\e[97m\]:\[\e[96m\]\w'"''${TERM_RECURSION_DEPTH:+\[\e[91m\]["$TERM_RECURSION_DEPTH"]}"'\[\e[24;97m\]\$ \[\e[0m\]'
else # sudo root: red username + red hostname
PS1='\[\e[0m\]\[\e[48;5;234m\]\[\e[96m\]$(printf "%-+ 4d" $?)\[\e[93m\][\D{%Y-%m-%d %H:%M:%S}] \[\e[91m\]\u\[\e[97m\]@\[\e[91m\]\h\[\e[97m\]:\[\e[96m\]\w'"''${TERM_RECURSION_DEPTH:+\[\e[91m\]["$TERM_RECURSION_DEPTH"]}"'\[\e[24;97m\]\$ \[\e[0m\]'
fi ; else # other user: green username + green hostname
PS1='\[\e[0m\]\[\e[48;5;234m\]\[\e[96m\]$(printf "%-+ 4d" $?)\[\e[93m\][\D{%Y-%m-%d %H:%M:%S}] \[\e[92m\]\u\[\e[97m\]@\[\e[92m\]\h\[\e[97m\]:\[\e[96m\]\w'"''${TERM_RECURSION_DEPTH:+\[\e[91m\]["$TERM_RECURSION_DEPTH"]}"'\[\e[24;97m\]\$ \[\e[0m\]'
fi
if test "$TERM" = "xterm" ; then
PS1="\[\033]2;\h:\u:\w\007\]$PS1"
fi
fi
export TERM_RECURSION_DEPTH=$(( 1 + ''${TERM_RECURSION_DEPTH:-0} ))
''; # The non-interactive version of bash does not remove »\[« and »\]« from PS1, but without those the terminal gets confused about the cursor position after the prompt once one types more than a bit of text there (at least via serial or SSH).
environment.interactiveShellInit = lib.mkBefore ''
# In RePl mode: remove duplicates from history; don't save commands with a leading space.
HISTCONTROL=ignoredups:ignorespace
# For shells bound to serial interfaces (which can't detect the size of the screen on the other end), default to a more reasonable screen size than 24x80 blocks/chars:
if [[ "$(realpath /dev/stdin)" != /dev/tty[1-8] && $LINES == 24 && $COLUMNS == 80 ]] ; then
stty rows 34 cols 145 # Fairly large font on 1080p. (Setting this too large for the screen warps the output really badly.)
fi
'';
}) (lib.mkIf (cfg.bashInit) { # other »interactiveShellInit« (and »shellAliases«) would go in here, being able to overwrite stuff from above, but still also being included in the alias completion below
environment.interactiveShellInit = lib.mkAfter ''
# enable completion for aliases
source ${ pkgs.fetchFromGitHub {
owner = "cykerway"; repo = "complete-alias";
rev = "4fcd018faa9413e60ee4ec9f48ebeac933c8c372"; # v1.18 (2021-07-17)
sha256 = "sha256-fZisrhdu049rCQ5Q90sFWFo8GS/PRgS29B1eG8dqlaI=";
} }/complete_alias
complete -F _complete_alias "''${!BASH_ALIASES[@]}"
'';
}) ]);
}

View File

@ -1 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }
dirname: inputs@{ self, nixpkgs, ...}: self.lib.__internal__.fun.importModules inputs dirname { }

View File

@ -15,16 +15,16 @@ This uses the same implementation as `boot.loader.generic-extlinux-compatible` t
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.bootloader.extlinux;
targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.wip.parentPaths cfg.targetDir); in config.fileSystems.${path};
dirname: inputs: args@{ config, options, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup extlinux;
cfg = config.boot.loader.${extlinux};
targetMount = let path = lib.findFirst (path: config.fileSystems?${path}) "/" (lib.fun.parentPaths cfg.targetDir); in config.fileSystems.${path};
supportedFSes = [ "vfat" "ntfs" "ext2" "ext3" "ext4" "btrfs" "xfs" "ufs" ]; fsSupported = fs: builtins.elem fs supportedFSes;
in {
options.${prefix} = { bootloader.extlinux = {
options = { boot.loader.${extlinux} = {
enable = lib.mkEnableOption (lib.mdDoc ''
extlinux, a simple bootloader for legacy-BIOS environments, like (by default) Qemu.
`extlinux`, a simple bootloader for legacy-BIOS environments, like (by default) Qemu.
This uses the same implementation as `boot.loader.generic-extlinux-compatible` to generate the bootloader configuration, but then actually also installs `extlinux` itself, instead of relying on something else (like an externally installed u-boot) to read and execute the configuration.
Any options affecting the config file generation by `boot.loader.generic-extlinux-compatible` apply, but `boot.loader.generic-extlinux-compatible.enable` should not be set to `true`.
@ -57,19 +57,17 @@ in {
assertions = [ {
assertion = cfg.allowInstableTargetPart || (builtins.match ''^/dev/disk/by-(id|label|partlabel|partuuid|uuid)/.*[^/]$'' cfg.targetPart) != null;
message = ''
`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.
`config.boot.loader.${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;
message = ''
`config.${prefix}.bootloader.extlinux.targetPart`'s closest mount (`${targetMount.mountPoint}`) is of type `${targetMount.fsType}`, which is not one of extlinux's supported types (${lib.concatStringsSep ", " supportedFSes}).
`config.boot.loader.${extlinux}.targetPart`'s closest mount (`${targetMount.mountPoint}`) is of type `${targetMount.fsType}`, which is not one of extlinux's supported types (${lib.concatStringsSep ", " supportedFSes}).
'';
} ];
${prefix} = {
fs.boot = { enable = lib.mkDefault true; mountpoint = lib.mkDefault cfg.targetDir; };
bootloader.extlinux.allowInstableTargetPart = lib.mkForce false;
};
${setup}.bootpart = { enable = lib.mkDefault true; mountpoint = lib.mkDefault cfg.targetDir; };
boot.loader.${extlinux}.allowInstableTargetPart = lib.mkForce false;
system.boot.loader.id = "extlinux";
system.build.installBootLoader = "${pkgs.writeShellScript "install-extlinux.sh" ''
@ -109,9 +107,9 @@ in {
}) (
(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))
boot.loader.${extlinux} = {
enable = lib.mkIf config.virtualisation.useDefaultFilesystems (lib.mkVMOverride false);
allowInstableTargetPart = lib.mkVMOverride true; # (»/dev/sdX« etc in the VM are stable (if the VM is invoked the same way))
};
})

View File

@ -1 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }
dirname: inputs@{ self, nixpkgs, ...}: self.lib.__internal__.fun.importModules inputs dirname { }

View File

@ -1 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -1,74 +0,0 @@
/*
# `mount a -o noexec` Experiment
This is a (so far not successful) experiment to mount (almost) all filesystems as `noexec`, `nosuid` and `nodev` -- and to then deal with the consequences.
## Exceptions
* `/dev` and `/dev/pts` need `dev`
* `/run/wrappers` needs `exec` `suid`
* `/run/binfmt` needs `exec`
* `/run` `/run/user/*` may need `exec` (TODO: test)
* The Nix build dir (default: `/tmp`) needs `exec` (TODO!)
* Some parts of `/home/<user>/` will need `exec`
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, name, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.experiments.noexec;
in {
options.${prefix} = { experiments.noexec = {
enable = lib.mkEnableOption "(almost) all filesystems being mounted as »noexec« (and »nosuid« and »nodev«)";
}; };
config = let
in lib.mkIf cfg.enable (lib.mkMerge [ ({
# This was the only "special" mount that did not have »nosuid« and »nodev« set:
systemd.packages = [ (lib.wip.mkSystemdOverride pkgs "dev-hugepages.mount" "[Mount]\nOptions=nosuid,nodev,noexec\n") ];
# And these were missing »noexec«:
boot.specialFileSystems."/dev".options = [ "noexec" ];
boot.specialFileSystems."/dev/shm".options = [ "noexec" ];
boot.specialFileSystems."/run/keys".options = [ "noexec" ];
# Make all "real" FSs »noexec« (if »wip.fs.temproot.enable = true«):
${prefix}.fs.temproot = let
it = { mountOptions = { nosuid = true; noexec = true; nodev = true; }; };
in { temp = it; local = it; remote = it; };
# Ensure that the /nix/store is not »noexec«, even if the FS it is on is:
boot.initrd.postMountCommands = ''
if ! mountpoint -q $targetRoot/nix/store ; then
mount --bind $targetRoot/nix/store $targetRoot/nix/store
fi
mount -o remount,exec $targetRoot/nix/store
'';
# Nix has no (direct) settings to change where the builders have their »/build« bound to, but many builds will need it to be »exec«:
systemd.services.nix-daemon = { # TODO: while noexec on /tmp is the problem, neither of this solve it:
serviceConfig.PrivateTmp = true;
#serviceConfig.PrivateMounts = true; serviceConfig.ExecStartPre = "/run/wrappers/bin/mount -o remount,exec /tmp";
};
nix.allowedUsers = [ "root" "@wheel" ]; # This goes hand-in-hand with setting mounts as »noexec«. Cases where a user other than root should build stuff are probably fairly rare. A "real" user might want to, but that is either already in the wheel(sudo) group, or explicitly adding that user is pretty reasonable.
boot.postBootCommands = ''
# Make the /nix/store non-iterable, to make it harder for unprivileged programs to search the store for programs they should not have access to:
unshare --fork --mount --uts --mount-proc --pid -- ${pkgs.bash}/bin/bash -euc '
mount --make-rprivate / ; mount --bind /nix/store /nix/store ; mount -o remount,rw /nix/store
chmod -f 1770 /nix/store
chmod -f 751 /nix/store/.links
'
'';
}) ]);
}

View File

@ -1,23 +1,25 @@
/*
# FS Nixpkgs "Patches"
# Additions to `fileSystems`
Filesystem related "patches" of options in nixpkgs, i.e. additions of options that are *not* prefixed.
Currently, this just adds `preMountCommands`.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inputs.self) lib; in let
dirname: inputs: moduleArgs@{ config, pkgs, lib, utils, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) preMountCommands;
in {
options = {
fileSystems = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule [ { options = {
preMountCommands = lib.mkOption { description = ''
${preMountCommands} = lib.mkOption { description = ''
Commands to be run as root every time before mounting this filesystem, but after all its dependents were mounted (TODO: or does this run just once per boot?).
This does not order itself before or after `systemd-fsck@''${utils.escapeSystemdPath device}.service`.
Note that if a symlink exists at a mount point when systemd's fstab-generator runs, it will read/resolve the symlink and use that as the mount point, resulting in mismatching unit names for that mount, effectively disabling its `preMountCommands`.
This is not implemented for mounts in the initrd (those that are `neededForBoot`) yet.
Note that if a symlink exists at a mount point when systemd's fstab-generator runs, it will read/resolve the symlink and use that as the mount point, resulting in mismatching unit names for that mount, effectively disabling its `${preMountCommands}`.
''; type = lib.types.lines; default = ""; };
}; } ]);
}; };
@ -25,9 +27,14 @@ in {
config = let
in ({
assertions = lib.mapAttrsToList (name: fs: {
assertion = (fs.${preMountCommands} == "") || (!utils.fsNeededForBoot fs);
message = ''The filesystem "${name}" has `.${preMountCommands}` but is also (possibly implicitly) `.neededForBoot`. This is not currently supported.'';
}) config.fileSystems;
# The implementation is derived from the "mkfs-${device'}" service in nixpkgs.
systemd.services = lib.wip.mapMergeUnique (_: { mountPoint, device, preMountCommands, depends, ... }: if (preMountCommands != "") then let
isDevice = lib.wip.startsWith "/dev/" device;
systemd.services = lib.fun.mapMergeUnique (_: args@{ mountPoint, device, depends, ... }: if (args.${preMountCommands} != "") then let
isDevice = lib.fun.startsWith "/dev/" device;
mountPoint' = utils.escapeSystemdPath mountPoint;
device' = utils.escapeSystemdPath device;
in { "pre-mount-${mountPoint'}" = {
@ -36,7 +43,7 @@ in {
requires = lib.optional isDevice "${device'}.device"; after = lib.optional isDevice "${device'}.device";
unitConfig.RequiresMountsFor = depends ++ [ (builtins.dirOf device) (builtins.dirOf mountPoint) ];
unitConfig.DefaultDependencies = false;
serviceConfig.Type = "oneshot"; script = preMountCommands;
serviceConfig.Type = "oneshot"; script = args.${preMountCommands};
}; } else { }) config.fileSystems;
});

View File

@ -1 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -1 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -1,76 +0,0 @@
## Builds the current system's (single »partitionDuringInstallation«ed) disk image and calls »deploy-image-to-hetzner-vps«. The installation heeds any »args« / CLI flags set.
function deploy-system-to-hetzner-vps { # 1: name, 2: serverType
if [[ ! ${args[quiet]:-} ]] ; then echo 'Building the worker image' ; fi
local image ; image=$( mktemp -u ) && prepend_trap "rm -f '$image'" EXIT || return
local buildPid ; install-system "$image" & buildPid=$!
if [[ ! ${args[parallel-build-deploy]:-} ]] ; then wait $buildPid || return ; fi
deploy-image-to-hetzner-vps "$1" "$2" "$image" ${args[parallel-build-deploy]:+"$buildPid"} || return
}
## Creates a new Hetzner Cloud VPS of name »name« and type/size »serverType«, optionally waits for »waitPid« to exit (successfully), copies the system image from the local »imagePath« to the new VPS, boots it, and waits until port 22 is open.
function deploy-image-to-hetzner-vps { # 1: name, 2: serverType, 3: imagePath, 4?: waitPid
local name=$1 serverType=$2 imagePath=$3 waitPid=${4:-}
local stdout=/dev/stdout ; if [[ ${args[quiet]:-} ]] ; then stdout=/dev/null ; fi
local work ; work=$( mktemp -d ) && prepend_trap "rm -rf $work" EXIT || return
local keyName ; for keyName in host login ; do
@{native.openssh}/bin/ssh-keygen -q -N "" -t ed25519 -f $work/$keyName -C $keyName || return
done
echo 'Creating the VPS' >$stdout
if [[ ! ${args[vps-keep-on-build-failure]:-} ]] ; then prepend_trap "if [[ ! -e $work/buildSucceeded ]] ; then @{native.hcloud}/bin/hcloud server delete '$name' ; fi" EXIT || return ; fi
cat <<EOC |
#cloud-config
chpasswd: null
#ssh_pwauth: false
package_update: false
package_upgrade: false
ssh_authorized_keys:
- '$( cat $work/login.pub )'
ssh_genkeytypes: [ ]
ssh_keys:
ed25519_public: '$( cat $work/host.pub )'
ed25519_private: |
$( cat $work/host | @{native.perl}/bin/perl -pe 's/^/ /' )
EOC
@{native.hcloud}/bin/hcloud server create --image=ubuntu-22.04 --name="$name" --type="$serverType" --user-data-from-file - ${args[vps-suppress-create-email]:+--ssh-key dummy} >$stdout || return
# @{native.hcloud}/bin/hcloud server poweron "$name" || return # --start-after-create=false
local ip ; ip=$( @{native.hcloud}/bin/hcloud server ip "$name" ) && echo "$ip" >$work/ip || return
printf "%s %s\n" "$ip" "$( cat $work/host.pub )" >$work/known_hosts || return
local sshCmd ; sshCmd="@{native.openssh}/bin/ssh -oUserKnownHostsFile=$work/known_hosts -i $work/login root@$ip"
printf %s 'Preparing the VPS/worker for image transfer ' >$stdout
sleep 5 ; local i ; for i in $(seq 20) ; do sleep 1 ; if $sshCmd -- true &>/dev/null ; then break ; fi ; printf . >$stdout ; done ; printf ' ' >$stdout
# 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 . >'$stdout'
cp -axT / /tmp/tmp-root/ ; printf . >'$stdout'
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
' || return ; echo . >$stdout
if [[ $waitPid ]] ; then wait $buildPid || return ; fi
echo 'Writing worker image to VPS' >$stdout
@{native.zstd}/bin/zstd -c "$imagePath" | $sshCmd 'set -o pipefail -u -e
</dev/null fuser -mk /old-root &>/dev/null ; 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
' || return
@{native.hcloud}/bin/hcloud server reset "$name" >$stdout || return
printf %s 'Waiting for the worker to boot ' >$stdout
sleep 2 ; local i ; for i in $(seq 20) ; do sleep 1 ; if ( exec 2>&- ; echo >/dev/tcp/"$ip"/22 ) ; then touch $work/buildSucceeded ; break ; fi ; printf . >$stdout ; done ; echo >$stdout
if [[ ! -e $work/buildSucceeded ]] ; then echo 'Unable to connect to VPS worker, it may not have booted correctly ' 1>&2 ; \return 1 ; fi
}

View File

@ -1,56 +0,0 @@
/*
# Hetzner Cloud VPS Base Config
This is "device" type specific configuration for Hetzner's cloud VPS VMs.
## Installation / Testing
Since the VPSes are qemu VMs, the systems can quite accurately be tested locally in qemu:
```bash
nix run '.#<hostname>' -- run-qemu --install
```
Once the system works locally, a fresh installation can be deployed to a new VPS:
```bash
HCLOUD_TOKEN=... nix run '.#<hostname>' -- deploy-system-to-hetzner-vps '<server-name>' '<server-type>'
```
Or deploy an existing image using `deploy-image-to-hetzner-vps`. The `HCLOUD_TOKEN` needs to be created in the cloud console, is specific to the cloud project, has to have write access, and can be revoked after the installation.
Alternatively, manually create a new server instance, boot it into rescue mode, and copy the [installed](../../lib/setup-scripts/README.md#install-system-documentation) image to it:
```bash
cat $image | zstd | ssh $newServerIP 'zstdcat >/dev/sda && sync'
```
If the system image is very large, even if it is mostly empty and with compression, the copy process can take quite a while.
Declaring a smaller image size and expanding it on boot may be a workaround, but (since it depends on the disk partitioning and filesystems used) is out of scope here.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.hardware.hetzner-vps;
in {
options.${prefix} = { hardware.hetzner-vps = {
enable = lib.mkEnableOption "the core hardware configuration for Hetzner VPS (virtual) hardware";
}; };
config = lib.mkIf cfg.enable ({
${prefix} ={
bootloader.extlinux.enable = true;
setup.scripts.hetzner-deploy.path = ./hetzner-deploy-vps.sh;
};
networking.interfaces.eth0.useDHCP = true;
networking.interfaces.eth0.ipv6.routes = [ { address = "::"; prefixLength = 0; via = "fe80::1"; } ];
networking.timeServers = [ "ntp1.hetzner.de" "ntp2.hetzner.com" "ntp3.hetzner.net" ]; # (these should be most accurate)
profiles.qemu-guest.enable = true;
});
}

View File

@ -1,111 +0,0 @@
/*
# Raspberry PI 3 / 4 / Zero 2
Device specific config for any 64bit capable Raspberry PI (esp. rPI4 and CM4, but also 3B(+) and Zero 2, maybe others).
To set up a rPI host, connect it's boot medium (USB SSD or microSD card) to some other host, and run [`install-system`](../../lib/setup-scripts/README.md#install-system-documentation), directly targeting the future boot medium.
## Notes
* Importing this module also makes the [`hardware.raspberry-pi."4".*`](https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/) options from [`nixos-hardware`](https://github.com/NixOS/nixos-hardware/) available (but disabled by default).
* All the `boot.loader.raspberryPi.*` stuff (and maybe also `boot.kernelParams`) seems to be effectively disabled if `boot.loader.generic-extlinux-compatible.enable == true`.
The odd thing is that various online sources, including `nixos-hardware`, enable extlinux (this may simply be because `nixos-generate-config` sets this by default, purely based on the presence of `/boot/extlinux` in the (derived from more generic) default images).
Without extlinux, u-boot is also disabled, which means that (on an rPI4) there is no way to get a generation selection menu, and generations would need to be restored by moving files in the `/boot` partition manually.
* Installing to the eMMC of a CM4: set the "eMMC Boot" jumper/switch to off/disabled; run `nix-shell -p rpiboot --run 'sudo rpiboot'` on a different host; and connect the CM4 carrier to it via the USB-OTG (microUSB / USB-C) port. Then install to the new block device (`/dev/sdX`) that should pop up on the host.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: args@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.hardware.raspberry-pi;
in {
options.${prefix} = { hardware.raspberry-pi = {
enable = lib.mkEnableOption "base configuration for Raspberry Pi 64bit hardware";
i2c = lib.mkEnableOption ''the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL). Also see `hardware.raspberry-pi."4".i2c{0,1}.enable`'';
lightless = lib.mkEnableOption "operation without any activity lights";
}; };
## Import the rPI4 config from nixos-hardware, but have it disabled by default.
# This provides some options for additional onboard hardware components as »hardware.raspberry-pi."4".*«, see: https://github.com/NixOS/nixos-hardware/blob/master/raspberry-pi/4/
imports = let
path = "${inputs.nixos-hardware}/raspberry-pi/4/default.nix"; module = import path args;
in [ { _file = path; imports = [ {
config = lib.mkIf cfg.enable (builtins.removeAttrs module [ "imports" ]);
} ]; } ] ++ module.imports;
config = lib.mkIf cfg.enable (lib.mkMerge [ ({ ## General rPI Stuff (firmware/boot/drivers)
boot.loader.raspberryPi.enable = true;
boot.loader.raspberryPi.version = lib.mkDefault 4; # (For now only relevant with »loader.raspberryPi.uboot.enable«? Which doesn't work with RPI4 yet.)
#boot.loader.raspberryPi.native.copyOldKernels = false; # TODO: Should tell »raspberrypi-builder.sh#copyToKernelsDir« to not to copy old kernels (but instead let the user get them from the main FS to restore them). This is implemented in the script, but there is no option (or code path) to change that parameter.
# use »boot.loader.raspberryPi.version = 3;« for Raspberry PI 3(B+)
boot.loader.grub.enable = false;
boot.loader.generic-extlinux-compatible.enable = lib.mkForce false; # See "Notes" above
# GPU support: https://nixos.wiki/wiki/NixOS_on_ARM/Raspberry_Pi_4#With_GPU
boot.kernelPackages = pkgs.linuxPackagesFor pkgs."linux_rpi${toString config.boot.loader.raspberryPi.version}";
boot.initrd.kernelModules = [ ];
boot.initrd.availableKernelModules = [ "usbhid" "usb_storage" "vc4" ]; # (»vc4« ~= VideoCore driver)
## Serial console:
# Different generations of rPIs have different hardware capabilities in terms of UARTs (driver chips and pins they are connected to), and different device trees (and options for them) and boot stages (firmware/bootloader/OS) can initialize them differently.
# There are three components to a UART port: the driver chip in the CPU, which GPIOs (and through the PCB thereby physical pins) or other interface (bluetooth) they are connected to, and how they are exposed by the running kernel (»/dev/?«).
# All rPIs so far have at least one (faster, data transfer capable) "fully featured PL011" uart0 chip, and a slower, console only "mini" uart1 chip.
# In Linux, uart0 usually maps to »/dev/ttyAMA0«, and uart1 to »/dev/ttyS0«. »/dev/serial0«/»1« are symlinks in Raspbian.
# What interfaces (GPIO/bluetooth) the chips connect to is configured in the device tree. The following should be true for the official/default device trees:
# When bluetooth is enabled (hardware present and not somehow disabled) then uart0 "physically" connects to that, and uart1 connects to pins 08+10 / GPIO 14+15, otherwise the former connects to those pins and the latter is unused. The uart at GPIO 14+15 is referred to as "primary" in the rPI documentation.
# If uart1 is primary, then UART is disabled by default, because uart1 only works at a fixed (250MHz) GPU/bus speed. At some performance or energy cost, the speed can be fixed, enabling the UART, by setting »enable_uart=1« (or »core_freq=250« or »force_turbo=1« (i.e. 400?)) in the firmware config.
# Bottom line, to use UART on GPIO 14+15, one needs to either disable bluetooth / not have it / disconnect uart0 from it and can (only then) use »/dev/ttyAMA0«, or fix the GPU speed and use uart1 (»/dev/ttyS0«).
# On a rPI4, one could use the additional (fast) uart2/3/4/5. Those need to be enabled via device tree( overlay)s, and will be exposed as »/dev/ttyAMAx«.
# For example: boot.loader.raspberryPi.firmwareConfig = "enable_uart=1\n"; boot.kernelParams = [ "console=ttyS0,115200" ];
# TODO: NixOS sets »enable_uart=1« by default. That is probably a bad idea.
boot.kernelParams = [
#"8250.nr_uarts=1" # 8250 seems to be the name of a serial port driver kernel module, which has a compiled limit of ports it manages. Setting this can override that. Not sure whether the 8250 module has any relevance for the rPI.
# (someones claim:) Some gui programs need this
#"cma=128M" # (Continuous Memory Area?)
];
hardware.enableRedistributableFirmware = true; # for WiFi
#hardware.firmware = [ pkgs.firmwareLinuxNonfree pkgs.raspberrypiWirelessFirmware ]; # TODO: try with this instead
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand"; # (not sure whether this is the best choice ...)
hardware.raspberry-pi."4".dwc2.enable = true; # this is required for (the builtin) USBs on the CM4 to work (with NixOS' device trees, setting »dtoverlay=dwc2« in config.txt or eeprom has no effect)
#hardware.raspberry-pi."4".dwc2.dr_mode = lib.mkDefault "otg"; # or "peripheral" or "host" ("otg" seems to work just fine also as host)
hardware.deviceTree.enable = true;
hardware.deviceTree.filter = lib.mkForce "bcm271[01]-rpi-*.dtb"; # rPI(cm)2/3/4(B(+))/Zero2(W) models
environment.systemPackages = with pkgs; [
libraspberrypi # »vcgencmd measure_temp« etc.
raspberrypi-eeprom # rpi-eeprom-update
];
}) (lib.mkIf cfg.i2c { ## i2c
hardware.i2c.enable = true; # includes »boot.kernelModules = [ "i2c-dev" ]« and some »services.udev.extraRules«
environment.systemPackages = [ pkgs.i2c-tools ]; # e.g. »i2cdetect«
boot.loader.raspberryPi.firmwareConfig = "dtparam=i2c_arm=on\n"; # with the default dtb, this enables the ARM i²c /dev/i2c-1 on pins 3+5 / GPIO2+3 (/ SDA+SCL) of all tested rPI models (this has mostly the same effect as setting »hardware.raspberry-pi."4".i2c1.enable«)
# "dtparam=i2c_vc=on" enables the VideoCore i²c on pins 27+28 / GPIO0+1, but https://raspberrypi.stackexchange.com/questions/116726/enabling-of-i2c-0-via-dtparam-i2c-vc-on-on-pi-3b-causes-i2c-10-i2c-11-t
# (there is also »hardware.raspberry-pi."4".i2c{0,1}.enable« as an alternative way to enable i2c_arm and i2c_vc, but that option seems bcm2711(/rPI4) specific)
}) (lib.mkIf cfg.lightless {
boot.loader.raspberryPi.firmwareConfig = ''
# turn off ethernet LEDs
dtparam=eth_led0=4
dtparam=eth_led1=4
'';
systemd.tmpfiles.rules = [
"w /sys/class/leds/led0/brightness - - - - 0" # yellow (activity) LED
"w /sys/class/leds/led1/brightness - - - - 0" # red (power) LED
];
}) ]);
}

65
modules/installer.nix.md Normal file
View File

@ -0,0 +1,65 @@
/*
# NixOS Installer Composition
This module allows for the composition of the installer from multiple script, which can be overridden for specific projects and be adjusted per host.
`writeSystemScripts` in [`../lib/nixos.nix`](../lib/nixos.nix) wraps the result such that the commands can be called from the command line, with options, arguments, and help output.
`mkSystemsFlake` exposes the individual host's installers as flake `apps`.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: moduleArgs@{ config, options, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) installer;
cfg = config.${installer};
in {
options = { ${installer} = {
scripts = lib.mkOption {
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 = {
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; };
text = lib.mkOption { description = "Script text to process."; type = lib.types.str; default = builtins.readFile config.path; };
order = lib.mkOption { description = "Inclusion order of the scripts. Higher orders will be sourced later, and can thus overwrite earlier definitions."; type = lib.types.int; default = 1000; };
}; })));
apply = lib.filterAttrs (k: v: v != null);
};
commands = let desc = when: mounted: ''
Bash commands that are executed during the system installation, ${when}.
Note that these commands are executed without any further sandboxing (i.e. when not using the VM installation mode, as root on the host).
Partitions may be used via `/dev/disk/by-partlabel/`.${lib.optionalString mounted '' The target system is mounted at `$mnt`.''}
''; in {
postPartition = lib.mkOption { description = desc "after partitioning the disks" false; type = lib.types.lines; default = ""; };
postFormat = lib.mkOption { description = desc "after formatting the partitions with filesystems" false; type = lib.types.lines; default = ""; };
postMount = lib.mkOption { description = desc "after mounting all filesystems" true; type = lib.types.lines; default = ""; };
preInstall = lib.mkOption { description = desc "before installing the bootloader" true; type = lib.types.lines; default = ""; };
postInstall = lib.mkOption { description = desc "just before unmounting the new system" true; type = lib.types.lines; default = ""; };
};
outputName = lib.mkOption {
description = ''The name this system is exported as by its defining flake (as »nixosConfigurations.''${outputName}« and »apps.*-linux.''${outputName}«).'';
type = lib.types.nullOr lib.types.str; # This explicitly does not have a default, so that accessing it when it is not set creates an error.
};
build.scripts = lib.mkOption {
type = lib.types.functionTo lib.types.str; internal = true; readOnly = true;
default = context: lib.fun.substituteImplicit { # This replaces the `@{}` references in the scripts with normal bash variables that hold serializations of the Nix values they refer to.
inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues cfg.scripts);
context = { inherit config options pkgs; inherit (moduleArgs) inputs; } // context;
# inherit (builtins) trace;
};
};
}; };
config = {
${installer} = {
scripts = lib.mapAttrs (name: path: lib.mkOptionDefault { inherit path; }) (lib.self.setup-scripts);
};
};
}

View File

@ -1,7 +0,0 @@
# NixOS Module Patches
This directory contains not self-sufficient modules, but modules that are in fact only "patches" to existing modules in NixOS.
While other modules should have an `enable` option, these don't. They define options in the namespace of some existing module, and become active as soon as those options are assigned by some other module.
If there are conflicts in the defined options, then the modules will have to be not imported.

View File

@ -1 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -1,25 +0,0 @@
/*
# `nixpkgs` Profiles as Options
The "modules" in `<nixpkgs>/nixos/modules/profile/` define sets of option defaults to be used in certain contexts.
Unfortunately, they apply their options unconditionally once included, and NixOS' module system does not allow conditional imports.
This wrapper makes it possible to apply a profile based on some option's values.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module patch:
dirname: inputs: specialArgs@{ config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
in {
imports = [
(args@{ config, pkgs, lib, modulesPath, utils, ... }: {
options.profiles.qemu-guest.enable = (lib.mkEnableOption "qemu-guest profile");
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 ...
];
}

View File

@ -1 +0,0 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importModules inputs dirname { }

View File

@ -1,80 +0,0 @@
/*
# Dropbear SSHd Configuration
OpenSSH adds ~35MB closure size. Let's try `dropbear` instead!
## Implementation
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.services.dropbear;
in {
options.${prefix} = { services.dropbear = {
enable = lib.mkEnableOption "dropbear SSH daemon";
port = lib.mkOption { description = "TCP port to listen on and open a firewall rule for."; type = lib.types.port; default = 22; };
socketActivation = lib.mkEnableOption "socket activation mode for dropbear, where systemd launches dropbear on incoming TCP connections, instead of dropbear permanently running and listening on its TCP port";
rootKeys = lib.mkOption { description = "Literal lines to write to »/root/.ssh/authorized_keys«"; default = ""; type = lib.types.lines; };
hostKeys = lib.mkOption { description = "Location of the host key(s) to use. If empty, then a key(s) will be generated at »/etc/dropbear/dropbear_(ecdsa/rsa)_host_key« on first access to the server."; default = [ ]; type = lib.types.listOf lib.types.path; };
}; };
config = let
defaultArgs = lib.concatStringsSep "" [
"${pkgs.dropbear}/bin/dropbear"
(if cfg.hostKeys == [ ] then (
" -R" # generate host keys on connection
) else lib.concatMapStrings (path: (
" -r ${path}"
)) cfg.hostKeys)
];
in lib.mkIf cfg.enable (lib.mkMerge [ ({
networking.firewall.allowedTCPPorts = [ cfg.port ];
}) (lib.mkIf (!cfg.socketActivation) {
systemd.services."dropbear" = {
description = "dropbear SSH server (listening)";
wantedBy = [ "multi-user.target" ]; after = [ "network.target" ];
serviceConfig.ExecStartPre = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/";
serviceConfig.ExecStart = lib.concatStringsSep "" [
defaultArgs
" -p ${toString cfg.port}" # listen on TCP/${port}
" -F -E" # don't fork, use stderr
];
#serviceConfig.PIDFile = "/var/run/dropbear.pid"; serviceConfig.Type = "forking"; after = [ "network.target" ]; # alternative to »-E -F« (?)
};
}) (lib.mkIf (cfg.socketActivation) {
systemd.sockets.dropbear = { # start a »dropbear@.service« on any number of TCP connections on port 22
conflicts = [ "dropbear.service" ];
listenStreams = [ "${toString cfg.port}" ];
socketConfig.Accept = "yes";
#socketConfig.Restart = "always";
wantedBy = [ "sockets.target" ]; # (isn't this implicit?)
};
systemd.services."dropbear@" = {
description = "dropbear SSH server (per-connection)";
after = [ "syslog.target" ];
serviceConfig.PreExec = lib.mkIf (cfg.hostKeys == [ ]) "${pkgs.coreutils}/bin/mkdir -p /etc/dropbear/"; # or before socket?
serviceConfig.ExecStart = lib.concatStringsSep "" [
"-" # for the most part ignore exit != 0
defaultArgs
" -i" # handle a single connection on stdio
];
serviceConfig.StandardInput = "socket";
};
}) (lib.mkIf (cfg.rootKeys != "") {
systemd.tmpfiles.rules = [ (lib.wip.mkTmpfile { type = "L+"; path = "/root/.ssh/authorized_keys"; argument = pkgs.writeText "root-ssh-authorized_keys" cfg.rootKeys; }) ];
}) ]);
}

View File

@ -9,7 +9,7 @@ This runs completely contrary to how NixOS usually does (and should) work (the s
The bash functions in [`lib/setup-scripts`](../../lib/setup-scripts/README.md) take these definitions and perform arbitrarily complex partitioning and file systems creation, as long as:
* Actual device instances or image files (by path) are passed to the installer for each device specified in the configuration.
* Disks are partitioned with GPT, but may optionally have an MBR for boot partitions if the loader requires that.
* `fileSystems.*.device`/`boot.initrd.luks.devices.*.device`/`wip.fs.zfs.pools.*.vdevArgs` refer to partitions by GPT partition label (`/dev/disk/by-partlabel/*`) or (LUKS) mapped name (`dev/mapper/*`).
* `boot.initrd.luks.devices.${name}` have a `wip.fs.keystore.keys."luks/${name}/0"` key specified.
* `fileSystems.*.device`/`boot.initrd.luks.devices.*.device`/`setup.zfs.pools.*.vdevArgs` refer to partitions by GPT partition label (`/dev/disk/by-partlabel/*`) or (LUKS) mapped name (`dev/mapper/*`).
* `boot.initrd.luks.devices.${name}` have a `setup.keystore.keys."luks/${name}/0"` key specified.
[`boot`](./boot.nix.md) and [`temproot`](./temproot.nix.md) help with a concise (if somewhat opinionated) file system setup that places `/` on a file system that gets cleared during reboot, to achieve a system that is truly stateless (or at least explicitly acknowledges where it has state).
[`bootpart`](./bootpart.nix.md) and [`temproot`](./temproot.nix.md) help with a concise (if somewhat opinionated) file system setup that places `/` on a file system that gets cleared during reboot, to achieve a system that is truly stateless (or at least explicitly acknowledges where it has state).

View File

@ -9,13 +9,13 @@ This is a simple shortcut to define and mount a boot/firmware/EFI partition and
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.fs.boot;
dirname: inputs: { config, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup;
cfg = config.${setup}.bootpart;
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
in {
options.${prefix} = { fs.boot = {
options = { ${setup}.bootpart = {
enable = lib.mkEnableOption "configuration of a boot partition as GPT partition 1 on the »primary« disk and a FAT32 filesystem on it";
mountpoint = lib.mkOption { description = "Path at which to mount a vfat boot partition."; type = lib.types.str; default = "/boot"; };
createMbrPart = lib.mkOption { description = "Whether to create a hybrid MBR with (only) the boot partition listed as partition 1."; type = lib.types.bool; default = true; };
@ -25,9 +25,9 @@ in {
config = let
in lib.mkIf cfg.enable (lib.mkMerge [ ({
${prefix} = {
fs.disks.partitions."boot-${hash}" = { type = lib.mkDefault "ef00"; size = lib.mkDefault cfg.size; index = lib.mkDefault 1; order = lib.mkDefault 1500; disk = lib.mkOptionDefault "primary"; }; # require it to be part1, and create it early
fs.disks.devices = lib.mkIf cfg.createMbrPart { primary = { mbrParts = lib.mkDefault "1"; extraFDiskCommands = ''
${setup} = {
disks.partitions."boot-${hash}" = { type = lib.mkDefault "ef00"; size = lib.mkDefault cfg.size; index = lib.mkDefault 1; order = lib.mkDefault 1500; disk = lib.mkOptionDefault "primary"; }; # require it to be part1, and create it early
disks.devices = lib.mkIf cfg.createMbrPart { primary = { mbrParts = lib.mkDefault "1"; extraFDiskCommands = ''
t;1;c # type ; part1 ; W95 FAT32 (LBA)
a;1 # active/boot ; part1
''; }; };

View File

@ -0,0 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.__internal__.fun.importModules inputs dirname { }

View File

@ -9,20 +9,19 @@ Options to declare devices and partitions to be picked up by the installer scrip
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.fs.disks;
globalConfig = config;
dirname: inputs: { config, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup;
cfg = config.${setup}.disks; globalConfig = config;
types.guid = lib.types.strMatching ''^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'';
in {
options.${prefix} = { fs.disks = {
options.${setup} = { disks = {
devices = lib.mkOption {
description = "Set of disk devices that this host will be installed on.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = {
name = lib.mkOption { description = "Name that this device is being referred to as in other places."; type = lib.types.str; default = name; readOnly = true; };
guid = lib.mkOption { description = "GPT disk GUID of the disk."; type = types.guid; default = lib.wip.sha256guid ("gpt-disk:${name}"+":${globalConfig.networking.hostName}"); };
size = lib.mkOption { description = "The size of the disk, either as number in bytes or as argument to »parseSizeSuffix«. When installing to a physical device, its size must match; images are created with this size."; type = lib.types.either lib.types.ints.unsigned lib.types.str; apply = lib.wip.parseSizeSuffix; default = "16G"; };
guid = lib.mkOption { description = "GPT disk GUID of the disk."; type = types.guid; default = lib.fun.sha256guid ("gpt-disk:${name}"+":${globalConfig.networking.hostName}"); };
size = lib.mkOption { description = "The size of the disk, either as number in bytes or as argument to »parseSizeSuffix«. When installing to a physical device, its size must match; images are created with this size."; type = lib.types.either lib.types.ints.unsigned lib.types.str; apply = lib.fun.parseSizeSuffix; default = "16G"; };
sectorSize = lib.mkOption { description = "The disk's logical sector size in bytes. Used to convert (e.g.) partition sizes in sectors to bytes or vice versa."; type = lib.types.ints.unsigned; default = 512; };
allowLarger = lib.mkOption { description = "Whether to allow installation to a physical disk that is larger than the declared size."; type = lib.types.bool; default = true; };
serial = lib.mkOption { description = "Serial number of the specific hardware device to use. If set the device path passed to the installer must point to the device with this serial. Use » udevadm info --query=property --name=$DISK | grep -oP 'ID_SERIAL_SHORT=\K.*' || echo '<none>' « to get the serial."; type = lib.types.nullOr lib.types.str; default = null; };
@ -42,7 +41,7 @@ in {
description = "Set of disks disk partitions that the system will need/use. Partitions will be created on their respective ».disk«s in ».order« using »sgdisk -n X:+0+$size«.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, config, ... }: { options = {
name = lib.mkOption { description = "Name/partlabel that this partition can be referred to as once created."; type = lib.types.str; default = name; readOnly = true; };
guid = lib.mkOption { description = "GPT partition GUID of the partition."; type = types.guid; default = lib.wip.sha256guid ("gpt-part:${name}"+":${globalConfig.networking.hostName}"); };
guid = lib.mkOption { description = "GPT partition GUID of the partition."; type = types.guid; default = lib.fun.sha256guid ("gpt-part:${name}"+":${globalConfig.networking.hostName}"); };
disk = lib.mkOption { description = "Name of the disk that this partition resides on, which will automatically be declared with default options."; type = lib.types.str; default = "primary"; };
type = lib.mkOption { description = "»gdisk« partition type of this partition."; type = lib.types.str; };
size = lib.mkOption { description = "Partition size, either as integer suffixed with »K«, »M«, »G«, etc for sizes in XiB, or an integer suffixed with »%« for that portion of the size of the actual disk the partition gets created on. Or »null« to fill the remaining disk space."; type = lib.types.nullOr lib.types.str; default = null; };
@ -56,30 +55,22 @@ in {
};
partitionList = lib.mkOption { description = "Partitions as a sorted list."; type = lib.types.listOf (lib.types.attrsOf lib.types.anything); default = lib.sort (before: after: before.order >= after.order) (lib.attrValues cfg.partitions); readOnly = true; internal = true; };
partitioning = lib.mkOption { description = "The resulting disk partitioning as »sgdisk --backup --print« per disk."; type = lib.types.package; readOnly = true; internal = true; };
# These are less disk-state-describing and more installation-imperative ...
# Also, these are run as root and thee are no security or safety checks ...
postPartitionCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
postFormatCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
postMountCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
preInstallCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
postInstallCommands = lib.mkOption { description = ""; type = lib.types.lines; default = ""; };
}; };
config.${prefix} = {
config.${setup} = {
# (Don't) create all devices referenced by partitions: (The problem with this is that all device attributes depend on the partition attributes, and it would thus be impossible to have a dependency in reverse (e.g. a partition's size based on the disk size).)
#fs.disks.devices = lib.genAttrs (lib.catAttrs "disk" config.${prefix}.fs.disks.partitionList) (name: { });
#disks.devices = lib.genAttrs (lib.catAttrs "disk" config.${setup}.disks.partitionList) (name: { });
fs.disks.partitioning = let
partition-disk = { name = "partition-disk"; text = lib.wip.extractBashFunction (builtins.readFile lib.wip.setup-scripts.disk) "partition-disk"; };
disks.partitioning = let
partition-disk = { name = "partition-disk"; text = lib.fun.extractBashFunction (builtins.readFile lib.self.setup-scripts.disk) "partition-disk"; };
esc = lib.escapeShellArg; native = pkgs.buildPackages;
in pkgs.runCommand "partitioning-${config.networking.hostName}" { } ''
${lib.wip.substituteImplicit { inherit pkgs; scripts = [ partition-disk ]; context = { inherit config native; }; }} # inherit (builtins) trace;
${lib.fun.substituteImplicit { inherit pkgs; scripts = [ partition-disk ]; context = { inherit config native; }; }} # inherit (builtins) trace;
set -u ; mkdir -p $out ; declare -A args=([debug]=1)
${lib.concatStrings (lib.mapAttrsToList (name: disk: ''
name=${esc name} ; img=$name.img
${native.coreutils}/bin/truncate -s ${esc disk.size} "$img"
partition-disk "$name" "$img" ${toString (lib.wip.parseSizeSuffix disk.size)}
partition-disk "$name" "$img" ${toString (lib.fun.parseSizeSuffix disk.size)}
${native.gptfdisk}/bin/sgdisk --backup=$out/"$name".backup "$img"
${native.gptfdisk}/bin/sgdisk --print "$img" >$out/"$name".gpt
${if disk.mbrParts != null then ''

View File

@ -27,24 +27,24 @@ Any number of other devices may thus specify paths in the keystore as keylocatio
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.fs.keystore;
dirname: inputs: { config, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup installer;
cfg = config.${setup}.keystore;
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
keystore = "/run/keystore-${hash}";
keystoreKeys = lib.attrValues (lib.filterAttrs (n: v: lib.wip.startsWith "luks/keystore-${hash}/" n) cfg.keys);
keystoreKeys = lib.attrValues (lib.filterAttrs (n: v: lib.fun.startsWith "luks/keystore-${hash}/" n) cfg.keys);
in let module = {
options.${prefix} = { fs.keystore = {
options = { ${setup}.keystore = {
enable = lib.mkEnableOption "the use of a keystore partition to unlock various things during early boot";
enableLuksGeneration = (lib.mkEnableOption "the generation of a LUKS mapper configuration for each »luks/*/0« entry in ».keys«") // { default = true; example = false; };
keys = lib.mkOption { description = "Keys declared to be generated during installation and then exist in the keystore for unlocking disks and such. See »${dirname}/keystore.nix.md« for much more information."; type = lib.types.attrsOf (lib.types.either (lib.types.nullOr lib.types.str) (lib.types.attrsOf lib.types.str)); default = { }; apply = keys: (
lib.wip.mapMergeUnique (usage: methods: if methods == null then { } else if builtins.isString methods then { "${usage}" = methods; } else lib.wip.mapMerge (slot: method: if method == null then { } else { "${usage}/${slot}" = method; }) methods) keys
lib.fun.mapMergeUnique (usage: methods: if methods == null then { } else if builtins.isString methods then { "${usage}" = methods; } else lib.fun.mapMerge (slot: method: if method == null then { } else { "${usage}/${slot}" = method; }) methods) keys
); };
unlockMethods = {
trivialHostname = lib.mkOption { description = "For headless auto boot, use »hostname« (in a file w/o newline) as trivial password/key for the keystore."; type = lib.types.bool; default = lib.elem "hostname" keystoreKeys; };
usbPartition = lib.mkOption { description = "Use (the random key stored on) a specifically named (tiny) GPT partition (usually on a USB-stick) to automatically unlock the keystore. Use »nix run .#$hostName -- add-bootkey-to-keydev $devPath« (see »${inputs.self}/lib/setup-scripts/maintenance.sh«) to cerate such a partition."; type = lib.types.bool; default = (lib.elem "usb-part" keystoreKeys); };
pinThroughYubikey = lib.mkOption { type = lib.types.bool; default = (lib.any (type: lib.wip.matches "^yubikey-pin=.*$" type) keystoreKeys); };
pinThroughYubikey = lib.mkOption { type = lib.types.bool; default = (lib.any (type: lib.fun.matches "^yubikey-pin=.*$" type) keystoreKeys); };
};
}; };
@ -57,15 +57,15 @@ in let module = {
message = ''At least one key (»0«) for »luks/keystore-${hash}« must be specified!'';
} ];
boot.initrd.luks.devices = lib.mkIf cfg.enableLuksGeneration (lib.wip.mapMerge (key: let
boot.initrd.luks.devices = lib.mkIf cfg.enableLuksGeneration (lib.fun.mapMerge (key: let
label = builtins.substring 5 ((builtins.stringLength key) - 7) key;
in { ${label} = {
device = lib.mkDefault "/dev/disk/by-partlabel/${label}";
keyFile = lib.mkIf (label != "keystore-${hash}") (lib.mkDefault "/run/keystore-${hash}/luks/${label}/0.key");
allowDiscards = lib.mkDefault true; # If attackers can observe trimmed blocks, then they can probably do much worse ...
}; }) (lib.wip.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys)));
}; }) (lib.fun.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys)));
${prefix}.fs.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; # (This is the only key that the setup scripts unconditionally require to be declared.)
${setup}.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; # (This is the only key that the setup scripts unconditionally require to be declared.)
}) ({
@ -88,22 +88,18 @@ in let module = {
# Create and populate keystore during installation:
fileSystems.${keystore} = { fsType = "vfat"; device = "/dev/mapper/keystore-${hash}"; options = [ "ro" "nosuid" "nodev" "noexec" "noatime" "umask=0277" "noauto" ]; formatOptions = ""; };
${prefix} = {
fs.disks.partitions."keystore-${hash}" = { type = lib.mkDefault "8309"; order = lib.mkDefault 1375; disk = lib.mkDefault "primary"; size = lib.mkDefault "32M"; };
fs.disks.postFormatCommands = ''
( : 'Copy the live keystore to its primary persistent location:'
tmp=$(mktemp -d) && ${pkgs.util-linux}/bin/mount "/dev/mapper/keystore-${hash}" $tmp && trap "${pkgs.util-linux}/bin/umount $tmp && rmdir $tmp" EXIT &&
${pkgs.rsync}/bin/rsync -a ${keystore}/ $tmp/
)
'';
};
${setup}.disks.partitions."keystore-${hash}" = { type = lib.mkDefault "8309"; order = lib.mkDefault 1375; disk = lib.mkDefault "primary"; size = lib.mkDefault "32M"; };
${installer}.commands.postFormat = ''( : 'Copy the live keystore to its primary persistent location:'
tmp=$(mktemp -d) && ${pkgs.util-linux}/bin/mount "/dev/mapper/keystore-${hash}" $tmp && trap "${pkgs.util-linux}/bin/umount $tmp && rmdir $tmp" EXIT &&
${pkgs.rsync}/bin/rsync -a ${keystore}/ $tmp/
)'';
}) (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 = ''
${if (lib.any (lib.wip.matches "^home/.*$") (lib.attrNames cfg.keys)) then ''
${if (lib.any (lib.fun.matches "^home/.*$") (lib.attrNames cfg.keys)) then ''
echo "Transferring home key composites"
# needs to be available later to unlock the home on demand
mkdir -p /run/keys/home-composite/ ; chmod 551 /run/keys/home-composite/ ; cp -a ${keystore}/home/*.key /run/keys/home-composite/

View File

@ -48,46 +48,46 @@ let hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networkin
This completely configures the disks, partitions, pool, datasets, and mounts for a ZFS `rpool` on a three-disk `raidz1` with read and write cache on an additional SSD, which also holds the boot partition and swap:
```nix
{ wip.fs.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; }); # Need more than one disk, so must declare them. When installing to a physical disk, the declared size must match the actual size (or be smaller). The »primary« disk will hold all implicitly created partitions and those not stating a »disk«.
wip.fs.boot.enable = true; wip.fs.boot.size = "512M"; # See »./boot.nix.md«. Creates a FAT boot partition.
{ setup.disks.devices = lib.genAttrs ([ "primary" "raidz1" "raidz2" "raidz3" ]) (name: { size = "16G"; }); # Need more than one disk, so must declare them. When installing to a physical disk, the declared size must match the actual size (or be smaller). The »primary« disk will hold all implicitly created partitions and those not stating a »disk«.
setup.bootpart.enable = true; setup.bootpart.size = "512M"; # See »./boot.nix.md«. Creates a FAT boot partition.
wip.fs.keystore.enable = true; # See »./keystore.nix.md«. With this enabled, »remote« will automatically be encrypted, with a random key by default.
setup.keystore.enable = true; # See »./keystore.nix.md«. With this enabled, »remote« will automatically be encrypted, with a random key by default.
wip.fs.temproot.enable = true; # Use ZFS for all categories.
wip.fs.temproot.temp.type = "zfs";
wip.fs.temproot.local.type = "zfs";
wip.fs.temproot.remote.type = "zfs";
setup.temproot.enable = true; # Use ZFS for all categories.
setup.temproot.temp.type = "zfs";
setup.temproot.local.type = "zfs";
setup.temproot.remote.type = "zfs";
# Change/set the pools storage layout (see above), then adjust the partitions disks/sizes. Declaring disks requires them to be passed to the system installer.
wip.fs.zfs.pools."rpool-${hash}".vdevArgs = [ "raidz1" "rpool-rz1-${hash}" "rpool-rz2-${hash}" "rpool-rz3-${hash}" "log" "rpool-zil-${hash}" "cache" "rpool-arc-${hash}" ];
wip.fs.disks.partitions."rpool-rz1-${hash}" = { disk = "raidz1"; };
wip.fs.disks.partitions."rpool-rz2-${hash}" = { disk = "raidz2"; };
wip.fs.disks.partitions."rpool-rz3-${hash}" = { disk = "raidz3"; };
wip.fs.disks.partitions."rpool-zil-${hash}" = { size = "2G"; };
wip.fs.disks.partitions."rpool-arc-${hash}" = { }; } # (this is actually already implicitly declared)
setup.zfs.pools."rpool-${hash}".vdevArgs = [ "raidz1" "rpool-rz1-${hash}" "rpool-rz2-${hash}" "rpool-rz3-${hash}" "log" "rpool-zil-${hash}" "cache" "rpool-arc-${hash}" ];
setup.disks.partitions."rpool-rz1-${hash}" = { disk = "raidz1"; };
setup.disks.partitions."rpool-rz2-${hash}" = { disk = "raidz2"; };
setup.disks.partitions."rpool-rz3-${hash}" = { disk = "raidz3"; };
setup.disks.partitions."rpool-zil-${hash}" = { size = "2G"; };
setup.disks.partitions."rpool-arc-${hash}" = { }; } # (this is actually already implicitly declared)
```
On a less beefy system, but also with less data to manage, `tmpfs` works fine for `tmp`, and `f2fs` promises to get more performance out of the flash/ram/cpu:
```nix
{ # See above for these:
#wip.fs.disks.devices.primary.size = "16G"; # (default)
wip.fs.boot.enable = true; wip.fs.boot.size = "512M";
wip.fs.keystore.enable = true;
wip.fs.temproot.enable = true;
#setup.disks.devices.primary.size = "16G"; # (default)
setup.bootpart.enable = true; setup.bootpart.size = "512M";
setup.keystore.enable = true;
setup.temproot.enable = true;
wip.fs.temproot.temp.type = "tmpfs"; # Put `/` on a `tmpfs`.
wip.fs.temproot.local.type = "bind"; # `bind`-mount all `local` locations to `/.local/*`, ...
wip.fs.temproot.local.bind.base = "f2fs-encrypted"; # ... where a LUKS-encrypted F2FS is mounted.
#wip.fs.keystore.keys."luks/local-${hash}/0" = "random"; # (implied by the »-encrypted« suffix above)
#wip.fs.disks.partitions."local-${hash}".size = "50%"; # (default, fixed after installation)
wip.fs.temproot.remote.type = "zfs";
#wip.fs.keystore.keys."zfs/rpool-${hash}/remote" = "random"; # (default)
#wip.fs.keystore.keys."luks/rpool-${hash}/0" = "random"; # Would also enable LUKS encryption of the pool, but there isn't too much point in encrypting twice.
#wip.fs.zfs.pools."rpool-${hash}".vdevArgs = [ "rpool-${hash}" ]; # (default)
#wip.fs.disks.partitions."rpool-${hash}" = { type = "bf00"; size = null; order = 500; }; # (default)
setup.temproot.temp.type = "tmpfs"; # Put `/` on a `tmpfs`.
setup.temproot.local.type = "bind"; # `bind`-mount all `local` locations to `/.local/*`, ...
setup.temproot.local.bind.base = "f2fs-encrypted"; # ... where a LUKS-encrypted F2FS is mounted.
#setup.keystore.keys."luks/local-${hash}/0" = "random"; # (implied by the »-encrypted« suffix above)
#setup.disks.partitions."local-${hash}".size = "50%"; # (default, fixed after installation)
setup.temproot.remote.type = "zfs";
#setup.keystore.keys."zfs/rpool-${hash}/remote" = "random"; # (default)
#setup.keystore.keys."luks/rpool-${hash}/0" = "random"; # Would also enable LUKS encryption of the pool, but there isn't too much point in encrypting twice.
#setup.zfs.pools."rpool-${hash}".vdevArgs = [ "rpool-${hash}" ]; # (default)
#setup.disks.partitions."rpool-${hash}" = { type = "bf00"; size = null; order = 500; }; # (default)
# Default mounts/binds can also be removed, in this case causing logs to be removed on reboot:
wip.fs.temproot.local.mounts."/var/log" = lib.mkForce null; }
setup.temproot.local.mounts."/var/log" = lib.mkForce null; }
```
@ -95,13 +95,13 @@ On a less beefy system, but also with less data to manage, `tmpfs` works fine fo
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inputs.self) lib; in let
prefix = inputs.config.prefix;
cfg = config.${prefix}.fs.temproot;
dirname: inputs: moduleArgs@{ config, options, pkgs, lib, utils, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup preMountCommands;
cfg = config.${setup}.temproot; opts = options.${setup}.temproot;
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
keep = if cfg.remote.type == "none" then "local" else "remote"; # preferred place for data that should be kept
optionsFor = type: desc: lib.wip.mergeAttrsRecursive [ ({
optionsFor = type: desc: lib.fun.mergeAttrsRecursive [ ({
zfs.dataset = lib.mkOption { description = "Dataset path under which to create the ${desc} »${type}« datasets."; type = lib.types.str; default = "rpool-${hash}/${type}"; };
}) (lib.optionalAttrs (type != "temp") {
bind.source = lib.mkOption { description = "Prefix for bind-mount targets."; type = lib.types.str; default = "/.${type}"; };
@ -109,7 +109,7 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inp
bind.base = lib.mkOption { description = "Filesystem to automatically create as the ».source« for the bind mounts."; type = lib.types.enum [ null "f2fs" "f2fs-encrypted" "ext4" "ext4-encrypted" ]; default = null; };
}) ({
mounts = lib.mkOption {
description = "Locations (for »temp« in addition to »/«) where a ${desc} filesystem should be mounted. Some are declared by default but may be removed by setting them to »null«.";
description = "Locations ${if type == "temp" then "(in addition to »/«)" else ""} where a ${desc} filesystem should be mounted. Some are declared by default but may be removed by setting them to »null«.";
type = lib.types.attrsOf (lib.types.nullOr (lib.types.submodule ({ name, ... }: { options = {
target = lib.mkOption { description = "Attribute name as the mount target path."; type = lib.types.strMatching ''^/.*[^/]$''; default = name; readOnly = true; };
source = lib.mkOption { description = "Relative source path of the mount. (Irrelevant for »tmpfs«.)"; type = lib.types.str; default = builtins.substring 1 (builtins.stringLength name - 1) name; };
@ -117,7 +117,7 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inp
gid = lib.mkOption { description = "GID owning the mounted target."; type = lib.types.ints.unsigned; default = 0; };
mode = lib.mkOption { description = "Permission mode of the mounted target."; type = lib.types.str; default = "750"; };
options = lib.mkOption { description = "Additional mount options to set. Note that not all mount types support all options, they may be silently ignored or cause errors. »bind« supports setting »nosuid«, »nodev«, »noexec«, »noatime«, »nodiratime«, and »relatime«. »zfs« will explicitly heed »noauto«, the other options are applied but may conflict with the ones implied by the ».zfsProps«."; type = lib.types.attrsOf (lib.types.oneOf [ lib.types.bool lib.types.str lib.types.str lib.types.int ]); default = { }; };
extraFsConfig = lib.mkOption { description = "Extra config options to set on the generated »fileSystems.*« entry (unless this mount is forced to »null«)."; type = lib.types.attrsOf lib.types.anything; default = { }; };
extraFsConfig = lib.mkOption { description = "Extra config options to set on the generated »fileSystems.*« entry (unless this mount is forced to »null«)."; type = options.fileSystems.type.nestedTypes.elemType // { visible = "shallow"; }; default = { }; };
zfsProps = lib.mkOption { description = "ZFS properties to set on the dataset, if mode type is »zfs«. Note that ZFS mounts made in the initramfs don't have the correct mount options from ZFS properties, so properties that affect mount options should (also) be set as ».options«."; type = lib.types.attrsOf (lib.types.nullOr lib.types.str); default = { }; };
}; })));
default = { };
@ -129,14 +129,15 @@ dirname: inputs: specialArgs@{ config, pkgs, lib, utils, ... }: let inherit (inp
zfsNoSyncProps = { sync = "disabled"; logbias = "throughput"; }; # According to the documentation, »logbias« should be irrelevant without sync (i.e. no logging), but some claim setting it to »throughput« still improves performance.
in {
options.${prefix} = { fs.temproot = {
options = { ${setup}.temproot = {
enable = lib.mkEnableOption "filesystem layouts with ephemeral root";
# Settings for filesystems that will be cleared on reboot:
temp = {
type = lib.mkOption { description = ''
The type of filesystem that holds the system's files that can (and should) be cleared on reboot:
"tmpfs": Creates »tmpfs« filesystems at »/« and all specified mount points.
"zfs": ...
"zfs": Creates a ZFS dataset for »/« and each specified mount point, as (nested) children of ».zfs.dataset«, which will have »sync« disabled. Also adds an pre-mount command that rolls back all children of that dataset to their »@empty« snapshots (which are taken right after the datasets are created).
"bind": Expects a filesystem to be mounted at »/«. Creates a hook to cre-create that filesystem on boot (TODO: implement and then enable this), and bind-mounts any additional mounts to ».bind.source+"/"+<mount.source>« (creating those paths if necessary).
''; type = lib.types.enum [ "tmpfs" "zfs" ]; default = "tmpfs"; };
} // (optionsFor "temp" "temporary");
@ -144,16 +145,18 @@ in {
# Settings for filesystems that persist across reboots:
local = {
type = lib.mkOption { description = ''
"bind": Expects a (locally persistent) filesystem to be mounted at ».bind.target«, and bind-mounts any additional mounts to ».bind.source+"/"+<mount.source>« (creating those paths if necessary).
"zfs": ...
The type of filesystem that holds the system's files that needs (e.g. the Nix store) or should (e.g. caches, logs) be kept across reboots, but that can be regenerated or not worth backing up:
"bind": Expects a (locally persistent) filesystem to be mounted at ».bind.target«, and bind-mounts to ».bind.source+"/"+<mount.source>« (creating those paths if necessary). ».bind.base« can be used to automatically mount different default filesystems at ».bind.target«.
"zfs": Creates a ZFS dataset for »/« and each specified mount point, as (nested) children of ».zfs.dataset«.
''; type = lib.types.enum [ "bind" "zfs" ]; default = "bind"; };
} // (optionsFor "local" "locally persistent");
# Settings for filesystems that should have remote backups:
remote = {
type = lib.mkOption { description = ''
"bind": Expects a filesystem to be mounted at ».bind.target« that gets backed. Bind-mounts any additional mounts to ».bind.source+"/"+<mount.source>« (creating those paths if necessary).
"zfs": ...
The type of filesystem that holds the system's files that need to be backed up (which some external mechanism should then do):
"bind": Expects a filesystem to be mounted at ».bind.target« that gets backed. Bind-mounts to ».bind.source+"/"+<mount.source>« (creating those paths if necessary).
"zfs": Creates a ZFS dataset for »/« and each specified mount point, as (nested) children of ».zfs.dataset«.
"none": Don't provide a »/remote«. For hosts that have no secrets or important state.
''; type = lib.types.enum [ "bind" "zfs" "none" ]; default = "bind"; };
} // (optionsFor "remote" "remotely backed-up");
@ -174,27 +177,27 @@ in {
in lib.mkIf cfg.enable (lib.mkMerge ([ ({
${prefix} = {
fs.temproot.temp.mounts = {
${setup} = {
temproot.temp.mounts = {
"/tmp" = { mode = "1777"; };
};
fs.temproot.local.mounts = {
temproot.local.mounts = {
"/local" = { source = "system"; mode = "755"; };
"/nix" = { zfsProps = zfsNoSyncProps; mode = "755"; }; # this (or /nix/store) is required
"/var/log" = { source = "logs"; mode = "755"; };
# »/swap« is used by »cfg.swap.asPartition = false«
};
fs.temproot.remote.mounts = {
"/remote" = { source = "system"; mode = "755"; extraFsConfig = { neededForBoot = true; }; }; # if any secrets need to be picked up by »activate«, they should be here
temproot.remote.mounts = {
"/remote" = { source = "system"; mode = "755"; extraFsConfig = { neededForBoot = lib.mkDefault true; }; }; # if any secrets need to be picked up by »activate«, they should be here
};
};
boot.tmpOnTmpfs = false; # This would create a systemd mount unit for »/tmp«.
boot = if options.boot?tmp then { tmp.useTmpfs = false; } else { tmpOnTmpfs = false; }; # This would create a systemd mount unit for »/tmp«.
}) ({ # Make each individual attribute on »wip.fs.temproot.*.mountOptions« a default, instead of having them be the default as a set:
}) ({ # Make each individual attribute on »setup.temproot.*.mountOptions« a default, instead of having them be the default as a set:
${prefix}.fs.temproot = let
${setup}.temproot = let
it = { mountOptions = { nosuid = lib.mkOptionDefault true; nodev = lib.mkOptionDefault true; noatime = lib.mkOptionDefault true; }; };
in { temp = it; local = it; remote = it; };
@ -232,13 +235,13 @@ in {
}) (lib.mkIf (cfg.swap.size != null && cfg.swap.asPartition) (let
useLuks = config.${prefix}.fs.keystore.keys?"luks/swap-${hash}/0";
device = "/dev/${if useLuks then "mapper" else "disk/by-partlabel"}/swap-${hash}";
device = "/dev/${if cfg.swap.encrypted then "mapper" else "disk/by-partlabel"}/swap-${hash}";
in {
${prefix} = {
fs.disks.partitions."swap-${hash}" = { type = lib.mkDefault "8200"; size = lib.mkDefault cfg.swap.size; order = lib.mkDefault 1250; };
fs.keystore.keys."luks/swap-${hash}/0" = lib.mkIf cfg.swap.encrypted (lib.mkOptionDefault "random");
${setup} = {
disks.partitions."swap-${hash}" = { type = lib.mkDefault "8200"; size = lib.mkDefault cfg.swap.size; order = lib.mkDefault 1250; };
keystore.enable = lib.mkIf cfg.swap.encrypted true;
keystore.keys."luks/swap-${hash}/0" = lib.mkIf cfg.swap.encrypted (lib.mkOptionDefault "random");
};
swapDevices = [ { inherit device; } ];
boot.resumeDevice = device;
@ -246,17 +249,19 @@ in {
})) (lib.mkIf (cfg.swap.size != null && !cfg.swap.asPartition) {
swapDevices = [ { device = "${cfg.local.bind.source}/swap"; size = (lib.wip.parseSizeSuffix cfg.swap.size) / 1024 / 1024; } ];
swapDevices = [ { device = "${cfg.local.bind.source}/swap"; size = (lib.fun.parseSizeSuffix cfg.swap.size) / 1024 / 1024; } ];
}) (lib.mkIf (cfg.temp.type == "tmpfs") (let type = "temp"; in { # (only temp can be of type tmpfs)
# TODO: this would probably be better implemented by creating a single /.temp tmpfs with a decent size restriction, and then bind-mounting all other mount points into that pool (or at least do that for any locations that are non-root writable?)
fileSystems = lib.mapAttrs (target: args@{ uid, gid, mode, extraFsConfig, ... }: (extraFsConfig // {
fileSystems = lib.mapAttrs (target: args@{ uid, gid, mode, ... }: (lib.mkMerge ((
map (def: def.${target}.extraFsConfig or { }) opts.${type}.mounts.definitions
) ++ [ (rec {
fsType = "tmpfs"; device = "tmpfs";
options = (extraFsConfig.options or [ ]) ++ (optionsToList (cfg.${type}.mountOptions // args.options // { uid = toString uid; gid = toString gid; mode = mode; }));
})) ({ "/" = { options = { }; uid = 0; gid = 0; mode = "755"; extraFsConfig = { }; }; } // cfg.${type}.mounts);
options = optionsToList (cfg.${type}.mountOptions // args.options // { uid = toString uid; gid = toString gid; mode = mode; });
}) ]))) ({ "/" = { options = { }; uid = 0; gid = 0; mode = "755"; }; } // cfg.${type}.mounts);
})) (lib.mkIf (cfg.temp.type == "zfs") {
@ -273,21 +278,21 @@ in {
}) (lib.mkIf (cfg.local.type == "bind" && (cfg.local.bind.base != null)) (let # Convenience option to create a local F2FS/EXT4 optimized to host the nix store:
type = if cfg.local.bind.base == "f2fs" || cfg.local.bind.base == "f2fs-encrypted" then "f2fs" else "ext4";
fsType = if cfg.local.bind.base == "f2fs" || cfg.local.bind.base == "f2fs-encrypted" then "f2fs" else "ext4";
encrypted = cfg.local.bind.base == "f2fs-encrypted" || cfg.local.bind.base == "ext4-encrypted";
useLuks = config.${prefix}.fs.keystore.keys?"luks/local-${hash}/0";
in {
# TODO: fsck
${prefix} = {
fs.keystore.keys."luks/local-${hash}/0" = lib.mkIf encrypted (lib.mkOptionDefault "random");
fs.disks.partitions."local-${hash}" = {
${setup} = {
disks.partitions."local-${hash}" = {
type = "8300"; order = lib.mkDefault 1000; disk = lib.mkDefault "primary"; size = lib.mkDefault (if cfg.remote.type == "none" then null else "50%");
};
keystore.enable = lib.mkIf encrypted true;
keystore.keys."luks/local-${hash}/0" = lib.mkIf encrypted (lib.mkOptionDefault "random");
};
fileSystems.${cfg.local.bind.source} = {
fsType = type; device = "/dev/${if useLuks then "mapper" else "disk/by-partlabel"}/local-${hash}";
} // (if type == "f2fs" then {
fsType = fsType; device = "/dev/${if encrypted then "mapper" else "disk/by-partlabel"}/local-${hash}";
} // (if fsType == "f2fs" then {
formatOptions = (lib.concatStrings [
" -O extra_attr" # required by other options
",inode_checksum" # enable inode checksum
@ -324,36 +329,37 @@ in {
})) (lib.mkIf (cfg.remote.type == "none") {
systemd.tmpfiles.rules = [ (lib.wip.mkTmpfile { type = "L+"; path = "/remote"; argument = "/local"; }) ]; # for compatibility (but use a symlink to make clear that this is not actually a separate mount)
systemd.tmpfiles.rules = [ (lib.fun.mkTmpfile { type = "L+"; path = "/remote"; argument = "/local"; }) ]; # for compatibility (but use a symlink to make clear that this is not actually a separate mount)
}) ] ++ (map (type: (lib.mkIf (cfg.${type}.type == "bind") {
fileSystems = (lib.mapAttrs (target: args@{ source, uid, gid, mode, extraFsConfig, ... }: extraFsConfig // (rec {
fileSystems = lib.mkMerge [ (lib.mapAttrs (target: args@{ source, uid, gid, mode, extraFsConfig, ... }: (lib.mkMerge ((
map (def: def.${target}.extraFsConfig or { }) opts.${type}.mounts.definitions
) ++ [ (rec {
device = "${cfg.${type}.bind.source}/${source}";
options = (extraFsConfig.options or [ ]) ++ (optionsToList (cfg.${type}.mountOptions // args.options // { bind = true; }));
preMountCommands = ''
${extraFsConfig.preMountCommands or ""}
options = optionsToList (cfg.${type}.mountOptions // args.options // { bind = true; });
${preMountCommands} = lib.mkIf (!extraFsConfig.neededForBoot && !(lib.elem target utils.pathsNeededForBoot)) ''
mkdir -pm 000 -- ${lib.escapeShellArg target}
mkdir -pm 000 -- ${lib.escapeShellArg device}
chown ${toString uid}:${toString gid} -- ${lib.escapeShellArg device}
chmod ${mode} -- ${lib.escapeShellArg device}
'';
})) cfg.${type}.mounts) // {
}) ]))) cfg.${type}.mounts) {
${cfg.${type}.bind.source} = { neededForBoot = lib.any utils.fsNeededForBoot (lib.attrValues (builtins.intersectAttrs cfg.${type}.mounts config.fileSystems)); };
};
} ];
})) [ "temp" "local" "remote" ]) ++ (map (type: (lib.mkIf (cfg.${type}.type == "zfs") (let
dataset = cfg.${type}.zfs.dataset;
in {
${prefix} = {
fs.zfs.enable = true;
fs.zfs.pools.${lib.head (lib.splitString "/" dataset)} = { }; # ensure the pool exists (all properties can be adjusted)
fs.keystore.keys."zfs/${dataset}" = lib.mkIf (type == "remote" && config.${prefix}.fs.keystore.enable) (lib.mkOptionDefault "random"); # the entire point of ZFS remote are backups, and those should be encrypted
${setup} = {
zfs.enable = true;
zfs.pools.${lib.head (lib.splitString "/" dataset)} = { }; # ensure the pool exists (all properties can be adjusted)
keystore.keys."zfs/${dataset}" = lib.mkIf (type == "remote" && config.${setup}.keystore.enable) (lib.mkOptionDefault "random"); # the entire point of ZFS remote are backups, and those should be encrypted
fs.zfs.datasets = {
zfs.datasets = {
${dataset} = {
mount = false; props = { canmount = "off"; mountpoint = "/"; } // (if type == "temp" then { refreservation = "1G"; } // zfsNoSyncProps else { });
};
@ -361,21 +367,23 @@ in {
"${dataset}/root" = {
mount = true; props = { canmount = "noauto"; mountpoint = "/"; }; mode = "755";
};
} else { }) // (lib.wip.mapMerge (target: { source, options, zfsProps, uid, gid, mode, ... }: {
} else { }) // (lib.fun.mapMerge (target: { source, options, zfsProps, uid, gid, mode, ... }: {
"${dataset}/${source}" = {
mount = if (options.noauto or false) == true then "noauto" else true; inherit uid gid mode;
props = { canmount = "noauto"; mountpoint = target; } // zfsProps;
};
} // (
lib.wip.mapMerge (prefix: if (lib.any (_:_.source == prefix) (lib.attrValues cfg.${type}.mounts)) then { } else {
lib.fun.mapMerge (prefix: if (lib.any (_:_.source == prefix) (lib.attrValues cfg.${type}.mounts)) then { } else {
"${dataset}/${prefix}" = lib.mkDefault { props.canmount = "off"; };
}) (lib.wip.parentPaths source)
}) (lib.fun.parentPaths source)
)) cfg.${type}.mounts);
};
fileSystems = lib.mapAttrs (target: args@{ extraFsConfig, ... }: extraFsConfig // {
options = (extraFsConfig.options or [ ]) ++ (optionsToList (cfg.${type}.mountOptions // args.options));
}) ((if type == "temp" then { "/" = { options = { }; extraFsConfig = { }; }; } else { }) // cfg.${type}.mounts);
fileSystems = lib.mapAttrs (target: args: (lib.mkMerge ((
map (def: def.${target}.extraFsConfig or { }) opts.${type}.mounts.definitions
) ++ [ (rec {
options = optionsToList (cfg.${type}.mountOptions // args.options);
}) ]))) ((if type == "temp" then { "/" = { options = { }; }; } else { }) // cfg.${type}.mounts);
}))) [ "temp" "local" "remote" ])));

View File

@ -10,13 +10,13 @@ Additionally, this module sets some defaults for ZFS (but only in a "always bett
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, options, pkgs, lib, ... }: let inherit (inputs.self) lib; in let
cfg = config.${prefix}.fs.zfs;
prefix = inputs.config.prefix;
dirname: inputs: { config, options, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let
inherit (inputs.config.rename) setup;
cfg = config.${setup}.zfs;
hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName);
in let module = {
options.${prefix} = { fs.zfs = {
options.${setup} = { zfs = {
enable = lib.mkEnableOption "NixOS managed ZFS pools and datasets";
pools = lib.mkOption {
@ -70,16 +70,16 @@ in let module = {
networking.hostId = lib.mkDefault (builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName)); # ZFS requires one, so might as well set a default.
## Implement »cfg.datasets.*.mount«:
fileSystems = lib.wip.mapMerge (path: { props, mount, ... }: if mount != false then {
fileSystems = lib.fun.mapMerge (path: { props, mount, ... }: if mount != false then {
"${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); };
} else { }) cfg.datasets;
## Load keys (only) for (all) datasets that are declared as encryption roots and aren't disabled:
boot.zfs.requestEncryptionCredentials = lib.attrNames (lib.filterAttrs (name: { props, ... }: if props?keylocation then props.keylocation != "file:///dev/null" else config.${prefix}.fs.keystore.keys?"zfs/${name}") cfg.datasets);
boot.zfs.requestEncryptionCredentials = lib.attrNames (lib.filterAttrs (name: { props, ... }: if props?keylocation then props.keylocation != "file:///dev/null" else config.${setup}.keystore.keys?"zfs/${name}") cfg.datasets);
${prefix} = {
${setup} = {
# Set default root dataset properties for every pool:
fs.zfs.datasets = lib.mapAttrs (name: { ... }: { props = {
zfs.datasets = lib.mapAttrs (name: { ... }: { props = {
# Properties to set at the root dataset of the root pool at its creation. All are inherited by default, but some can't be changed later.
devices = lib.mkOptionDefault "off"; # Don't allow character or block devices on the file systems, where they might be owned by non-root users.
setuid = lib.mkOptionDefault "off"; # Don't allow suid binaries (NixOS puts them in »/run/wrappers/«).
@ -95,12 +95,12 @@ in let module = {
}; }) 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 (but only if the keystore is not disabled):
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));
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.fun.startsWith "file:///run/keystore-${hash}/" props.keylocation else config.${setup}.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):
fs.disks.partitions = lib.wip.mapMergeUnique (name: { ${name} = { # (This also implicitly ensures that no partition is used twice for zpools.)
disks.partitions = lib.fun.mapMergeUnique (name: { ${name} = { # (This also implicitly ensures that no partition is used twice for zpools.)
type = lib.mkDefault "bf00"; size = lib.mkOptionDefault null; order = lib.mkDefault 500;
}; }) (lib.wip.filterMismatching ''/|^(mirror|raidz[123]?|draid[123]?.*|spare|log|dedup|special|cache)$'' (lib.concatLists (lib.catAttrs "vdevArgs" (lib.attrValues cfg.pools))));
}; }) (lib.fun.filterMismatching ''/|^(mirror|raidz[123]?|draid[123]?.*|spare|log|dedup|special|cache)$'' (lib.concatLists (lib.catAttrs "vdevArgs" (lib.attrValues cfg.pools))));
};
@ -156,14 +156,14 @@ in let module = {
poolNames = filterBy: lib.attrNames (lib.filterAttrs (name: pool: pool.${filterBy}) cfg.pools);
filter = pool: "^${pool}($|[/])";
ensure-datasets = zfsPackage: pkgs.writeShellScript "ensure-datasets" ''
${lib.wip.substituteImplicit { inherit pkgs; scripts = lib.attrValues { inherit (lib.wip.setup-scripts) zfs utils; }; context = { inherit config; native = pkgs // { zfs = zfsPackage; }; }; }}
${lib.fun.substituteImplicit { inherit pkgs; scripts = lib.attrValues { inherit (lib.self.setup-scripts) zfs utils; }; context = { inherit config; native = pkgs // { zfs = zfsPackage; }; }; }}
set -eu ; ensure-datasets "$@"
'';
ensure-datasets-for = filterBy: zfsPackage: ''( if [ ! "''${IN_NIXOS_ENTER:-}" ] && [ -e ${zfsPackage}/bin/zfs ] ; then
${lib.concatStrings (map (pool: ''
expected=${lib.escapeShellArg (builtins.toJSON (lib.mapAttrs (n: v: v.props // (if v.permissions != { } then { ":permissions" = v.permissions; } else { })) (lib.filterAttrs (path: _: path == pool || lib.wip.startsWith "${pool}/" path) cfg.datasets)))}
if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${prefix}:applied-datasets ${pool})" != "$expected" ] ; then
${ensure-datasets zfsPackage} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${prefix}:applied-datasets="$expected" ${pool}
expected=${lib.escapeShellArg (builtins.toJSON (lib.mapAttrs (n: v: v.props // (if v.permissions != { } then { ":permissions" = v.permissions; } else { })) (lib.filterAttrs (path: _: path == pool || lib.fun.startsWith "${pool}/" path) cfg.datasets)))}
if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${setup}:applied-datasets ${pool})" != "$expected" ] ; then
${ensure-datasets zfsPackage} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${setup}:applied-datasets="$expected" ${pool}
fi
'') (poolNames filterBy))}
fi )'';
@ -173,7 +173,7 @@ in let module = {
${ensure-datasets-for "autoApplyDuringBoot" extraUtils}
'');
boot.initrd.supportedFilesystems = lib.mkIf (anyPool "autoApplyDuringBoot") [ "zfs" ];
${prefix}.fs.zfs.extraInitrdPools = (poolNames "autoApplyDuringBoot");
${setup}.zfs.extraInitrdPools = (poolNames "autoApplyDuringBoot");
system.activationScripts.A_ensure-datasets = lib.mkIf (anyPool "autoApplyOnActivation") {
text = ensure-datasets-for "autoApplyOnActivation" (pkgs.runCommandLocal "booted-system-link" { } ''ln -sT /run/booted-system/sw $out''); # (want to use the version of ZFS that the kernel module uses, also it's convenient that this does not yet exist during activation at boot)
@ -183,7 +183,7 @@ in let module = {
}) ])) (
# 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);
${setup}.zfs.enable = lib.mkIf config.virtualisation.useDefaultFilesystems (lib.mkVMOverride false);
}
) ];

View File

@ -29,7 +29,7 @@ nix run .../nixos-config'#'nixosConfigurations.${hostName}.config.system.build.v
```nix
#*/# end of MarkDown, beginning of NixOS module:
dirname: inputs: { config, options, pkgs, lib, modulesPath, extendModules, ... }: let inherit (inputs.self) lib; in let
dirname: inputs: { config, options, pkgs, lib, modulesPath, extendModules, ... }: let lib = inputs.self.lib.__internal__; in let
prefix = inputs.config.prefix;
cfg = config.virtualisation.vmVariantExec;
in let hostModule = {
@ -52,7 +52,7 @@ in let hostModule = {
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"}
${lib.fun.extractBashFunction (builtins.readFile lib.self.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}" )
@ -85,6 +85,8 @@ in let hostModule = {
_file = "${dirname}/vm-exec.nix.md#vmModule";
imports = [ ({
virtualisation.graphics = false;
# Instead of tearing down the initrd environment, adjust some mounts and run the »command« in the initrd:
boot.initrd.postMountCommands = ''
@ -97,7 +99,7 @@ in let hostModule = {
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«):
# Nix insists on setting the ownership of »/nix/store« to »0:30000« (if run as root(?) and the current ownership 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
@ -107,7 +109,7 @@ in let hostModule = {
ln -sfT $toplevel /run/booted-system
ln -sfT $toplevel/kernel-modules/lib/modules /lib/modules
# ALso mostly dor debugging shells:
# 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
@ -135,13 +137,14 @@ in let hostModule = {
}) ({
virtualisation.writableStore = false;
fileSystems = lib.mkVMOverride {
"/nix/var/nix/db.lower" = {
fsType = "9p"; device = "nix-var-nix-db"; neededForBoot = true;
options = [ "trans=virtio" "version=9p2000.L" "msize=4194304" "ro" ];
};
"/nix/store".options = lib.mkAfter [ "ro" "msize=4194304" ];
"/nix/store".mountPoint = "/nix/store.lower";
"/nix/store".mountPoint = lib.mkForce "/nix/store.lower";
}; # mount -t 9p -o trans=virtio -o version=9p2000.L -o msize=4194304 nix-var-nix-db /nix/var/nix/db
virtualisation.qemu.options = [ "-virtfs local,path=/nix/var/nix/db,security_model=none,mount_tag=nix-var-nix-db,readonly=on" ]; # (doing this manually to pass »readonly«, to not ever corrupt the host's Nix DBs)

View File

@ -1,104 +0,0 @@
# NixOS Overlays
Nix(OS) manages its packages in a global attribute set, mostly referred to as `nixpkgs` (as repository/sources) or simply as `pkgs` (when evaluated).
Overlays are a mechanism to add or replace packages in that attribute set, such that wherever else they are referenced (e.g. as `pkg.<package>`) the added/replaced version is used.
Any number of overlays can be applied in sequence when instantiating/evaluating `nixpkgs` into `pkgs`.
Each overlay is a function with two parameters returning an attrset which is merged onto `pkgs`.
The first parameter (called `final`) is the `pkgs` as it will result after applying all overlays. This works because of nix's lazy evaluation, but accessing attributes that are based on the result of the current overlay will logically cause unresolvable recursions.
For that reason, the second parameter `prev` is the version of `pkgs` from before applying the overlay.
As a general guideline, use `final` where possible (to avoid consuming unpatched packages) and `prev` only when necessary to avoid recursions.
`prev` thus gives access to the packages being overridden and allows (the build instructions for) the overriding package to be based off the unmodified package.
Most packages in `nixpkgs` are constructed using something like `callPackage ({ ...args }: mkDerivation { ...attributes }) { ...settings }`, where `callPackage` is usually in `all-packages.nix` and imports the code in the parentheses from a different file.
Passed by `callPackage`, `args` includes `pkgs` plus optionally the `settings` to the package.
The `attributes` are then based on local values and packages and settings from `args`.
Any package built that way then has two functions which allow overlays (or code elsewhere) to define modified versions of that package:
* `.overwrite` is a function taking an attrset that is merged over `args` before re-evaluation the package;
* `.overrideAttrs` is a function from the old `attributes` to ones that are merged over `attributes` before building the derivation.
Using the above mechanisms, each file in this folder adds and/or modifies one or more packages to/in `pkgs`.
[`./default.nix`](./default.nix) exports all overlays as an attribute set; [`flake#outputs.packages.<arch>.*`](../flake.nix), exports all packages resulting from the overlays.
## Template/Examples
Here is a skeleton structure / collection of examples for writing a new `<overlay>.nix.md`:
````md
/*
# TODO: title
TODO: documentation
## Implementation
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in {
# e.g.: add a patched version of a package (use the same name to replace)
systemd-patched = prev.systemd.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
../patches/systemd-....patch
];
});
# e.g.: add a prebuilt program as package
qemu-aarch64-static = pkgs.stdenv.mkDerivation {
name = "qemu-aarch64-static";
src = builtins.fetchurl {
url = "https://github.com/multiarch/qemu-user-static/releases/download/v6.1.0-8/qemu-aarch64-static";
sha256 = "075l122p3qfq6nm07qzvwspmsrslvrghar7i5advj945lz1fm6dd";
}; dontUnpack = true;
installPhase = "install -D -m 0755 $src $out/bin/qemu-aarch64-static";
};
# e.g.: update (or pin the version of) a package
raspberrypifw = prev.raspberrypifw.overrideAttrs (old: rec {
version = "1.20220308";
src = pkgs.fetchFromGitHub {
owner = "raspberrypi"; repo = "firmware"; rev = version;
sha256 = "sha256-pwhI9sklAGq5+fJqQSadrmW09Wl6+hOFI/hEewkkLQs=";
};
});
# e.g.: add a program as new package
udptunnel = pkgs.stdenv.mkDerivation rec {
pname = "udptunnel"; version = "1"; # (not versioned)
src = pkgs.fetchFromGitHub {
owner = "rfc1036"; repo = pname; rev = "482ed94388a0dde68561584926c7d5c14f079f7e"; # 2018-11-18
sha256 = "1wkzzxslwjm5mbpyaq30bilfi2mfgi2jqld5l15hm5076mg31vp7";
};
patches = [ ../patches/....patch ];
installPhase = ''
mkdir -p $out/bin $out/share/udptunnel
cp -T udptunnel $out/bin/${pname}
cp COPYING $out/share/udptunnel
'';
meta = {
homepage = "https://github.com/rfc1036/udptunnel";
description = "Tunnel UDP packets in a TCP connection ";
license = lib.licenses.gpl2;
maintainers = [ ];
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

@ -1 +1 @@
dirname: inputs@{ self, nixpkgs, ...}: self.lib.wip.importOverlays inputs dirname { }
dirname: inputs@{ self, nixpkgs, ...}: self.lib.__internal__.fun.importOverlays inputs dirname { }

View File

@ -10,7 +10,7 @@ GPT-FDisk patched to be able to move not only the primary, but also the backup p
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
inherit (final) pkgs; lib = inputs.self.lib.__internal__;
debug = false;
in {

View File

@ -1,46 +0,0 @@
/*
# `libubootenv` - Library to access U-Boot environment
As an `environment.systemPackages` entry this provides the `fw_printenv` / `fw_setenv` commands to work with U-Boot's environment variables.
## Example
Assuming `/dev/disk/by-partlabel/config-${...}` is placed at the same location that U-Boot was configured (via `CONFIG_ENV_OFFSET` and `CONFIG_ENV_SIZE`) to expect/save the environment:
```nix
{
environment.systemPackages = [ pkgs.libubootenv ];
environment.etc."fw_env.config".text = "/dev/disk/by-partlabel/config-${...} 0x0 0x${lib.concatStrings (map toString (lib.toBaseDigits 16 envSize))}";
}
```
## Implementation
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in {
libubootenv = pkgs.stdenv.mkDerivation rec {
pname = "libubootenv"; version = "0.3.2";
src = pkgs.fetchFromGitHub {
owner = "sbabic"; repo = pname; rev = "ba7564f5006d09bec51058cf4f5ac90d4dc18b3c"; # 2018-11-18
hash = "sha256-6cHkr3s7/2BVXBTn9bUfPFbYAfv9VYh6C9GAbWILNjs=";
};
nativeBuildInputs = [ pkgs.buildPackages.cmake ];
buildInputs = [ pkgs.zlib ];
outputs = [ "out" "lib" ];
meta = {
homepage = "https://github.com/sbabic/libubootenv";
description = "Generic library and tools to access and modify U-Boot environment from User Space";
license = [ lib.licenses.lgpl21Plus lib.licenses.mit lib.licenses.cc0 ];
maintainers = [ ];
platforms = lib.platforms.linux;
};
};
}

View File

@ -1,7 +0,0 @@
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

@ -1,18 +0,0 @@
# 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

@ -1,59 +0,0 @@
/*
# U-Boot with Env Compiled and on MMC
A function reconfiguring an u-boot package to save its env on MMC (e.g. internal boot storage or microSD) if it doesn't already, and to compile a custom default env into u-boot itself.
## Implementation
```nix
#*/# end of MarkDown, beginning of NixPkgs overlay:
dirname: inputs: final: prev: let
inherit (final) pkgs; inherit (inputs.self) lib;
in {
uboot-with-mmc-env = {
base ? null,
# The (maximum) total size, position (each in in bytes), and MMC device number of the u-boot env that can be set later:
envSize ? base.envSize or 16384,
envOffset ? base.envOffset or 4194304,
envMmcDev ? 1,
# Default boot variables for u-boot. This amends the variables that will be compiled into u-boot as the default env, but also is the base for the variables written by the ».mkEnv« function. As such, this should include everything necessary to boot something:
defaultEnv ? ({ }), # $src/scripts/get_default_env.sh can read this again
# Lines to append to the u-boot config:
extraConfig ? [ ],
}: base.overrideAttrs (old: let
envTxt = env: pkgs.writeText "uboot-env.txt" "${lib.concatStrings (lib.mapAttrsToList (k: v: if v == null then "" else "${k}=${toString v}\n") env)}";
defaultEnv' = (base.defaultEnv or { }) // defaultEnv;
in {
passthru = (old.passthru or { }) // {
inherit envSize envOffset; defaultEnv = defaultEnv';
# Creates a (user) env blob for this u-boot by merging »env« over its »defaultEnv«. The resulting file can be flashed to »CONFIG_ENV_OFFSET« to replace the default env.
mkEnv = env: pkgs.runCommandLocal "uboot-env.img" {
env = envTxt (defaultEnv' // env);
} "${pkgs.buildPackages.ubootTools}/bin/mkenvimage -p 0x00 -s ${toString envSize} -o $out $env";
};
extraConfig = (old.extraConfig or "") + "${lib.concatStringsSep "\n" ([
# (these need to be passed as 0x<hex>:)
"CONFIG_ENV_OFFSET=0x${lib.concatStrings (map toString (lib.toBaseDigits 16 envOffset))}"
"CONFIG_ENV_SIZE=0x${lib.concatStrings (map toString (lib.toBaseDigits 16 envSize))}"
# Ensure that env is configured to be stored on MMC(/microSD):
"CONFIG_ENV_IS_IN_MMC=y" "CONFIG_SYS_MMC_ENV_DEV=${toString envMmcDev}" # (not sure this is enough)
# CONFIG_EXTRA_ENV_SETTINGS here would be overwritten, and CONFIG_DEFAULT_ENV_FILE replaces some basics that should be kept.
] ++ extraConfig)}\n";
CONFIG_EXTRA_ENV_SETTINGS = ''${lib.concatMapStringsSep ''"\0"'' builtins.toJSON (lib.mapAttrsToList (k: v: if v == null then "" else ''${k}=${toString v}'') defaultEnv')}"\0"''; # (this is in addition to whatever u-boot derives from its other CONFIG_*)
postConfigure = (old.postConfigure or "") + ''
# Set CONFIG_EXTRA_ENV_SETTINGS just before it's used, to make sure it actually applies:
printf "%s\n%s\n" "#define CONFIG_EXTRA_ENV_SETTINGS $CONFIG_EXTRA_ENV_SETTINGS" "$(cat include/env_default.h)" >include/env_default.h
'';
});
ubootTools = prev.ubootTools.overrideAttrs (old: {
buildInputs = (old.buildInputs or [ ]) ++ [ final.openssl ];
});
}

View File

@ -1,51 +0,0 @@
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

@ -1,23 +0,0 @@
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index c07567ec..ecd69f04 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -33,16 +33,14 @@ let
netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
- empty_file=$(${pkgs.coreutils}/bin/mktemp)
-
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n}
- ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
+ ${pkgs.coreutils}/bin/install -D /dev/null "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n}
'') cfg.extraFiles)}
${concatStrings (mapAttrsToList (n: v: ''
${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n}
- ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
+ ${pkgs.coreutils}/bin/install -D /dev/null "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n}
'') cfg.extraEntries)}
'';
};

View File

@ -1,12 +0,0 @@
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 05174e03754..b4620ee1715 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -100,6 +100,7 @@ let
getKeyLocations = pool:
if isBool cfgZfs.requestEncryptionCredentials
then "${cfgZfs.package}/sbin/zfs list -rHo name,keylocation,keystatus ${pool}"
+ else if (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials) == [ ] then ":"
else "${cfgZfs.package}/sbin/zfs list -Ho name,keylocation,keystatus ${toString (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials)}";
createImportService = { pool, systemd, force, prefix ? "" }:

View File

@ -1,14 +0,0 @@
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index 0c93b288..d26d630a 100644
# For systems with BIOS boot, allow specifying the grub device by partition (e.g. by partlabel).
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -725,6 +725,8 @@ symlink "$bootPath", "$tmpDir/boot" or die "Failed to symlink $tmpDir/boot: $!";
if (($requireNewInstall != 0) && ($efiTarget eq "no" || $efiTarget eq "both")) {
foreach my $dev (@deviceTargets) {
next if $dev eq "nodev";
+ my $realpath = qx{realpath $dev};
+ if ($realpath =~ m;^/dev/(sd[a-z](?=\d+\s$)|\w+\d+(?=p\d+\s$));) { $dev = "/dev/$1"; }
print STDERR "installing the GRUB $grubVersion boot loader on $dev...\n";
my @command = ("$grub/sbin/grub-install", "--recheck", "--root-directory=$tmpDir", Cwd::abs_path($dev), @extraGrubInstallArgs);
if ($forceInstall eq "true") {

View File

@ -1,13 +0,0 @@
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 [];

View File

@ -1,7 +0,0 @@
diff --git a/patched! b/patched!
new file mode 100644
index 0000000..e69de29
--- a/patched!
+++ b/patched!
@@ -1,0 +1,1 @@
+yay