From 762037d17fa2d07227069a5011ab83a3b4570a74 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Mon, 27 Oct 2025 12:21:57 +0000 Subject: [PATCH] (untested) config for stinky and diff script. --- .envrc | 1 + .gitignore | 1 + .sops.yaml | 7 + common/consul.nix | 2 +- common/ethereum.nix | 2 +- common/global/default.nix | 1 + common/global/impermanence-options.nix | 14 ++ common/global/network.nix | 4 +- common/global/tailscale.nix | 2 +- common/impermanence-common.nix | 30 +++ common/impermanence-tmpfs.nix | 30 +++ common/impermanence.nix | 36 +--- common/nfs-services-server.nix | 2 +- common/nomad.nix | 2 +- docs/DIFF_CONFIGS.md | 288 +++++++++++++++++++++++++ docs/RASPBERRY_PI_SD_IMAGE.md | 98 +++++++++ flake.nix | 37 ++++ hosts/alo-cloud-1/reverse-proxy.nix | 4 +- hosts/stinky/default.nix | 47 ++++ hosts/stinky/hardware.nix | 53 +++++ scripts/diff-configs.sh | 250 +++++++++++++++++++++ secrets/stinky.yaml | 25 +++ 22 files changed, 899 insertions(+), 37 deletions(-) create mode 100644 .envrc create mode 100644 common/global/impermanence-options.nix create mode 100644 common/impermanence-common.nix create mode 100644 common/impermanence-tmpfs.nix create mode 100644 docs/DIFF_CONFIGS.md create mode 100644 docs/RASPBERRY_PI_SD_IMAGE.md create mode 100644 hosts/stinky/default.nix create mode 100644 hosts/stinky/hardware.nix create mode 100755 scripts/diff-configs.sh create mode 100644 secrets/stinky.yaml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 16475c3..f8deaa6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ result .aider* .claude +.direnv/ diff --git a/.sops.yaml b/.sops.yaml index 8087ad2..3e17866 100644 --- a/.sops.yaml +++ b/.sops.yaml @@ -3,6 +3,7 @@ keys: - &server_zippy age1gtyw202hd07hddac9886as2cs8pm07e4exlnrgfm72lync75ng9qc5fjac - &server_chilly age16yqffw4yl5jqvsr7tyd883vn98zw0attuv9g5snc329juff6dy3qw2w5wp - &server_sparky age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy + - &server_stinky age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy - &server_alo_cloud_1 age1w5w4wfvtul3sge9mt205zvrkjaeh3qs9gsxhmq7df2g4dztnvv6qylup8z - &server_c1 age1wwufz86tm3auxn6pn27c47s8rvu7en58rk00nghtaxsdpw0gya6qj6qxdt - &server_c2 age1jy7pe4530s8w904wtvrmpxvteztqy5ewdt92a7y3lq87sg9jce5qxxuydt @@ -15,6 +16,7 @@ creation_rules: - *server_zippy - *server_chilly - *server_sparky + - *server_stinky - *server_alo_cloud_1 - *server_c1 - *server_c2 @@ -34,6 +36,11 @@ creation_rules: - age: - *admin_ppetru - *server_sparky + - path_regex: secrets/stinky\.yaml + key_groups: + - age: + - *admin_ppetru + - *server_stinky - path_regex: secrets/alo-cloud-1\.yaml key_groups: - age: diff --git a/common/consul.nix b/common/consul.nix index 4c5b532..4477783 100644 --- a/common/consul.nix +++ b/common/consul.nix @@ -27,7 +27,7 @@ in }; }; - environment.persistence."/persist".directories = [ "/var/lib/consul" ]; + environment.persistence.${config.custom.impermanence.persistPath}.directories = [ "/var/lib/consul" ]; networking.firewall = { allowedTCPPorts = [ diff --git a/common/ethereum.nix b/common/ethereum.nix index 96204d7..0a9b81b 100644 --- a/common/ethereum.nix +++ b/common/ethereum.nix @@ -12,7 +12,7 @@ checkpoint-sync-url = "https://beaconstate.info"; }; }; - environment.persistence."/persist".directories = [ + environment.persistence.${config.custom.impermanence.persistPath}.directories = [ "/var/lib/private/lighthouse-mainnet" ]; } diff --git a/common/global/default.nix b/common/global/default.nix index 8280724..b2fb086 100644 --- a/common/global/default.nix +++ b/common/global/default.nix @@ -5,6 +5,7 @@ ./console.nix ./cpufreq.nix ./flakes.nix + ./impermanence-options.nix ./kernel.nix ./locale.nix ./network.nix diff --git a/common/global/impermanence-options.nix b/common/global/impermanence-options.nix new file mode 100644 index 0000000..ca4ac32 --- /dev/null +++ b/common/global/impermanence-options.nix @@ -0,0 +1,14 @@ +{ + lib, + ... +}: +{ + # Define impermanence options that need to be available to all modules + # The actual impermanence implementation is in common/impermanence.nix or common/impermanence-tmpfs.nix + + options.custom.impermanence.persistPath = lib.mkOption { + type = lib.types.str; + default = "/persist"; + description = "Path where persistent data is stored (e.g., /persist for btrfs, /nix/persist for tmpfs)"; + }; +} diff --git a/common/global/network.nix b/common/global/network.nix index d657242..6cc46fd 100644 --- a/common/global/network.nix +++ b/common/global/network.nix @@ -1,4 +1,4 @@ -{ lib, ... }: +{ lib, config, ... }: { networking = { useDHCP = true; @@ -10,7 +10,7 @@ ''; }; - environment.persistence."/persist" = { + environment.persistence.${config.custom.impermanence.persistPath} = { directories = [ "/var/db/dhcpcd" ]; }; } diff --git a/common/global/tailscale.nix b/common/global/tailscale.nix index f273cd5..386d603 100644 --- a/common/global/tailscale.nix +++ b/common/global/tailscale.nix @@ -22,6 +22,6 @@ in config = mkIf cfg.enable { services.tailscaleAutoconnect.enable = true; services.tailscale.package = pkgs.unstable.tailscale; - environment.persistence."/persist".directories = [ "/var/lib/tailscale" ]; + environment.persistence.${config.custom.impermanence.persistPath}.directories = [ "/var/lib/tailscale" ]; }; } diff --git a/common/impermanence-common.nix b/common/impermanence-common.nix new file mode 100644 index 0000000..245ddce --- /dev/null +++ b/common/impermanence-common.nix @@ -0,0 +1,30 @@ +{ + lib, + config, + ... +}: +{ + # Common impermanence configuration shared by both btrfs and tmpfs variants + # This module should be imported by impermanence.nix or impermanence-tmpfs.nix + # The option custom.impermanence.persistPath is defined in common/global/impermanence-options.nix + + environment.persistence.${config.custom.impermanence.persistPath} = { + directories = [ + "/var/lib/nixos" + "/home" + ]; + files = [ + "/etc/machine-id" + "/etc/ssh/ssh_host_ed25519_key" + "/etc/ssh/ssh_host_ed25519_key.pub" + "/etc/ssh/ssh_host_rsa_key" + "/etc/ssh/ssh_host_rsa_key.pub" + ]; + }; + + users.mutableUsers = false; + + security.sudo.extraConfig = '' + Defaults lecture = never + ''; +} diff --git a/common/impermanence-tmpfs.nix b/common/impermanence-tmpfs.nix new file mode 100644 index 0000000..0054632 --- /dev/null +++ b/common/impermanence-tmpfs.nix @@ -0,0 +1,30 @@ +{ + lib, + config, + ... +}: +{ + # Impermanence configuration for tmpfs root filesystem + # Used for systems with tmpfs root (e.g., Raspberry Pi with SD card) + # Root is in-memory and wiped on every boot + # Persistent data is stored in /nix/persist (directory on the /nix partition) + + # Import common impermanence configuration + imports = [ ./impermanence-common.nix ]; + + config = { + # Use /nix/persist for tmpfs-based impermanence + custom.impermanence.persistPath = "/nix/persist"; + + # tmpfs root filesystem + fileSystems."/" = { + device = "none"; + fsType = "tmpfs"; + options = [ + "defaults" + "size=2G" + "mode=755" + ]; + }; + }; +} diff --git a/common/impermanence.nix b/common/impermanence.nix index a65811b..bcbc4f4 100644 --- a/common/impermanence.nix +++ b/common/impermanence.nix @@ -1,6 +1,5 @@ { pkgs, - inputs, lib, config, ... @@ -9,31 +8,22 @@ let cfg = config.custom.impermanence; in { + # Import common impermanence configuration + imports = [ ./impermanence-common.nix ]; + options.custom.impermanence = { enable = lib.mkOption { type = lib.types.bool; default = true; - description = "Enable impermanent root fs"; + description = "Enable impermanent root fs with btrfs subvolume rollback"; }; }; config = lib.mkIf cfg.enable { - environment.persistence = { - "/persist" = { - directories = [ - "/var/lib/nixos" - "/home" - ]; - files = [ - "/etc/machine-id" - "/etc/ssh/ssh_host_ed25519_key" - "/etc/ssh/ssh_host_ed25519_key.pub" - "/etc/ssh/ssh_host_rsa_key" - "/etc/ssh/ssh_host_rsa_key.pub" - ]; - }; - }; + # Use /persist for btrfs-based impermanence + custom.impermanence.persistPath = "/persist"; + # Btrfs-specific filesystem options fileSystems."/".options = [ "compress=zstd" "noatime" @@ -53,17 +43,7 @@ in ]; fileSystems."/var/log".neededForBoot = true; - users.mutableUsers = false; - - # rollback results in sudo lectures after each reboot - security.sudo.extraConfig = '' - Defaults lecture = never - ''; - - # needed for allowOther in the home-manager impermanence config - programs.fuse.userAllowOther = true; - - # reset / at each boot + # Btrfs subvolume rollback at each boot # Note `lib.mkBefore` is used instead of `lib.mkAfter` here. boot.initrd.postDeviceCommands = pkgs.lib.mkBefore '' mkdir /mnt diff --git a/common/nfs-services-server.nix b/common/nfs-services-server.nix index dbe44d5..f0848e6 100644 --- a/common/nfs-services-server.nix +++ b/common/nfs-services-server.nix @@ -24,7 +24,7 @@ in config = lib.mkIf cfg.enable { # Persist root SSH directory for replication key - environment.persistence."/persist" = { + environment.persistence.${config.custom.impermanence.persistPath} = { directories = [ "/root/.ssh" ]; diff --git a/common/nomad.nix b/common/nomad.nix index 0617371..3e22290 100644 --- a/common/nomad.nix +++ b/common/nomad.nix @@ -150,7 +150,7 @@ in plugin.raw_exec.config.enabled = true; }; - environment.persistence."/persist".directories = [ + environment.persistence.${config.custom.impermanence.persistPath}.directories = [ "/var/lib/docker" "/var/lib/nomad" ]; diff --git a/docs/DIFF_CONFIGS.md b/docs/DIFF_CONFIGS.md new file mode 100644 index 0000000..d7e111a --- /dev/null +++ b/docs/DIFF_CONFIGS.md @@ -0,0 +1,288 @@ +# Configuration Diff Tool + +Tool to compare all NixOS host configurations between current working tree and HEAD commit. + +## Purpose + +Before committing changes (especially refactors), verify that you haven't accidentally broken existing host configurations. This tool: +- Builds all host configurations in current state (with uncommitted changes) +- Builds all host configurations at HEAD (last commit) +- Uses `nvd` to show readable diffs for each host +- Highlights which hosts changed and which didn't + +## Usage + +### Prerequisites + +The script requires `nvd` to be in PATH. Use either: + +**Option 1: direnv (recommended)** +```bash +# Allow direnv in the repository (one-time setup) +direnv allow + +# direnv will automatically load the dev shell when you cd into the directory +cd /home/ppetru/projects/alo-cluster +# nvd is now in PATH +``` + +**Option 2: nix develop** +```bash +# Enter dev shell manually +nix develop + +# Now run the script +./scripts/diff-configs.sh +``` + +### Quick Start + +```bash +# Compare all hosts (summary) +./scripts/diff-configs.sh + +# Compare with detailed path listing +./scripts/diff-configs.sh -v c1 + +# Compare with content diffs of changed files (deep mode) +./scripts/diff-configs.sh --deep c1 + +# Compare only x86_64 hosts (avoid slow ARM cross-compilation) +./scripts/diff-configs.sh c1 c2 c3 zippy chilly sparky + +# Verbose mode with multiple hosts +./scripts/diff-configs.sh --verbose c1 c2 c3 + +# Via flake app +nix run .#diff-configs + +# Show help +./scripts/diff-configs.sh --help +``` + +### Typical Workflow + +```bash +# 1. Make changes to configurations +vim common/impermanence.nix + +# 2. Stage changes (required for flake to see them) +git add common/impermanence.nix + +# 3. Check what would change if you committed now +# For quick feedback, compare only x86_64 hosts first: +./scripts/diff-configs.sh c1 c2 c3 zippy chilly sparky + +# 4. Review output, make adjustments if needed + +# 5. If changes look good and affect ARM hosts, check those too: +./scripts/diff-configs.sh stinky alo-cloud-1 + +# 6. Commit when satisfied +git commit -m "Refactor impermanence config" +``` + +## Output Explanation + +### No Changes +``` +━━━ c1 ━━━ + Building current... done + Building HEAD... done + ✓ No changes +``` +This host's configuration is identical between current and HEAD. + +### Changes Detected +``` +━━━ stinky ━━━ + Building current... done + Building HEAD... done + ⚠ Configuration changed + +<<< /nix/store/abc-nixos-system-stinky-25.05 (HEAD) +>>> /nix/store/xyz-nixos-system-stinky-25.05 (current) + +Version changes: +[C] octoprint: 1.9.3 -> 1.10.0 +[A+] libcamera: ∅ -> 0.1.0 +Closure size: 1500 -> 1520 (5 paths added, 2 paths removed, +3, +15.2 MB) +``` + +Legend: +- `[C]` - Changed package version +- `[A+]` - Added package +- `[R-]` - Removed package +- `[U.]` - Updated (same version, rebuilt) + +### Verbose Mode (--verbose) + +With `-v` or `--verbose`, also shows the actual store paths that changed: + +``` +━━━ c1 ━━━ + Building current... done + Building HEAD... done + ⚠ Configuration changed + +[nvd summary as above] + + Changed store paths: + Removed (17 paths): + - config.fish + - system-units + - home-manager-generation + - etc-fuse.conf + ... and 13 more + + Added (17 paths): + - config.fish + - system-units + - home-manager-generation + - etc-fuse.conf + ... and 13 more +``` + +This is useful when nvd shows "No version changes" but paths still changed (e.g., refactors that rebuild config files). + +### Deep Mode (--deep) + +With `-d` or `--deep`, shows actual content diffs of changed files within store paths (implies verbose): + +``` +━━━ c1 ━━━ + Building current... done + Building HEAD... done + ⚠ Configuration changed + +[nvd summary and path listing as above] + + Content diffs of changed files: + + ▸ etc-fuse.conf + @@ -1,2 +1,2 @@ + -user_allow_other + +#user_allow_other + mount_max = 1000 + + ▸ nixos-system-c1-25.05 + activate: + @@ -108,7 +108,7 @@ + echo "setting up /etc..." + -/nix/store/...-perl/bin/perl /nix/store/...-setup-etc.pl /nix/store/abc-etc/etc + +/nix/store/...-perl/bin/perl /nix/store/...-setup-etc.pl /nix/store/xyz-etc/etc + + ▸ unit-dbus.service + dbus.service: + @@ -1,5 +1,5 @@ + [Service] + +Environment="LD_LIBRARY_PATH=/nix/store/.../systemd/lib" + Environment="LOCALE_ARCHIVE=..." +``` + +**What it shows**: +- Matches changed paths by basename (e.g., both have "config.fish") +- Diffs important files: activate scripts, etc/*, *.conf, *.fish, *.service, *.nix +- Shows unified diff format (lines added/removed) +- Limits to first 50 lines per file + +**When to use**: +- When you need to know **what exactly changed** in config files +- Debugging unexpected configuration changes +- Reviewing refactors that don't change package versions +- Understanding why a host rebuilt despite "No version changes" + +### Build Failures +``` +━━━ broken-host ━━━ + Building current... FAILED + Error: attribute 'foo' missing +``` +If a host fails to build, the error is shown and the script continues with other hosts. + +## How It Works + +1. **Discovers hosts**: Queries `deploy.nodes` from flake to get all configured hosts +2. **Creates worktree**: Uses `git worktree` to check out HEAD in a temporary directory +3. **Builds configurations**: Builds `config.system.build.toplevel` for each host in both locations +4. **Compares with nvd**: Runs `nvd diff` to show package-level changes +5. **Cleans up**: Removes temporary worktree automatically + +## Important Notes + +### Git Staging Required + +Flakes only evaluate files that are tracked by git. To make changes visible: +```bash +# Stage new files +git add new-file.nix + +# Stage changes to existing files +git add modified-file.nix + +# Or stage everything +git add . +``` + +Unstaged changes to tracked files **are** visible (flake uses working tree content). + +### Performance + +- First run may be slow (building all configurations) +- Subsequent runs benefit from Nix evaluation cache +- Typical runtime: 1-5 minutes depending on changes +- **ARM cross-compilation is slow**: Use host filtering to avoid building ARM hosts when not needed + - Example: `./scripts/diff-configs.sh c1 c2 c3` (x86_64 only, fast) + - vs `./scripts/diff-configs.sh` (includes stinky/alo-cloud-1, slow) + +### When to Use + +**Good use cases**: +- Refactoring shared modules (like impermanence) +- Updating common configurations +- Before committing significant changes +- Verifying deploy target consistency + +**Not needed for**: +- Adding a single new host +- Trivial one-host changes +- Documentation updates + +## Troubleshooting + +### "Not in a git repository" +```bash +cd /home/ppetru/projects/alo-cluster +./scripts/diff-configs.sh +``` + +### "No changes detected" +All changes are already committed. Stage some changes first: +```bash +git add . +``` + +### Build failures for all hosts +Check flake syntax: +```bash +nix flake check +``` + +### nvd not found +Install nvd: +```bash +nix profile install nixpkgs#nvd +``` +(Already included in workstation-node.nix packages) + +## Related Tools + +- `nvd` - Package diff tool (used internally) +- `nix diff-closures` - Low-level closure diff +- `nix store diff-closures` - Alternative diff command +- `deploy-rs` - Actual deployment tool + +## See Also + +- `common/global/show-changelog.nix` - Shows changes during system activation +- `docs/RASPBERRY_PI_SD_IMAGE.md` - SD image building process diff --git a/docs/RASPBERRY_PI_SD_IMAGE.md b/docs/RASPBERRY_PI_SD_IMAGE.md new file mode 100644 index 0000000..167eb09 --- /dev/null +++ b/docs/RASPBERRY_PI_SD_IMAGE.md @@ -0,0 +1,98 @@ +# Raspberry Pi SD Image Building and Deployment + +Guide for building and deploying NixOS SD card images for Raspberry Pi hosts (e.g., stinky). + +## Overview + +Raspberry Pi hosts use a different deployment strategy than regular NixOS hosts: +- **First deployment**: Build and flash an SD card image +- **Subsequent updates**: Use `deploy-rs` like other hosts + +## Architecture + +### Storage Layout + +**Partition structure** (automatically created by NixOS): +- `/boot/firmware` - FAT32 partition (label: `FIRMWARE`) + - Contains Raspberry Pi firmware, U-Boot bootloader, device trees +- `/` - tmpfs (in-memory, ephemeral root) + - 2GB RAM disk, wiped on every boot +- `/nix` - ext4 partition (label: `NIXOS_SD`) + - Nix store and persistent data + - Contains `/nix/persist` directory for impermanence + +### Impermanence with tmpfs + +Unlike btrfs-based hosts that use `/persist`, Pi hosts use `/nix/persist`: +- Root filesystem is tmpfs (no disk writes, auto-wiped) +- Single ext4 partition mounted at `/nix` +- Persistent data stored in `/nix/persist/` (directory, not separate mount) +- Better for SD card longevity (fewer writes) + +**Persisted paths**: +- `/nix/persist/var/lib/nixos` - System state +- `/nix/persist/home/ppetru` - User home directory +- `/nix/persist/etc` - SSH host keys, machine-id +- Service-specific: `/nix/persist/var/lib/octoprint`, etc. + +## Building the SD Image + +### Prerequisites + +- ARM64 emulation enabled on build machine: + ```nix + boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; + ``` + (Already configured in `workstation-node.nix`) + +### Build Command + +```bash +# Build SD image for stinky +nix build .#stinky-sdImage + +# Result location +ls -lh result/sd-image/ +# nixos-sd-image-stinky-25.05-*.img.zst (compressed with zstd) +``` + +**Build location**: Defined in `flake.nix`: +```nix +packages.aarch64-linux.stinky-sdImage = + self.nixosConfigurations.stinky.config.system.build.sdImage; +``` + +## Flashing the SD Card + +### Find SD Card Device + +```bash +# Before inserting SD card +lsblk + +# Insert SD card, then check again +lsblk + +# Look for new device, typically: +# - /dev/sdX (USB SD card readers) +# - /dev/mmcblk0 (built-in SD card slots) +``` + +**Warning**: Double-check the device! Wrong device = data loss. + +### Flash Image + +```bash +# Decompress and flash in one command +zstd -d -c result/sd-image/*.img.zst | sudo dd of=/dev/sdX bs=4M status=progress conv=fsync + +# Or decompress first, then flash +unzstd result/sd-image/*.img.zst +sudo dd if=result/sd-image/*.img of=/dev/sdX bs=4M status=progress conv=fsync +``` + +### Eject SD Card + +```bash +sudo eject /dev/sdX +``` diff --git a/flake.nix b/flake.nix index 860b59e..92144be 100644 --- a/flake.nix +++ b/flake.nix @@ -163,6 +163,7 @@ ]; chilly = mkHost "x86_64-linux" "workstation" [ ./hosts/chilly ]; sparky = mkHost "x86_64-linux" "desktop" [ ./hosts/sparky ]; + stinky = mkHost "aarch64-linux" "minimal" [ ./hosts/stinky ]; }; deploy = { @@ -224,9 +225,45 @@ }; }; }; + stinky = { + hostname = "stinky"; + profiles = { + system = { + user = "root"; + path = (deployPkgsFor "aarch64-linux").deploy-rs.lib.activate.nixos self.nixosConfigurations.stinky; + }; + }; + }; }; }; + # SD card image for stinky (Raspberry Pi 4) + packages.aarch64-linux.stinky-sdImage = self.nixosConfigurations.stinky.config.system.build.sdImage; + + # Apps - utility scripts + apps.x86_64-linux.diff-configs = { + type = "app"; + program = "${(pkgsFor "x86_64-linux").writeShellScriptBin "diff-configs" (builtins.readFile ./scripts/diff-configs.sh)}/bin/diff-configs"; + }; + + apps.aarch64-linux.diff-configs = { + type = "app"; + program = "${(pkgsFor "aarch64-linux").writeShellScriptBin "diff-configs" (builtins.readFile ./scripts/diff-configs.sh)}/bin/diff-configs"; + }; + + # Development shells + devShells.x86_64-linux.default = (pkgsFor "x86_64-linux").mkShell { + packages = with (pkgsFor "x86_64-linux"); [ + nvd + ]; + }; + + devShells.aarch64-linux.default = (pkgsFor "aarch64-linux").mkShell { + packages = with (pkgsFor "aarch64-linux"); [ + nvd + ]; + }; + checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt-rfc-style; diff --git a/hosts/alo-cloud-1/reverse-proxy.nix b/hosts/alo-cloud-1/reverse-proxy.nix index e069181..38c8ce4 100644 --- a/hosts/alo-cloud-1/reverse-proxy.nix +++ b/hosts/alo-cloud-1/reverse-proxy.nix @@ -1,7 +1,7 @@ -{ pkgs, ... }: +{ pkgs, config, ... }: { environment.systemPackages = [ pkgs.traefik ]; - environment.persistence."/persist".files = [ "/acme/acme.json" ]; + environment.persistence.${config.custom.impermanence.persistPath}.files = [ "/acme/acme.json" ]; services.traefik = { enable = true; diff --git a/hosts/stinky/default.nix b/hosts/stinky/default.nix new file mode 100644 index 0000000..3303dc1 --- /dev/null +++ b/hosts/stinky/default.nix @@ -0,0 +1,47 @@ +{ + lib, + pkgs, + config, + ... +}: +{ + imports = [ + ../../common/global + ../../common/impermanence-tmpfs.nix # Use tmpfs root with /nix/persist + ../../common/resource-limits.nix + ../../common/sshd.nix + ../../common/user-ppetru.nix + ../../common/systemd-boot.nix + ./hardware.nix + ]; + + networking.hostName = "stinky"; + + # Tailscale configuration + services.tailscaleAutoconnect.authkey = "PLACEHOLDER"; # Will be set in secrets + + # OctoPrint for 3D printer + services.octoprint = { + enable = true; + }; + + # Persist OctoPrint data + environment.persistence.${config.custom.impermanence.persistPath}.directories = [ + "/var/lib/octoprint" + ]; + + # Pi HQ Camera support + boot.kernelModules = [ "bcm2835-v4l2" ]; + + environment.systemPackages = with pkgs; [ + libcamera + raspberrypi-tools + ]; + + # Firewall: Allow access to OctoPrint + networking.firewall.allowedTCPPorts = [ + 5000 # OctoPrint + ]; + + system.stateVersion = "25.05"; +} diff --git a/hosts/stinky/hardware.nix b/hosts/stinky/hardware.nix new file mode 100644 index 0000000..bc1f8fd --- /dev/null +++ b/hosts/stinky/hardware.nix @@ -0,0 +1,53 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: +{ + imports = [ + (modulesPath + "/installer/sd-card/sd-image-aarch64.nix") + ]; + + # Raspberry Pi 4 platform + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + + # Boot configuration - provided by sd-image-aarch64.nix + # (grub disabled, generic-extlinux-compatible enabled, U-Boot setup) + + # Override root filesystem to use tmpfs (from impermanence-tmpfs.nix) + # The sd-image module sets root to /dev/disk/by-label/NIXOS_SD (ext4) + # but impermanence-tmpfs.nix overrides it to tmpfs + + # /boot/firmware is automatically configured by sd-image module + # Device: /dev/disk/by-label/FIRMWARE (vfat) + + # Mount /nix from the NIXOS_SD partition + # /nix/persist will be a directory on this partition (not a separate mount) + fileSystems."/nix" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + neededForBoot = true; + }; + + # No swap on SD card (wear concern) + swapDevices = [ ]; + + # SD image build configuration + sdImage = { + compressImage = true; + + # Populate root with directories + populateRootCommands = '' + mkdir -p ./files/boot + ${config.boot.loader.generic-extlinux-compatible.populateCmd} -c ${config.system.build.toplevel} -d ./files/boot + + # Create /nix/persist directory structure for impermanence + mkdir -p ./files/nix/persist/var/lib/nixos + mkdir -p ./files/nix/persist/home/ppetru + mkdir -p ./files/nix/persist/etc + ''; + }; +} diff --git a/scripts/diff-configs.sh b/scripts/diff-configs.sh new file mode 100755 index 0000000..778d165 --- /dev/null +++ b/scripts/diff-configs.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# Compare NixOS configurations between current state and HEAD +# Shows what would change if you committed the current changes +# +# Requirements: nvd must be in PATH +# Run inside `nix develop` or with direnv enabled + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Check for nvd +if ! command -v nvd &> /dev/null; then + echo "Error: nvd not found in PATH" + echo "Run this script inside 'nix develop' or enable direnv" + exit 1 +fi + +# Parse flags +verbose=false +deep=false +hosts_args=() + +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + echo "Usage: $0 [-v|--verbose] [-d|--deep] [HOST...]" + echo "Compare NixOS configurations between working tree and HEAD" + echo "" + echo "Options:" + echo " -v, --verbose Show detailed list of added/removed store paths" + echo " -d, --deep Show content diffs of changed files (implies -v)" + echo "" + echo "Arguments:" + echo " HOST One or more hostnames to compare (default: all)" + echo "" + echo "Examples:" + echo " $0 # Compare all hosts (summary)" + echo " $0 -v c1 # Compare c1 with path list" + echo " $0 --deep c1 # Compare c1 with content diffs" + echo " $0 c1 c2 c3 # Compare only c1, c2, c3" + exit 0 + ;; + -v|--verbose) + verbose=true + shift + ;; + -d|--deep) + deep=true + verbose=true # deep implies verbose + shift + ;; + *) + hosts_args+=("$1") + shift + ;; + esac +done + +# Restore positional parameters +set -- "${hosts_args[@]}" + +# Check if we're in a git repo +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: Not in a git repository" + exit 1 +fi + +# Check if there are any changes +if git diff --quiet && git diff --cached --quiet; then + echo "No changes detected between working tree and HEAD" + exit 0 +fi + +echo "Comparing configurations: current working tree vs HEAD" +echo "=======================================================" +echo + +# Get list of hosts to compare +if [ $# -gt 0 ]; then + # Use hosts provided as arguments + hosts="$@" + echo -e "${YELLOW}Comparing selected hosts: $hosts${NC}" +else + # Get all hosts from flake + echo "Discovering all hosts from flake..." + hosts=$(nix eval --raw .#deploy.nodes --apply 'nodes: builtins.concatStringsSep "\n" (builtins.attrNames nodes)' 2>/dev/null) + + if [ -z "$hosts" ]; then + echo "Error: No hosts found in flake" + exit 1 + fi +fi + +echo + +# Create temp worktree at HEAD +worktree=$(mktemp -d) +trap "git worktree remove --force '$worktree' &>/dev/null || true; rm -rf '$worktree'" EXIT + +echo "Creating temporary worktree at HEAD..." +git worktree add --quiet --detach "$worktree" HEAD + +echo "Building and comparing configurations..." +echo + +any_changes=false + +for host in $hosts; do + echo -e "${BLUE}━━━ $host ━━━${NC}" + + # Build current (with uncommitted changes) + echo -n " Building current... " + if ! current=$(nix build --no-link --print-out-paths \ + ".#nixosConfigurations.$host.config.system.build.toplevel" 2>/dev/null); then + echo -e "${RED}FAILED${NC}" + # Re-run to show error + nix build --no-link ".#nixosConfigurations.$host.config.system.build.toplevel" 2>&1 | head -20 | sed 's/^/ /' + continue + fi + echo "done" + + # Build HEAD + echo -n " Building HEAD... " + if ! head=$(nix build --no-link --print-out-paths \ + "$worktree#nixosConfigurations.$host.config.system.build.toplevel" 2>/dev/null); then + echo -e "${RED}FAILED${NC}" + # Re-run to show error + nix build --no-link "$worktree#nixosConfigurations.$host.config.system.build.toplevel" 2>&1 | head -20 | sed 's/^/ /' + continue + fi + echo "done" + + # Compare + if [ "$head" = "$current" ]; then + echo -e " ${GREEN}✓ No changes${NC}" + else + any_changes=true + echo -e " ${RED}⚠ Configuration changed${NC}" + echo + + # Show nvd summary + if ! nvd diff "$head" "$current" 2>&1; then + echo -e " ${RED}(nvd diff failed - see error above)${NC}" + fi + + # Show detailed closure diff if verbose + if [ "$verbose" = true ]; then + echo + echo -e " ${YELLOW}Changed store paths:${NC}" + + # Get paths unique to HEAD and current + head_only=$(comm -23 <(nix-store -q --requisites "$head" 2>/dev/null | sort) \ + <(nix-store -q --requisites "$current" 2>/dev/null | sort)) + current_only=$(comm -13 <(nix-store -q --requisites "$head" 2>/dev/null | sort) \ + <(nix-store -q --requisites "$current" 2>/dev/null | sort)) + + # Count changes + removed_count=$(echo "$head_only" | wc -l) + added_count=$(echo "$current_only" | wc -l) + + echo -e " ${RED}Removed ($removed_count paths):${NC}" + echo "$head_only" | head -10 | sed 's|^/nix/store/[^-]*-| - |' + if [ "$removed_count" -gt 10 ]; then + echo " ... and $((removed_count - 10)) more" + fi + + echo + echo -e " ${GREEN}Added ($added_count paths):${NC}" + echo "$current_only" | head -10 | sed 's|^/nix/store/[^-]*-| - |' + if [ "$added_count" -gt 10 ]; then + echo " ... and $((added_count - 10)) more" + fi + + # Show content diffs if deep mode + if [ "$deep" = true ]; then + echo + echo -e " ${YELLOW}Content diffs of changed files:${NC}" + + # Extract basenames for matching + declare -A head_paths + while IFS= read -r path; do + [ -z "$path" ] && continue + basename="${path#/nix/store/[a-z0-9]*-}" + head_paths["$basename"]="$path" + done <<< "$head_only" + + # Find matching pairs and diff them + matched=false + while IFS= read -r path; do + [ -z "$path" ] && continue + basename="${path#/nix/store/[a-z0-9]*-}" + + # Check if we have a matching path in head + if [ -n "${head_paths[$basename]:-}" ]; then + old_path="${head_paths[$basename]}" + new_path="$path" + matched=true + + echo + echo -e " ${BLUE}▸ $basename${NC}" + + # If it's a directory, diff key files within it + if [ -d "$old_path" ] && [ -d "$new_path" ]; then + # Focus on important files + for pattern in "activate" "etc/*" "*.conf" "*.fish" "*.service" "*.nix"; do + while IFS= read -r file; do + [ -z "$file" ] && continue + relpath="${file#$new_path/}" + old_file="$old_path/$relpath" + + if [ -f "$old_file" ] && [ -f "$file" ]; then + # Check if file is text + if file "$file" | grep -q "text"; then + echo -e " ${YELLOW}$relpath:${NC}" + diff -u "$old_file" "$file" 2>/dev/null | head -50 | tail -n +3 | sed 's/^/ /' || true + fi + fi + done < <(find "$new_path" -type f -name "$pattern" 2>/dev/null | head -20) + done + # If it's a file, diff it directly + elif [ -f "$old_path" ] && [ -f "$new_path" ]; then + if file "$new_path" | grep -q "text"; then + diff -u "$old_path" "$new_path" 2>/dev/null | head -50 | tail -n +3 | sed 's/^/ /' || true + else + echo " (binary file)" + fi + fi + fi + done <<< "$current_only" + + if [ "$matched" = false ]; then + echo " (no matching paths found to compare)" + fi + fi + fi + fi + echo +done + +if [ "$any_changes" = false ]; then + echo -e "${GREEN}✓ All configurations unchanged${NC}" +else + echo -e "${RED}⚠ Some configurations changed - review carefully before committing${NC}" +fi diff --git a/secrets/stinky.yaml b/secrets/stinky.yaml new file mode 100644 index 0000000..4b632f1 --- /dev/null +++ b/secrets/stinky.yaml @@ -0,0 +1,25 @@ +kopia: ENC[AES256_GCM,data:boi8V0Kn,iv:Kwe1hn44DJe9dpv8jVrJjwyblVouakuCdnEK9uotTkY=,tag:B5hrpRBP17kFVn4iy5TOlA==,type:str] +sops: + age: + - recipient: age1df9ukkmg9yn9cjeheq9m6wspa420su8qarmq570rdvf2de3rl38saqauwn + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOGdzMGpnSFNJTS80enUy + UG9PUDdYV2VyQVBvSzdaeG5zWk9jL2JCYUFjCm5SL1QycWo0a29FaFlXaEN0VWdp + bmN1M0lHVUVLTERmSjJYNDZncStJT1UKLS0tIGREYjJHN1dxeFN4NHdWTVBRcERT + dzNVeXZzODJSSmxnRXRDZW55NzJwZWsKwRf3hvRmFvUC5CStHGmOigcgIhXodzBL + EXS8SdNQOr2qwaekJv/jQ9ApHw7TlSYYrKK+i4xn4qAIG1nTH1FX+A== + -----END AGE ENCRYPTED FILE----- + - recipient: age10zxwwufrf5uu9cv9p9znse2ftfm74q9ce893us6cnvxjc7e3ypcqy709dy + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJMGIxUFNvaGtTVmNHd1Ny + bjBFc2JybTdhZjhjbnU1cEZQTEV0NFhISW1jCkNwODBPdU4reDRsSEhWM04wR3V6 + cFhKZXFReStvWXc1VGV4YTVNckp0a2MKLS0tIEtFVHZUR0VRMWdlWjd4YlQ0Q3VM + SklxeG1VcDBpMlF5Z2hXRVdWS3hXK3MKY6sNfLPpm/LVNgIk76zNgiCxQWp3TM3I + rf2tp6YZSYHzzOdFmodj6Li4NMhe2tRrm0koHirxG2TqibhylOo9FA== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2025-10-26T16:59:40Z" + mac: ENC[AES256_GCM,data:FlSv9PIcmX+oJNVaUpXIG2thzUvEb7bMGDOvIRgAFVzoUipIes0qdbU0R/pqogW0NpgbXNLhNBmemKfheGusngatJmbNwHT9Hqo7a82U9j1G302sziqrcz1pOxG79oacFEM+coWpXGgmMXYeNlQEihUvvvUt810VWBb3Hjba80g=,iv:6gSTUd2y9YxiOCzwQ/udLN46lgfwgWDgfSTOpaJpPmY=,tag:q/Ta6fejjKMg0TmZhNmy8Q==,type:str] + unencrypted_suffix: _unencrypted + version: 3.11.0