The Complete Computing Environment

NixOS Automatic Partitioning Installer

LifeTechEmacsTopicsArcology

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

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

DONE rewrite from kexec to iso installation procedures

NEXT pull public key out of SSH Configuration

NEXT document my deviations from the upstream justdoit.nix

NEXT bootstrap home-manager on My NixOS configuration

NEXT bootstrap wireguard

NEXT bootstrap backup and restore