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 frustrating1, 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).
{ 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
false;
networking.wireless.enable = 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:
"\n"
lib.concatStringsSep (["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:
"\n"
lib.concatStringsSep (["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.zfsPoolszfsPoolMountCommands =
"\n"
concatStringsSep (map (pair: pair.theCommand)
(mkZfsPoolMountCommands mkMountCmd).result);
mkUmountCmd = (poolName: volName: vol:
{sortKey = "/mnt${vol.mountPoint}";
theCommand = "umount /mnt${vol.mountPoint}";});
zfsPoolUmountCommands =
"\n"
concatStringsSep (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" [2021-04-03 Sat 02:20]
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" [2021-04-05 Mon 02:02]
- Note taken on [2021-04-02 Fri 21:53]
I'm not sure how dynamic configuration.nix generation will compose with nixops - State "WAITING" from "NEXT" [2021-04-02 Fri 21:53]
DONE rewrite from kexec to iso installation procedures
- State "DONE" from "NEXT" [2021-04-05 Mon 02:02]