There are a few "Supported" methods for installing NixOS: you can use the installer image to manually partition and prepare a system, you can LUSTRATE an existing Linux installation with Nix installed on it, or you can use a kexec script generally referred to as justdoit which is distributed as a script which boots the system in to a NixOS system with a partitioning script which blindly partitions the rootDevice and installs NixOS on to that partition. I spent some time this month trying to get the kexec method working and it was pretty frustrating. The auto-partitioner and the easy setup through kexec.justdoit is nice, but getting the kexec working out of the box was frustrating[fn:1:nothing worth complaining about: fedora couldn't kexec the images, fedora couldn't expand the images without doing =TMPDIR=/var/tmp= because tmp is a ramdisk... my mongrel Fedora Linux plus home-manager environment struggled to get virtualization running, and the testing cycle was pretty painful. The repository I based this around was broken OOTB and even the testing scripts included in it didn't work reliably. The justdoit itself took a fair bit of tweaking to get to booting, too. I'll reintegrate kexec some day.], so for now I'll stick with ISO and ISO-as-SD with the justdoit work included.
To build an ISO image: pushd ~/arroyo-nix/kexec && nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage -I nixos-config=configuration.nix & The file will get symlinked to ~/arroyo-nix/kexec/result/iso.
My NixOS configuration
Configuring the Installer
This record describes a basic NixOS installation which runs an installer script and can optionally be configured to automatically reboot (see below).
{ lib, pkgs, config, ... }:
let
overlayFn = import <arroyo/overlay.nix>;
pkgs' = overlayFn pkgs {};
in with lib;
{
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
./kexec.nix ./justdoit.nix
];
nixpkgs.overlays = [
(overlayFn)
];
boot.supportedFilesystems = [ "zfs" ];
boot.loader.grub.enable = false;
boot.kernelParams = [
"console=ttyS0,115200" # allows certain forms of remote access, if the hardware is setup right
"panic=30" "boot.panic_on_fail" # reboot the machine upon fatal boot issues
# "fbcon=rotate:1" # rotate display in framebuffer
];
systemd.services.sshd.wantedBy = mkForce [ "multi-user.target" ];
networking.hostName = "kexec";
# hahaha! yes!
users.users.root.openssh.authorizedKeys.keys = pkgs'.lib.publicKeys.rrix;
# hardware.video.hidpi.enable = true;
# services.xserver.dpi = 280;
isoImage = {
includeSystemBuildDependencies = false;
makeUsbBootable = true;
};
kexec.justdoit = {
hostName = "virtuous-cassette";
rootDevice = "/dev/nvme0n1";
# swapSize = 16384;
swapSize = 8192;
poolName = "host";
bootType = "vfat";
bootSize = 2048;
wifiName = "TsukiNoMayu";
luksEncrypt = true;
uefi = true;
nvme = true;
zfsPools = {
host = {
devices = "$ROOT_DEVICE";
volumes = {
root = {
snapshot = false;
compression = false;
mountPoint = "/";
};
home = {
snapshot = true;
compression = true;
mountPoint = "/home";
};
landfill = {
snapshot = true;
compression = true;
mountPoint = "/media";
};
nix = {
snapshot = false;
compression = false;
mountPoint = "/nix";
};
};
};
};
};
}Minimal target-config.nix
A subset of My NixOS configuration, enough to get the rest Morph deployed to it. Head over there for in-depth discussion.
{ config, pkgs, ... }:
{
imports = [ ./hardware-configuration.nix ./generated.nix ];
# boot.loader.systemd-boot.enable = true;
# boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;
# boot.kernelPackages = pkgs.linuxPackages_5_15;
services.openssh.enable = true;
boot.zfs.forceImportRoot = true;
boot.kernelParams = [
"boot.shell_on_fail"
];
# less nix crap
nix.gc.automatic = true;
nix.gc.dates = "23:30";
# system clock
time.timeZone = "America/Los_Angeles";
users.groups.humans = {
name = "humans";
gid = 1000;
};
users.users.rrix = {
isNormalUser = true;
home = "/home/rrix";
description = "Ryan Rix";
extraGroups = [ "wheel" "networkmanager" "adbusers" ];
uid = 1000;
group = "humans";
initialPassword = "changeme!";
openssh.authorizedKeys.keys = <<get_ssh_pubkey()>>
};
users.users.root.openssh.authorizedKeys.keys = <<get_ssh_pubkey()>> # (ref:gen_call)
# hardware.video.hidpi.enable = true;
# networking
networking.wireless.enable = false;
networking.networkmanager.enable = true;
# hahaha! yes
nixpkgs.config = { allowUnfree = true; };
# power management
powerManagement.enable = true;
environment.systemPackages = (with pkgs.libsForQt5; [
pkgs.vim
]);
}That noweb call (gen_call) gets my public ssh key this way. i should and could and will define this in my SSH Configuration:
[ -f ~/.ssh/id_rsa.pub ] && \
cat ~/.ssh/id_rsa.pub \
| awk 'BEGIN {print "["} {print "\"" $1 " " $2 "\"" } END {print "];"}' \ (ref:tr)
| tr \\n " " LMAO there must be a way to get (tr)'s awk output on to one line but ORS doesn't do what i expect...
NEXT Would be pretty neat to add a bootstrap script here...
open up a konsole and run bootstrap or bootstrap-local to get a backup restored on to the machine? bootstrap-push might be better but a bootstrap script is so alluring!
config.system.build.justdoit formats and installs a system
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.kexec.justdoit;
x = if cfg.nvme then "p" else "";
in {
options = {
kexec.justdoit = {
hostName = mkOption {
type = types.str;
description = "set the networking.hostName of the installed system";
};
rootDevice = mkOption {
type = types.str;
default = "/dev/sda";
description = "the root block device that justdoit will nuke from orbit and force nixos onto";
};
bootSize = mkOption {
type = types.int;
default = 256;
description = "size of /boot in mb";
};
bootType = mkOption {
type = types.enum [ "ext4" "vfat" "zfs" ];
default = "ext4";
};
swapSize = mkOption {
type = types.int;
default = 1024;
description = "size of swap in mb";
};
poolName = mkOption {
type = types.str;
default = "tank";
description = "zfs pool name";
};
luksEncrypt = mkOption {
type = types.bool;
default = false;
description = "encrypt all of zfs and swap";
};
uefi = mkOption {
type = types.bool;
default = false;
description = "create a uefi install";
};
nvme = mkOption {
type = types.bool;
default = false;
description = "rootDevice is nvme";
};
zfsPools = mkOption {
type = types.attrs;
default = {
"${cfg.poolName}" = {
devices = "$ROOT_DEVICE";
volumes = {
nix = { mountPoint = "/nix"; };
root = { mountPoint = "/"; };
};
};
};
description = "Extra ZFS pools to create and mount";
example = {
kiddypool = {
devices = "$ROOT_DEVICE";
volumes = {
nix = {
snapshot = false;
compression = false;
mountPoint = "/nix";
};
root ={
snapshot = false;
compression = "lz4";
mountPoint = "/";
};
};
};
deepend = {
devices = "mirror /dev/sda /dev/sdb mirror /dev/sdc /dev/sdd";
volumes = {
home = {
snapshot = true;
compression = "lz4";
mountPoint = "/home";
};
media = {
snapshot = true;
compression = false;
mountPoint = "/media";
};
};
};
};
};
wifiName = mkOption {
type = types.str;
default = "";
description = "Name of wifi network to connect to";
};
};
};
config = let
mkBootTable = {
ext4 = "mkfs.ext4 $NIXOS_BOOT -L NIXOS_BOOT";
vfat = "mkfs.vfat $NIXOS_BOOT -n NIXOS_BOOT";
zfs = "";
};
mkZfsCreateCmd =
(poolName: volName: vol:
"zfs create ${lib.optionalString (lib.isString vol.compression) "-o compression=${vol.compression}"} -o mountpoint=legacy ${poolName}/${volName}");
zfsPoolSetup = concatStringsSep "\n"
(lib.mapAttrsToList
(poolName: pool:
lib.concatStringsSep "\n"
(["zpool create -f -o altroot=/mnt/${poolName} ${poolName} ${pool.devices}"] ++
(lib.mapAttrsToList (mkZfsCreateCmd poolName) pool.volumes)))
cfg.zfsPools);
mkSnapshotCmd = (poolName: volName: vol:
"zfs set com.sun:auto-snapshot=${boolToString vol.snapshot} ${poolName}/${volName}");
zfsPoolSnapshotRules = concatStringsSep "\n"
(lib.mapAttrsToList
(poolName: pool:
lib.concatStringsSep "\n"
(["zfs set com.sun:auto-snapshot=false ${poolName}"] ++
(lib.mapAttrsToList (mkSnapshotCmd poolName) pool.volumes)))
cfg.zfsPools);
# mounts need to be sorted so that /mnt doesn't occlude /mnt/home etc later on...
mkMountCmd = (poolName: volName: vol:
{sortKey = "/mnt${vol.mountPoint}";
theCommand = "mkdir -p /mnt${vol.mountPoint} && mount -t zfs ${poolName}/${volName} /mnt${vol.mountPoint}";});
mkZfsPoolMountCommands = cmd:
(lists.toposort (p1: p2: hasPrefix p1.sortKey p2.sortKey)
(flatten
(mapAttrsToList
(poolName: pool:
(mapAttrsToList (cmd poolName) pool.volumes))
cfg.zfsPools)));
zfsPoolMountCommands =
concatStringsSep "\n"
(map (pair: pair.theCommand)
(mkZfsPoolMountCommands mkMountCmd).result);
mkUmountCmd = (poolName: volName: vol:
{sortKey = "/mnt${vol.mountPoint}";
theCommand = "umount /mnt${vol.mountPoint}";});
zfsPoolUmountCommands =
concatStringsSep "\n"
(reverseList
(map (pair: pair.theCommand)
(mkZfsPoolMountCommands mkUmountCmd).result));
mkExportCmd = (poolName: ''
zfs set cachefile=none ${poolName}
zfs export ${poolName}
'');
zfsExportCmd = concatStringsSep "\n"
(lib.mapAttrsToList
(poolName: pool:
mkExportCmd poolName)
cfg.zfsPools);
in lib.mkIf true {
system.build.justdoit = pkgs.writeScriptBin "justdoit" ''
#!${pkgs.stdenv.shell}
set -e
${lib.optionalString (cfg.wifiName != "") ''
echo Connecting to "${cfg.wifiName}" ...
nmcli dev wifi connect --ask "${cfg.wifiName}"
''}
vgchange -a n
wipefs -a ${cfg.rootDevice}
dd if=/dev/zero of=${cfg.rootDevice} bs=512 count=10000
sfdisk ${cfg.rootDevice} <<EOF
label: gpt
device: ${cfg.rootDevice}
unit: sectors
${lib.optionalString (cfg.bootType != "zfs") "1 : size=${toString (2048 * cfg.bootSize)}, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B"}
${lib.optionalString (! cfg.uefi) "4 : size=4096, type=21686148-6449-6E6F-744E-656564454649"}
2 : size=${toString (2048 * cfg.swapSize)}, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F
3 : type=0FC63DAF-8483-4772-8E79-3D69D8477DE4
EOF
${if cfg.luksEncrypt then ''
cryptsetup luksFormat ${cfg.rootDevice}${x}2
cryptsetup open --type luks ${cfg.rootDevice}${x}2 swap
cryptsetup luksFormat ${cfg.rootDevice}${x}3
cryptsetup open --type luks ${cfg.rootDevice}${x}3 root
export ROOT_DEVICE=/dev/mapper/root
export SWAP_DEVICE=/dev/mapper/swap
'' else ''
export ROOT_DEVICE=${cfg.rootDevice}${x}3
export SWAP_DEVICE=${cfg.rootDevice}${x}2
''}
${lib.optionalString (cfg.bootType != "zfs") "export NIXOS_BOOT=${cfg.rootDevice}${x}1"}
${mkBootTable.${cfg.bootType}}
mkswap $SWAP_DEVICE -L NIXOS_SWAP
${lib.optionalString (cfg.zfsPools != {}) zfsPoolSetup}
${lib.optionalString (cfg.zfsPools != {}) zfsPoolSnapshotRules}
swapon $SWAP_DEVICE
${lib.optionalString (cfg.zfsPools != {}) zfsPoolMountCommands}
mkdir -p /mnt/boot
${lib.optionalString (cfg.bootType != "zfs") "mount $NIXOS_BOOT /mnt/boot/"}
nixos-generate-config --root /mnt/
hostId=$(echo $(head -c4 /dev/urandom | od -A none -t x4))
cp ${./target-config.nix} /mnt/etc/nixos/configuration.nix
cat > /mnt/etc/nixos/generated.nix <<EOF
{ ... }:
{
${lib.optionalString (cfg.hostName != "") "networking.hostName = \"${cfg.hostName}\";"}
${if cfg.uefi then ''
# boot.loader.grub.efiInstallAsRemovable = true;
boot.loader.grub.efiSupport = true;
boot.loader.grub.device = "nodev";
boot.loader.efi.canTouchEfiVariables = true;
boot.loader.grub.enable = true;
# boot.loader.systemd-boot.enable = true;
# boot.loader.systemd-boot.consoleMode = "max";
'' else '' #
boot.loader.grub.enable = true;
boot.loader.grub.device = "${cfg.rootDevice}";
''}
networking.hostId = "$hostId"; # required for zfs use
${if cfg.luksEncrypt then ''
boot.zfs.devNodes = "/dev/mapper"; # (ref:devNodes)
boot.initrd.luks.devices = {
"swap" = { name = "swap"; device = "${cfg.rootDevice}${x}2"; preLVM = true; };
"root" = { name = "root"; device = "${cfg.rootDevice}${x}3"; preLVM = true; };
};
'' else ''
boot.zfs.devNodes = "/dev/disk/by-uuid"; # (ref:devNodes)
''}
}
EOF
nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
export NIX_PATH="home-manager=/nix/var/nix/profiles/per-user/root/channels/home-manager/:$NIX_PATH"
nix-channel --update
nixos-install
${lib.optionalString (cfg.bootType != "zfs") "umount /mnt/boot"}
${lib.optionalString (cfg.zfsPools != {}) zfsPoolUmountCommands}
${lib.optionalString (cfg.zfsPools != {}) zfsExportCmd}
swapoff $SWAP_DEVICE
'';
# boot.kernelPackages = pkgs.linuxPackages_5_15;
environment.systemPackages = [ config.system.build.justdoit ];
boot.supportedFilesystems = [ "zfs" ];
# for the children
networking.wireless.enable = false;
networking.networkmanager.enable = true;
};
}DONE all config.zfsPools should be exported
system.build.kexec_tarball produces a kexecutable file
{ pkgs, config, ... }:
{
system.build = rec {
image = pkgs.runCommand "image" { buildInputs = [ pkgs.nukeReferences ]; } ''
mkdir $out
cp ${config.system.build.kernel}/bzImage $out/kernel
cp ${config.system.build.netbootRamdisk}/initrd $out/initrd
echo "init=${builtins.unsafeDiscardStringContext config.system.build.toplevel}/init ${toString config.boot.kernelParams}" > $out/cmdline
nuke-refs $out/kernel
'';
kexec_script = pkgs.writeTextFile {
executable = true;
name = "kexec-nixos";
text = ''
#!${pkgs.stdenv.shell}
export PATH=${pkgs.kexectools}/bin:${pkgs.cpio}/bin:$PATH
set -x
set -e
cd $(mktemp -d)
pwd
mkdir initrd
pushd initrd
if [ -e /ssh_pubkey ]; then
cat /ssh_pubkey >> authorized_keys
fi
find -type f | cpio -o -H newc | gzip -9 > ../extra.gz
popd
cat ${image}/initrd extra.gz > final.gz
kexec -l ${image}/kernel --initrd=final.gz --append="init=${builtins.unsafeDiscardStringContext config.system.build.toplevel}/init ${toString config.boot.kernelParams}"
sync
echo "executing kernel, filesystems will be improperly umounted"
kexec -e
'';
};
};
boot.initrd.postMountCommands = ''
mkdir -p /mnt-root/root/.ssh/
cp /authorized_keys /mnt-root/root/.ssh/
'';
system.build.kexec_tarball = pkgs.callPackage <nixpkgs/nixos/lib/make-system-tarball.nix> {
storeContents = [
{ object = config.system.build.kexec_script; symlink = "/kexec_nixos"; }
];
contents = [];
};
}Work Stream
DONE get justdoit working
State "DONE" from "INPROGRESS"
NEXT include the full installation closure in to the image
NEXT home-manager-alike for nixos configuration.nix generation (??)
DONE basic nixops setup to get the boot configuration stuff parameterized
State "DONE" from "WAITING"
Note taken on \\ I'm not sure how dynamic configuration.nix generation will compose with nixops
State "WAITING" from "NEXT"
DONE rewrite from kexec to iso installation procedures
State "DONE" from "NEXT"