(untested) config for stinky and diff script.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
result
|
||||
.aider*
|
||||
.claude
|
||||
.direnv/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
./console.nix
|
||||
./cpufreq.nix
|
||||
./flakes.nix
|
||||
./impermanence-options.nix
|
||||
./kernel.nix
|
||||
./locale.nix
|
||||
./network.nix
|
||||
|
||||
14
common/global/impermanence-options.nix
Normal file
14
common/global/impermanence-options.nix
Normal file
@@ -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)";
|
||||
};
|
||||
}
|
||||
@@ -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" ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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" ];
|
||||
};
|
||||
}
|
||||
|
||||
30
common/impermanence-common.nix
Normal file
30
common/impermanence-common.nix
Normal file
@@ -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
|
||||
'';
|
||||
}
|
||||
30
common/impermanence-tmpfs.nix
Normal file
30
common/impermanence-tmpfs.nix
Normal file
@@ -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"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
|
||||
@@ -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"
|
||||
];
|
||||
|
||||
288
docs/DIFF_CONFIGS.md
Normal file
288
docs/DIFF_CONFIGS.md
Normal file
@@ -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
|
||||
98
docs/RASPBERRY_PI_SD_IMAGE.md
Normal file
98
docs/RASPBERRY_PI_SD_IMAGE.md
Normal file
@@ -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
|
||||
```
|
||||
37
flake.nix
37
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,8 +225,44 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
47
hosts/stinky/default.nix
Normal file
47
hosts/stinky/default.nix
Normal file
@@ -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";
|
||||
}
|
||||
53
hosts/stinky/hardware.nix
Normal file
53
hosts/stinky/hardware.nix
Normal file
@@ -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
|
||||
'';
|
||||
};
|
||||
}
|
||||
250
scripts/diff-configs.sh
Executable file
250
scripts/diff-configs.sh
Executable file
@@ -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
|
||||
25
secrets/stinky.yaml
Normal file
25
secrets/stinky.yaml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user