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 frustratingfn1nothing 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: [[shell:pushd ~/arroyo-nix/kexec && nix-build '<nixpkgs/nixos>' -A config.system.build.isoImage -I nixos-config=configuration.nix &]] The file will get symlinked to file:~/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).
nix source: :tangle ~/arroyo-nix/kexec/configuration.nix :noweb yes{ 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.
nix source: :tangle ~/arroyo-nix/kexec/target-config.nix :noweb yes{ 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 :
get_ssh_pubkeyshell source: :results drawer -n[ -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
nix source: :tangle ~/arroyo-nix/kexec/justdoit.nix{ 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
nix source: :tangle ~/arroyo-nix/kexec/kexec.nix{ 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 nixopsState "WAITING" from "NEXT"
DONE rewrite from kexec to iso installation procedures
State "DONE" from "NEXT"