From 336c4267c8142d08e3bb0581102d34952d88cc1b Mon Sep 17 00:00:00 2001 From: Jack O'Sullivan Date: Sat, 31 Aug 2024 23:08:26 +0100 Subject: [PATCH] firmware: Working Pi 3 firmware --- firmware/.envrc | 1 + firmware/.gitignore | 1 + firmware/.keys/.gitignore | 1 + firmware/.keys/management.pub | 1 + firmware/base.nix | 84 ++++++++++++-- firmware/default.nix | 66 ++++++++++- firmware/disk.nix | 205 ++++++++++++++++++++++++++++++++++ firmware/network.nix | 18 +++ firmware/target/rpi3.nix | 89 ++++++++++++++- flake.lock | 16 +++ flake.nix | 2 + 11 files changed, 466 insertions(+), 18 deletions(-) create mode 100644 firmware/.keys/.gitignore create mode 100644 firmware/.keys/management.pub create mode 100644 firmware/disk.nix create mode 100644 firmware/network.nix diff --git a/firmware/.envrc b/firmware/.envrc index 29316ce..2b1ef09 100644 --- a/firmware/.envrc +++ b/firmware/.envrc @@ -1 +1,2 @@ +watch_file default.nix use flake ..#firmware --override-input rootdir "file+file://"<(printf %s "$PWD") diff --git a/firmware/.gitignore b/firmware/.gitignore index c4a847d..f3b12c3 100644 --- a/firmware/.gitignore +++ b/firmware/.gitignore @@ -1 +1,2 @@ /result +/*.img diff --git a/firmware/.keys/.gitignore b/firmware/.keys/.gitignore new file mode 100644 index 0000000..f5d2d1d --- /dev/null +++ b/firmware/.keys/.gitignore @@ -0,0 +1 @@ +/*.key diff --git a/firmware/.keys/management.pub b/firmware/.keys/management.pub new file mode 100644 index 0000000..8d23e6f --- /dev/null +++ b/firmware/.keys/management.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINd+aujm9F06jaQIAvk/0ptQNDHp57J429SqIquVmhbh qclk-management diff --git a/firmware/base.nix b/firmware/base.nix index 16f3ac4..16c164f 100644 --- a/firmware/base.nix +++ b/firmware/base.nix @@ -1,19 +1,85 @@ -{ lib, ... }: +{ lib, pkgs, ... }: let inherit (lib) mkOption; in { - options.my.build = { - image = mkOption { - description = "Final output image for distribution."; - type = lib.types.unspecified; - }; - }; - config = { system = { stateVersion = "24.11"; + nixos = { + distroName = "qCLKOS"; + }; + name = "qclk"; + }; + documentation.nixos.enable = false; + + time.timeZone = "Europe/Dublin"; + i18n.defaultLocale = "en_IE.UTF-8"; + + boot = { + loader = { + grub.enable = false; + }; + initrd = { + systemd = { + enable = true; + emergencyAccess = true; + }; + }; + consoleLogLevel = 7; + }; + + nix = { + # We're a flake-only gal + channel.enable = false; + settings = { + experimental-features = [ "nix-command" "flakes" "ca-derivations" ]; + extra-substituters = [ "https://nix-cache.nul.ie" ]; + extra-trusted-public-keys = [ "nix-cache.nul.ie-1:BzH5yMfF4HbzY1C977XzOxoPhEc9Zbu39ftPkUbH+m4=" ]; + fallback = false; + }; + }; + + users = { + users = { + root = { + openssh.authorizedKeys.keyFiles = [ + .keys/management.pub + ]; + }; + }; + }; + + environment = { + systemPackages = with pkgs; [ + usbutils + tcpdump + + (pkgs.vim.customize { + name = "vim"; + vimrcConfig.packages.default = { + start = [ pkgs.vimPlugins.vim-nix ]; + }; + vimrcConfig.customRC = "syntax on"; + }) + ]; + }; + + programs = { + command-not-found.enable = false; + htop.enable = true; + }; + + services = { + getty.autologinUser = "root"; + + openssh = { + enable = true; + settings = { + PermitRootLogin = "prohibit-password"; + PasswordAuthentication = false; + }; + }; }; }; } - diff --git a/firmware/default.nix b/firmware/default.nix index 5ff893e..23aea00 100644 --- a/firmware/default.nix +++ b/firmware/default.nix @@ -1,8 +1,32 @@ -{ inputs, ... }: +{ self, inputs, ... }: let - mkSystem = target: inputs.nixpkgs.lib.nixosSystem { + nixpkgsLib = inputs.nixpkgs.lib.extend (final: prev: + let + date = final.substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101"); + revCode = flake: flake.shortRev or "dirty"; + in + { + trivial = prev.trivial // { + release = "24.08:u-${prev.trivial.release}"; + codeName = "Alpha"; + revisionWithDefault = default: self.rev or default; + versionSuffix = ".${date}.${revCode self}:u-${revCode inputs.nixpkgs}"; + }; + } + ); + + mkSystem = target: nixpkgsLib.nixosSystem { modules = [ + { + imports = [ + inputs.impermanence.nixosModules.impermanence + ]; + } + ./base.nix + ./disk.nix + ./network.nix + target ]; }; @@ -24,7 +48,43 @@ in nix build "..#nixosConfigurations.qclk-$1.config.system.build.toplevel" ''; build-image.exec = '' - nix build "..#nixosConfigurations.qclk-$1.config.my.build.image" + set -e + export PATH="$PATH:${pkgs.util-linux}/bin:${pkgs.fakeroot}/bin:${pkgs.e2fsprogs}/bin" + die() { + echo "$1" >&2 + exit 1 + } + + [ -z "$1" ] && die "Need to set target" + + target=$1 + out=qclkos-$target.img + + nix build "..#nixosConfigurations.qclk-$target.config.my.disk.image" + + persistRoot=$(mktemp --tmpdir -d qclkos-persist-XXXXX) + # TODO: bless with unique stuff (e.g. keys) + touch "$persistRoot"/test.txt + + cp --sparse=always result/$out $out + chmod u+w $out + + eval $(partx $out -o START,SECTORS --nr 2 --pairs) + persistImg=$(mktemp --tmpdir qclkos-persist-XXXXX.img) + truncate -s $((SECTORS * 512)) $persistImg + fakeroot mkfs.ext4 -L qclkos-persist -d $persistRoot $persistImg + + dd conv=notrunc if=$persistImg of=$out seek=$START count=$SECTORS + rm -r "$persistRoot" "$persistImg" + ''; + + push-config.exec = '' + host=$1; shift + target=$1; shift + verb=$1; shift + + export NIX_SSHOPTS="-i .keys/management.key" + nixos-rebuild $verb --flake ..#qclk-$target --target-host root@"$host" --use-substitutes "$@" ''; }; }; diff --git a/firmware/disk.nix b/firmware/disk.nix new file mode 100644 index 0000000..c862ac0 --- /dev/null +++ b/firmware/disk.nix @@ -0,0 +1,205 @@ +# Based on `nixos/modules/installer/sd-card/sd-image.nix` +{ lib, modulesPath, pkgs, config, ... }: +let + inherit (lib) concatMap mkOption; + + cfg = config.my.disk; + + nixImage = (pkgs.callPackage "${modulesPath}/../lib/make-ext4-fs.nix" { + volumeLabel = "qclkos-nix"; + uuid = "38bd3706-c049-430d-9e33-5cd0e437279b"; + storePaths = config.system.build.toplevel; + compressImage = false; + }).overrideAttrs (o: { + buildCommand = '' + # HACK: `populateImageCommands` is executed in a subshell _before_ the paths are copied in... + shopt -s expand_aliases + nixUpThenMkfs() { + mv rootImage/{nix/store,} + rmdir rootImage/nix + + faketime "$@" + } + alias faketime=nixUpThenMkfs + + ${o.buildCommand} + ''; + }); +in +{ + options.my.disk = with lib.types; { + bootSize = mkOption { + description = "/boot size (MiB)."; + type = ints.unsigned; + default = 1024; + }; + persistSize = mkOption { + description = "/persist size (MiB)."; + type = ints.unsigned; + default = 8192; + }; + + populateBootCommands = mkOption { + description = '' + Shell commands to populate the ./boot directory. + All files in that directory are copied to the + /boot partition on the image. + ''; + }; + + imageBaseName = mkOption { + description = "Prefix of the name of the generated image file."; + default = "qclkos"; + }; + image = mkOption { + description = "Output disk image."; + type = unspecified; + }; + + rootSize = mkOption { + description = "tmpfs root size."; + type = str; + default = "2G"; + }; + }; + + config = { + fileSystems = { + "/boot" = { + device = "/dev/disk/by-label/QCLKOS_BOOT"; + fsType = "vfat"; + }; + "/persist" = { + device = "/dev/disk/by-label/qclkos-persist"; + fsType = "ext4"; + neededForBoot = true; + }; + "/nix" = { + device = "/dev/disk/by-label/qclkos-nix"; + fsType = "ext4"; + }; + + "/" = { + device = "yeet"; + fsType = "tmpfs"; + options = [ "size=${cfg.rootSize}" "mode=755" ]; + }; + }; + + boot = { + postBootCommands = '' + # On the first boot do some maintenance tasks + if [ -f /nix/nix-path-registration ]; then + set -euo pipefail + set -x + # Figure out device names for the boot device and nix filesystem. + nixPart=$(${pkgs.util-linux}/bin/findmnt -n -o SOURCE /nix) + bootDevice=$(lsblk -npo PKNAME $nixPart) + partNum=$(lsblk -npo MAJ:MIN $nixPart | ${pkgs.gawk}/bin/awk -F: '{print $2}') + + # Resize the nix partition and the filesystem to fit the disk + echo ",+," | sfdisk -N$partNum --no-reread $bootDevice + ${pkgs.parted}/bin/partprobe + ${pkgs.e2fsprogs}/bin/resize2fs $nixPart + + # Register the contents of the initial Nix store + ${config.nix.package.out}/bin/nix-store --load-db < /nix/nix-path-registration + + # nixos-rebuild also requires a "system" profile and an /etc/NIXOS tag. + touch /etc/NIXOS + ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system + + # Prevents this from running on later boots. + rm -f /nix/nix-path-registration + fi + ''; + }; + + users.mutableUsers = false; + + environment = { + # This is based on parts of `nixfiles/nixos/modules/tmproot.nix` + persistence."/persist" = { + directories = [ + "/var/lib/nixos" + "/var/lib/systemd" + ]; + files = [ + "/etc/machine-id" + ] ++ (concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys); + }; + }; + + services = { + journald.storage = "volatile"; + }; + + my.disk.image = pkgs.callPackage ({ + stdenv, dosfstools, e2fsprogs, mtools, libfaketime, util-linux + }: stdenv.mkDerivation { + name = "${cfg.imageBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}"; + nativeBuildInputs = [ + dosfstools e2fsprogs libfaketime mtools util-linux + ]; + + buildCommand = '' + mkdir -p $out + export img=$out/${cfg.imageBaseName}.img + + nix_fs=${nixImage} + + # Gap in front of the first partition, in MiB + gap=8 + + # Create the image file sized to fit /boot and /nix, plus slack for the gap. + nixSizeBlocks=$(du -B 512 --apparent-size $nix_fs | awk '{ print $1 }') + bootSizeBlocks=$((${toString cfg.bootSize} * 1024 * 1024 / 512)) + persistSizeBlocks=$((${toString cfg.persistSize} * 1024 * 1024 / 512)) + imageSize=$((nixSizeBlocks * 512 + persistSizeBlocks * 512 + bootSizeBlocks * 512 + gap * 1024 * 1024)) + truncate -s $imageSize $img + + # type=b is 'W95 FAT32', type=83 is 'Linux'. + # The "bootable" partition is where u-boot will look file for the bootloader + # information (dtbs, extlinux.conf file). + sfdisk --no-reread --no-tell-kernel $img <