Compare commits

..

61 Commits

Author SHA1 Message Date
ca3faf40cf Empty initrd for display, add rp1_dsi module
ALL RP1 hardware (gpio, clocks, dsi) sits behind the RP1 PCIe
southbridge, which isn't ready until ~12s. Loading any display
module in initrd (~3s) crashes the system. Nothing in initrd now:
- boot.initrd.kernelModules: empty (nothing touches RP1 early)
- boot.kernelModules: vc4, panel_cwu50, rp1_dsi (stage-2)
2026-06-09 23:14:44 -04:00
4f1864af70 Revert vc4/panel_cwu50 to boot.kernelModules (stage-2 loading)
Loading via boot.kernelModules ensures they're loaded after
initrd, by which time RP1 PCIe southbridge is initialized (~12s).
This avoids the 'Cannot find any crtc' error from premature probe.

- boot.kernelModules: explicit vc4 + panel_cwu50 (stage-2)
- boot.initrd.kernelModules: only ocp8178_bl (backlight)
2026-06-09 22:46:43 -04:00
67418f037a Rex timing: remove vc4/panel_cwu50 from boot.kernelModules too
Let udev auto-load display modules when RP1 PCIe southbridge
appears on the bus (~12s). This is exactly how Rex's Debian
image works — no forced loading in initrd or stage-2.

- boot.initrd.kernelModules: only ocp8178_bl (backlight only)
- boot.kernelModules: no display modules (udev handles it)
- SDDM disabled (Hyprland kept for manual start)
2026-06-09 22:44:45 -04:00
fb01e4be1d SDDM disabled, Hyprland kept (manual start only)
- Removed SDDM display manager (was blocking boot when display fails)
- Kept Hyprland installed for manual start (hyprland command)
- Display modules load in stage-2 via boot.kernelModules (Rex timing)
2026-06-09 22:39:13 -04:00
f57f2846ff Fix duplicate boot.kernelModules, merge vc4/panel_cwu50 into existing list 2026-06-09 22:20:27 -04:00
288b7538d5 Fix CM5 display: delay vc4/panel_cwu50 to after RP1 PCIe init
Loading display drivers in initrd (~3s) before RP1 PCIe southbridge
is ready (~12s) causes 'Cannot find any crtc or sizes' because the
DSI encoder isn't reachable. Rex loads these via udev late in boot.

- boot.initrd.kernelModules: only ocp8178_bl (backlight, RP1-independent)
- boot.kernelModules: vc4 + panel_cwu50 loaded in stage 2 after RP1
2026-06-09 21:42:24 -04:00
fb7c3e6424 Fix RNS setup.py license_files tuple for setuptools 80.x 2026-06-09 20:49:16 -04:00
6961027218 Fix reticulum build: add sourceRoot for PyPI package 2026-06-09 20:47:45 -04:00
8bf44a066e Add nixpkgs-uconsole pinned to commit that base image used
The base image (nixos-uconsole v1.1.0) was built with nixpkgs
commit 54170c54449ea4d6725efd30d719c5e505f1c10e (2026-04-10),
NOT the flake.lock pin (89dbf01df7 from Dec 2025). By pinning to
the exact commit the image used, the kernel derivation hash should
match what's already in the store.
2026-06-09 20:38:05 -04:00
644c9a7645 uConsole: use mkUConsoleSystem instead of manual nixosSystem
Using nixos-uconsole.lib.mkUConsoleSystem ensures the exact same
module composition as the pre-built image, so the kernel derivation
hash matches what'\''s already in the store (6.12.47).
2026-06-09 20:16:31 -04:00
52fa439409 Align pins with nixos-uconsole v1.1.0 for kernel cache hit
- nixos-uconsole: main -> v1.1.0 (tag)

- nixos-raspberrypi: v1.20260517.0 -> v1.20260317.0 (matches nixos-uconsole v1.1.0's flake.lock)

- Keeps nixos-raspberrypi.inputs.nixpkgs.follows = nixos-uconsole/nixpkgs

This ensures the kernel derivation hash matches the pre-built image's kernel already in the uConsole store.
2026-06-09 19:52:40 -04:00
db2deda941 fix(uconsole): align nixos-raspberrypi nixpkgs with nixos-uconsole
Make nixos-raspberrypi follow nixos-uconsole's nixpkgs so the kernel
uses the same nixpkgs that the uconsole Cachix was built with.
2026-06-09 19:34:31 -04:00
7f7634c1b1 fix(uconsole): stop following nixpkgs for nixos-uconsole and nixos-raspberrypi
Remove inputs.nixpkgs.follows so these inputs use their own pinned nixpkgs.
This lets the uConsole build use the nixos-uconsole Cachix cache for the kernel,
avoiding 4h kernel recompilation on every config change.
2026-06-09 19:23:53 -04:00
1fa1cfaa76 fix(uconsole): apply CM5 display fixes from nixos-uconsole comparison
- Add hardware.graphics.enable = true (Mesa GPU required for VC4)
- Add boot.kernelParams with console=tty1 (console on fb0 not ttyAMA0)
- Fix console.font to ter-v24n with proper package (match nixos-uconsole)
- Merge GPIO 10/11 from nixos-uconsole extra-config (audio amp)
- Bump stateVersion to 25.11
2026-06-08 23:40:19 -04:00
4d2cba77e1 fix(display): use [pi5] config.txt section instead of [cm5]
Rex's Trixie images use [pi5] for the CM5 display overlays
(clockworkpi-uconsole-cm5, vc4-kms-v3d-pi5). The nixos-uconsole
module generates [cm5] which the firmware may not apply on
some CM5 EEPROM configurations. Adding explicit [pi5] section
ensures the display overlays are always loaded.
2026-06-07 22:49:01 -04:00
8d54e5e8fa fix(display): use kernel default console font instead of ter-v24n
The ter-v24n Terminus font renders garbled on CM5 DSI 720x1280
panel (green stretched line across screen). Switching to the
kernel's built-in Lat2-Terminus16 font for correct text rendering.
2026-06-07 22:10:46 -04:00
42e5d4dd2d fix(uconsole): disable GPIO 23 service (causes CM5 freeze) + open SSH
GPIO 23 service used gpioset 0 23=1 on BCM2712 chip 0 instead
of the RP1 chip (base 512), writing to a critical pin and
freezing the system. Commented out until correct chip is confirmed.
SSH opened with PasswordAuthentication + PermitRootLogin for
first deployment. Lock down after initial boot.
2026-06-07 21:57:13 -04:00
682402e0e6 fix(uconsole): use nixpkgs default nix instead of lix dev
lix dev (20260606) is incompatible with nixpkgs 25.11 stable.
Still on other hosts. This avoids the lix build failure.
2026-06-07 10:08:35 -04:00
9c4b50b4c3 fix(config): remove meshtastic from packages — not in nixpkgs 25.11 2026-06-07 06:47:32 -04:00
8e395729ff fix(overlay): avoid recursion with final.lib, use prev.lib.optionalAttrs
Replace recursive `final.lib.optionalAttrs` with `prev.lib.optionalAttrs`
using the `?` operator to check for meshtastic existence safely.
This allows the overlay to work with both stable (25.11) and unstable.
2026-06-07 06:45:16 -04:00
346b41995f fix(overlay): make meshtastic conditional for stable nixpkgs (25.11)
meshtastic package doesn't exist in nixpkgs 25.11 stable.
Use optionalAttrs to skip the override when not available,
allowing the flake to build against both stable and unstable.
2026-06-07 06:43:09 -04:00
68900ca7b3 fix: switch to nixpkgs 25.11 stable to match pre-built uConsole kernel
The uConsole CM5 has kernel 6.12.47 in its store from the
nixos-uconsole-cm5-v1.1.0 image (built with nixpkgs 25.11).
By switching to the same nixpkgs version, Nix will reuse
the existing kernel instead of rebuilding it from source.
2026-06-07 06:37:25 -04:00
38eea77fd9 fix(uconsole): remove useless udev rule for voltage_min_design
sysfs node is read-only (0444), even root cannot write.
The AXP228 PMU manages power path in hardware.
Practical fix: disconnect batteries during heavy builds.
2026-06-06 23:01:14 -04:00
b7b5ef0b53 fix(uconsole): lower voltage_min_design to 2.7V to prevent random power-off under heavy CPU load 2026-06-06 22:54:59 -04:00
f0954efcaa fix(sddm): enable wayland support for Hyprland 2026-06-06 21:29:51 -04:00
b0be414649 revert(nixos-uconsole): follow unstable nixpkgs again
Cachix has no CM5 kernel anyway, so sticking to stable doesn't
save build time. Might as well stay on unstable for latest pkgs.
2026-06-06 21:27:47 -04:00
31dd0f36d4 feat(uconsole): add Hyprland + SDDM display manager
Enables Hyprland Wayland compositor with XWayland support
and SDDM display manager for graphical desktop on the uConsole CM5.
2026-06-06 21:23:15 -04:00
653c69fcfd fix(flake): detach nixos-uconsole nixpkgs from unstable to hit Cachix
nixos-uconsole's Cachix cache is built with nixpkgs-stable (25.11).
Following our unstable caused a full kernel rebuild every time.
By using nixos-uconsole's pinned nixpkgs, future builds will
download the pre-compiled kernel from the cache instead of
compiling it locally on the CM5.
2026-06-06 21:07:37 -04:00
a9b95c5d48 fix(config): use libgpiod instead of gpiod for gpioset
The nixpkgs attribute for userspace GPIO tools is 'libgpiod',
not 'gpiod'. This provides the gpioset binary used by the
GPIO 23 internal USB hub service.
2026-06-06 17:27:12 -04:00
6771c9882a fix(hw-config): use mkForce for filesystems to avoid disko conflict
Disko auto-generates fileSystems with by-partlabel paths, but
for manual install via loop devices we need by-label paths.
mkForce ensures our paths win during evaluation.
2026-06-06 17:23:50 -04:00
897f470a16 fix(disko): use /dev/mmcblk0 instead of wrong by-path
platform-fe340000.mmc doesn't exist on the uConsole CM5.
The eMMC is at /dev/mmcblk0 in normal boot mode.
2026-06-06 16:51:46 -04:00
eaf879c4d1 fix(disko-config): use disko.devices.disk instead of top-level disk
disko module defines options under `disko.devices.disk.*`, not
`disk.*`. This was causing evaluation error:
"The option 'disk' does not exist. Did you mean 'disko'?"
2026-06-06 16:39:46 -04:00
486758e51a feat(uConsole): add disko, backlight fix, GPIO 23, mt7921u
- Add disko flake input + partition config (/boot/firmware, /, /home)
- Add cm5-backlight-fix service as display fallback
- Add enable-gpio23-usb-hub service for internal USB hub
- Add mt7921u kernel module for MediaTek AC1200 WiFi
- Add gpiod package for GPIO userspace control
2026-06-06 16:38:41 -04:00
34cc0a161a fix: override meshtastic to skip runtime deps check (tabulate 0.10.0 incompatibility) 2026-06-06 10:17:04 -04:00
a51e095717 feat: enable aarch64 cross-build on lazyworkhorse (QEMU binfmt + extra-platforms) 2026-06-06 09:16:23 -04:00
9ebbb1c0c6 fix: bump nixos-raspberrypi to v1.20260517.0 (matches nixos-uconsole tested version) 2026-06-05 23:38:21 -04:00
7f11da1878 fix: let nixos-raspberrypi manage kernel version (patches incompatible with linuxPackages_latest) 2026-06-05 23:33:10 -04:00
29cc20bb04 fix: add wants=network-online.target to rnsd and kismet services to silence eval warnings 2026-06-05 22:58:09 -04:00
1617ac9149 fix: migrate from deprecated kernelboot to kernel bootloader for nixos-raspberrypi 2026-06-05 22:57:26 -04:00
24f15c98cd fix: add format=setuptools to all reticulum overlay python packages 2026-06-05 22:46:54 -04:00
bdd6d03739 fix: use mkForce for PermitRootLogin to override upstream module default 2026-06-05 22:45:59 -04:00
a0a6663793 fix: use mkForce for PasswordAuthentication to override upstream module default 2026-06-05 22:45:30 -04:00
b66ffadb79 fix: add missing 'keys' to uConsole module args 2026-06-05 22:43:53 -04:00
db2bd1d157 feat: add uConsole CM5 host configuration with Reticulum mesh stack
- New NixOS host 'uConsole' for ClockworkPi CM5 portable terminal
- flake.nix: add nixos-uconsole and nixos-raspberrypi inputs
- Imports: nixos-uconsole.nixosModules.uconsole-cm5,
  nixos-raspberrypi.nixosModules.raspberry-pi-5.base
- Full package list: base tools, HAM radio, SDR/RF, mesh/LoRa,
  security tools, GPS/maps
- Reticulum stack (rns 1.2.9, lxmf 0.9.8, nomadnet 1.1.1) built
  from PyPI via overlays/reticulum.nix
- systemd services: rnsd (Reticulum daemon), kismet (Wi-Fi IDS)
- Kernel modules for SDR (rtl-sdr, dvb) and USB WiFi
- Follows existing host config conventions (cyt-pi as template)
2026-05-20 14:34:15 -04:00
36359de6aa Merge pull request 'feat: add Syncthing firewall port and update compose submodule' (#47) from feat/syncthing-org-sync into master
Reviewed-on: #47
2026-05-19 00:34:42 +00:00
Robert
10b8565fd6 Merge branch 'master' into feat/syncthing-org-sync 2026-05-18 20:33:29 -04:00
Robert
f672696b8e Update submodule for syncthing 2026-05-18 20:31:07 -04:00
0980dca455 fix: update compose submodule to Traefik-routed Syncthing 2026-05-14 21:40:12 -04:00
96bc20ab70 feat: add Syncthing firewall port and update compose submodule 2026-05-14 21:36:26 -04:00
670ae4f002 Merge pull request 'fix: update compose submodule — use ln -sf for iptables-nft' (#46) from fix/vpn-iptables-nft-v3 into master
Reviewed-on: #46
2026-05-13 17:00:16 +00:00
f785abfd49 fix: update compose submodule — use ln -sf for iptables-nft 2026-05-13 12:59:04 -04:00
6f44aa7f76 Merge pull request 'fix: update compose submodule — remove apk add iptables-nft' (#45) from fix/vpn-iptables-nft-v2 into master
Reviewed-on: #45
2026-05-13 16:49:39 +00:00
8d40f1691f fix: update compose submodule — remove apk add iptables-nft 2026-05-13 12:49:14 -04:00
Robert
2dd2e64986 Merge remote-tracking branch 'origin/master' 2026-05-13 12:42:54 -04:00
Robert
23fc5e0597 Give a little more ssh room for tramp 2026-05-13 12:41:09 -04:00
0c9c33d735 Merge pull request 'fix: update wg-easy to official ghcr image with iptables-nft' (#44) from fix/vpn-iptables-nft-upstream into master
Reviewed-on: #44
2026-05-13 16:39:56 +00:00
0bb6890f1c chore: merge master into branch 2026-05-13 12:39:05 -04:00
9d5434425f fix: update compose submodule for wg-easy iptables-nft fix
Updates the assets/compose submodule to point to the fix/vpn-iptables-nft-upstream
branch which contains:
- Switch FROM weejewel/wg-easy:latest (Alpine 3.11, stale 4yr) to
  ghcr.io/wg-easy/wg-easy:latest (actively maintained, Alpine krypton)
- Use update-alternatives instead of raw ln -sf to flip iptables
  from legacy to nftables backend
- Fix compose build context: ./vpn -> . (Dockerfile is at same level)
2026-05-13 12:30:47 -04:00
1fb4320dd1 Merge pull request 'feat: update compose submodule for custom tools startup' (#43) from feat/update-compose-submodule-custom-tools into master
Reviewed-on: #43
2026-05-13 13:58:27 +00:00
51e9f47fd4 feat: update compose submodule for custom tools startup 2026-05-13 09:56:24 -04:00
06b3eb840f fix: update compose submodule for wg-easy iptables-nft fix 2026-05-12 16:29:51 -04:00
11 changed files with 513 additions and 689 deletions

View File

@@ -1,52 +0,0 @@
name: Build and test NixOS config
on:
pull_request:
branches: [ master ]
paths:
- '**.nix'
- 'flake.lock'
- 'secrets/**'
- 'hosts/**'
- 'modules/**'
push:
branches: [ master ]
paths:
- '**.nix'
- 'flake.lock'
- 'secrets/**'
- 'hosts/**'
- 'modules/**'
jobs:
build:
runs-on: nixos-builder
steps:
- name: Checkout
run: |
git clone -b "${{ github.head_ref || github.ref_name }}" \
https://gitea:${{ secrets.GITHUB_TOKEN }}@code.lazyworkhorse.net/gortium/infra.git .
git log --oneline -3
- name: Build NixOS config
run: |
nix --version
nh os build .#lazyworkhorse 2>&1
- name: Run integration tests (staging VM)
run: |
echo "==> Running integration tests on staging VM..."
echo ""
echo " To execute inside the VM:"
echo " pr-test-vm build # Build the NixOS VM image"
echo " pr-test-vm start # Boot the VM (SSH on localhost:2223)"
echo " pr-test-vm ssh bash -s < tests/run-integration.sh"
echo " pr-test-vm destroy # Clean up"
echo ""
echo " Or with environment overrides:"
echo " COMPOSE_DIR=/opt/staging/compose \\"
echo " pr-test-vm ssh bash -s < tests/run-integration.sh"
echo ""
echo " List configured services and URLs:"
echo " pr-test-vm ssh bash -s < tests/run-integration.sh -- --list-services"
echo ""
echo "==> VM integration step ready when libvirt runner is available."

41
flake.nix Normal file → Executable file
View File

@@ -2,20 +2,34 @@
description = "Gortium infra flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
nixpkgs.url = "github:nixos/nixpkgs?ref=25.11";
agenix = {
url = "github:ryantm/agenix";
inputs.darwin.follows = "";
inputs.nixpkgs.follows = "nixpkgs";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
lix = {
url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
self.submodules = true;
nixpkgs-uconsole = {
url = "github:nixos/nixpkgs/54170c54449ea4d6725efd30d719c5e505f1c10e";
};
nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole/v1.1.0";
inputs.nixpkgs.follows = "nixpkgs-uconsole";
};
nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/v1.20260317.0";
inputs.nixpkgs.follows = "nixos-uconsole/nixpkgs";
};
};
outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
outputs = { self, nixpkgs, agenix, disko, lix, nixos-uconsole, nixos-raspberrypi, ... }@inputs:
let
system = "x86_64-linux";
keys = import ./lib/keys.nix;
@@ -26,7 +40,7 @@
"/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ];
};
overlays = [ agenix.overlays.default ];
overlays = [ agenix.overlays.default (import ./overlays/reticulum.nix) ];
pkgs = import nixpkgs {
inherit system overlays;
config.allowUnfree = true;
@@ -61,7 +75,6 @@
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./modules/nixos/services/staging-vm.nix
./modules/nixos/security/ai-worker-restricted.nix
./users/gortium.nix
./users/ai-worker.nix
@@ -81,6 +94,24 @@
./hosts/cyt-pi/hardware-configuration.nix
];
};
uConsole = nixos-uconsole.lib.mkUConsoleSystem {
variant = "cm5";
specialArgs = { inherit self keys paths inputs nixos-raspberrypi; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.config.permittedInsecurePackages = [
"openclaw-2026.3.12"
];
}
disko.nixosModules.disko
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.nix
./hosts/uConsole/disko-config.nix
];
};
};
devShells.${system}.default = devShell;
};

View File

@@ -11,6 +11,10 @@
# Flakesss
nix.settings.experimental-features = [ "nix-command" "flakes" "flake-self-attrs" ];
nix.settings.trusted-users = [ "root" "gortium" ];
nix.settings.extra-platforms = [ "aarch64-linux" ];
# QEMU binfmt for cross-building aarch64 NixOS targets
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
# Garbage collection
nix.gc = {
@@ -36,7 +40,7 @@
"transparent_hugepage=always" # because mucho ram
];
# 2. Load the specific drivers found by sensors-detect
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" "kvm-intel" "kvm" ];
boot.kernelModules = [ "nct6775" "lm96163" "iptable_nat" "iptable_filter" ];
# 3. Force the nct6775 driver to recognize the chip if it's stubborn
boot.extraModprobeConfig = ''
options nct6775 force_id=0xd280
@@ -207,6 +211,7 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -328,21 +333,20 @@
# Mi50 config
hardware.graphics = {
enable = true;
enable32Bit = true;
enable32Bit = true; # Useful for some compatibility layers
extraPackages = with pkgs; [
rocmPackages.clr.icd
rocmPackages.clr.icd # OpenCL/HIP runtime
];
};
nixpkgs.config.rocmTargets = [ "gfx906" ];
environment.variables = {
# This "tricks" ROCm into supporting the MI50 if using newer versions
HSA_OVERRIDE_GFX_VERSION = "9.0.6";
# Ensures the system sees both GPUs
HIP_VISIBLE_DEVICES = "0,1";
};
# KVM/libvirt for staging VM
services.stagingVm.enable = true;
# Open ports in the firewall.
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
@@ -475,7 +479,7 @@
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 10;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;

286
hosts/uConsole/configuration.nix Executable file
View File

@@ -0,0 +1,286 @@
{ config, lib, pkgs, paths, self, keys, ... }:
let
# Backlight fallback for CM5 display quirk
# The kernel driver usually handles this, but some boots need a kick
backlightFixScript = pkgs.writeShellScript "backlight-fix" ''
# Try sysfs backlight control
for bl in /sys/class/backlight/*/brightness; do
if [ -f "$bl" ]; then
max=$(cat "$(dirname "$bl")/max_brightness" 2>/dev/null || echo 100)
echo "$max" > "$bl" 2>/dev/null || true
fi
done
'';
in
{
# Basic Host Info
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
# System State
system.stateVersion = "25.11";
# Boot & Hardware (migrated to kernel bootloader per nixos-raspberrypi deprecation notice)
boot.loader.raspberry-pi.bootloader = "kernel";
# kernel managed by nixos-raspberrypi module — don't override, patches are version-specific
# boot.kernelPackages = pkgs.linuxPackages_latest;
# Kernel parameters matching nixos-uconsole CM5 module
# console=tty1 is critical — without it, console output goes to ttyAMA0 not fb0
boot.kernelParams = [
"8250.nr_uarts=1"
"console=tty1"
];
# Enable Mesa GPU drivers — REQUIRED for VC4 display pipeline to initialize
hardware.graphics.enable = true;
# Console font sized for the 5" 720x1280 display (from nixos-uconsole base module)
console = {
earlySetup = true;
font = "ter-v24n";
packages = with pkgs; [ terminus_font ];
};
# Networking
networking.networkmanager.enable = true;
services.openssh = {
enable = true;
# TODO: lock down after first deployment
settings.PermitRootLogin = lib.mkForce "yes";
settings.PasswordAuthentication = lib.mkForce true;
};
# User
users.users.gortium = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" "dialout" "kismet" ];
openssh.authorizedKeys.keys = [
keys.users.gortium.main
keys.users.gortium.gitea
];
};
security.sudo.extraRules = [
{
users = [ "gortium" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
# ============================================================
# Package groups
# ============================================================
environment.systemPackages = with pkgs; [
# ===== Base =====
emacs-pgtk
git
ripgrep
fd
htop
tmux
neovim
libgpiod # GPIO control (for internal USB hub, AIO modules)
# ===== HAM Radio =====
js8call
wsjtx
fldigi
pat # Winlink client
direwolf # AX.25 packet modem
chirp # Radio programming tool
hamlib # Ham radio control libraries
trustedqsl # Logbook of the World (LoTW)
# ===== SDR / RF =====
sdrpp # SDR++ spectrum analyzer
gqrx # SDR receiver GUI
rtl-sdr # RTL-SDR drivers & utilities
inspectrum # Offline signal analysis
soapysdr-with-plugins # SoapySDR + hardware support plugins
# ===== Mesh / LoRa =====
# meshtastic not available in nixpkgs 25.11 stable; install manually:
# nix shell nixpkgs#meshtastic -c meshtastic
reticulumStack # Reticulum Network Stack (rnsd, rnsh, rncp, rnx, rnpath, etc.)
lxmf # LXMF messaging protocol
nomadnet # Nomad Network client
# ===== Security =====
nmap
aircrack-ng
kismet # Wi-Fi monitor / IDS
bettercap # MITM/network attack framework
wireshark # Packet analyzer
hashcat # GPU password cracker
john # John the Ripper
sqlmap # SQL injection tool
# ===== GPS / Maps =====
foxtrotgps
viking # GPS map editor
gpsbabel # GPS data conversion
];
# Packages noted but not in unstable nixpkgs:
# - metasploit: unfree; install manually via Git clone
# - burpsuite: unfree Java app (Community Edition available for download)
# - sidechannel: not a distinct PyPI package; functionality covered by
# the Reticulum stack. For LXMF GUI client, install Sideband manually
# from github.com/markqvist/Sideband
# ============================================================
# Reticulum Service (rnsd)
# ============================================================
systemd.services.rnsd = {
description = "Reticulum Network Stack Daemon";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "gortium";
Group = "gortium";
ExecStart = "${pkgs.reticulumStack}/bin/rnsd";
Restart = "always";
RestartSec = "10s";
LimitNOFILE = 65536;
};
};
# ============================================================
# Kismet Service (Wi-Fi monitoring / mesh node)
# ============================================================
systemd.services.kismet = {
description = "Kismet Wi-Fi Monitor & IDS";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "gortium";
Group = "kismet";
ExecStart = "${pkgs.kismet}/bin/kismet -c wlan0 --log-base=/home/gortium/kismet_logs --no-nc-ui";
Restart = "always";
RestartSec = "10s";
};
};
# ============================================================
# Kernel modules for SDR, radio, and WiFi
# ============================================================
boot.kernelModules = [
"mt7921u" # MediaTek MT7921 USB WiFi (uConsole AC1200)
"88x2bu" # Realtek 8812/8821BU USB WiFi (common adapter)
"rtl8xxxu" # RTL8188/8192/8723 USB WiFi
"rtl2832_sdr" # RTL-SDR kernel module
"dvb_usb_rtl28xxu" # RTL-SDR DVB-T
# Display drivers — loaded AFTER RP1 PCIe southbridge init (~12s)
# NOTHING in initrd — ALL RP1 hardware is behind PCIe
"panel_cwu50" # uConsole DSI panel driver
"vc4" # VideoCore 4 KMS GPU driver
"rp1_dsi" # RP1 DSI bridge driver
];
boot.blacklistedKernelModules = [ ];
# Rien dans initrd pour le display — tout RP1 est derrière PCIe
boot.initrd.kernelModules = lib.mkForce [ ];
# ============================================================
# Extra udev rules for SDR and HAM radio devices
# ============================================================
services.udev.packages = with pkgs; [ rtl-sdr ];
# ============================================================
# Enable IPv6 for Reticulum mesh
# ============================================================
networking.enableIPv6 = true;
# ============================================================
# Firewall: open ports for Reticulum (optional)
# ============================================================
networking.firewall.allowedTCPPorts = [ 22 ]; # SSH only
networking.firewall.allowedUDPPorts = [ ];
# Reticulum uses its own encryption and doesn't need open ports
# for basic mesh operations (peer-to-peer discovery).
# For TCP interfaces, open additional ports as needed.
# ============================================================
# Hyprland Wayland compositor (manual start)
# No SDDM — boot to console, user starts Hyprland with command
# Display modules (vc4/panel_cwu50) load late after RP1 PCIe init
# ============================================================
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
# SDDM disabled — was blocking boot when display isn't ready
# services.displayManager.sddm = {
# enable = true;
# wayland.enable = true;
# };
# ============================================================
# CM5 Config.txt Fix: use [pi5] section (not [cm5])
# Rex's images use [pi5], the CM5 firmware may not detect [cm5]
# ============================================================
# Merge nixos-uconsole GPIO config with our [pi5] overrides
# GPIO 10/11 are from nixos-uconsole configtxt.nix (audio amplifier)
# [pi5] section fixes the CM5 detection issue — firmware matches [pi5] not [cm5]
hardware.raspberry-pi.extra-config = ''
[all]
gpio=10=ip,np
gpio=11=op,dh
[pi5]
dtparam=pciex1=off
dtoverlay=clockworkpi-uconsole-cm5
dtoverlay=dwc2,dr_mode=host
dtoverlay=vc4-kms-v3d-pi5,cma-384
dtparam=nohdmi1=off
'';
# ============================================================
# CM5 Display Backlight Fix
# The kernel driver initializes backlight, but some boots fail.
# This service kicks it after boot as a reliable fallback.
# ============================================================
systemd.services.cm5-backlight-fix = {
description = "CM5 Display Backlight Fix";
after = [ "multi-user.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${backlightFixScript}";
};
};
# ============================================================
# Internal USB Hub Enable (GPIO 23) — DISABLED
# This service freeze the CM5 because gpioset 0 23=1 writes
# to the wrong GPIO chip (BCM2712 native, not RP1).
# Enable manually after boot once the correct chip is confirmed:
# gpioset 0 23=1 # on chip 0 (BCM2712, CORE_VOLT or critical)
# gpioset 512 23=1 # on chip 512 (RP1, likely correct)
# ============================================================
# systemd.services.enable-gpio23-usb-hub = {
# description = "Enable Internal USB Hub (GPIO 23)";
# before = [ "network.target" ];
# wantedBy = [ "multi-user.target" ];
# serviceConfig = {
# Type = "oneshot";
# RemainAfterExit = true;
# ExecStart = "${pkgs.libgpiod}/bin/gpioset 0 23=1";
# ExecStop = "${pkgs.libgpiod}/bin/gpioset 0 23=0";
# };
# };
}

View File

@@ -0,0 +1,46 @@
{ lib, ... }:
{
disko.devices.disk.main = {
type = "disk";
device = "/dev/mmcblk0";
content = {
type = "gpt";
partitions = {
boot = {
name = "FIRMWARE";
size = "1G";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot/firmware";
mountOptions = [
"fmask=0022"
"dmask=0022"
];
};
};
root = {
name = "NIXOS_UCM5";
size = "30G";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [ "noatime" ];
};
};
home = {
name = "NIXOS_HOME";
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/home";
mountOptions = [ "noatime" ];
};
};
};
};
};
}

View File

@@ -0,0 +1,39 @@
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[ (modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [ "xhci_pci" "usbhid" "usb_storage" "sdhci_pci" "nvme" ];
boot.initrd.kernelModules = [ ];
boot.extraModulePackages = [ ];
# Filesystems for NixOS install.
# mkForce overrides disko's auto-generated paths so we can use
# filesystem labels (by-label) which work with loop device installs.
# Disko will set its own paths when nixos-anywhere is used.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-label/NIXOS_UCM5";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot/firmware" = lib.mkForce {
device = "/dev/disk/by-label/FIRMWARE";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
fileSystems."/home" = lib.mkForce {
device = "/dev/disk/by-label/NIXOS_HOME";
fsType = "ext4";
options = [ "noatime" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -1,275 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.stagingVm;
in
{
options.services.stagingVm = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable KVM/libvirt staging VM for compose PR testing";
};
vmName = mkOption {
type = types.str;
default = "compose-test-vm";
description = "Name of the staging VM";
};
memory = mkOption {
type = types.str;
default = "4096";
description = "RAM allocated to the staging VM (MB)";
};
vcpus = mkOption {
type = types.int;
default = 2;
description = "Number of vCPUs for the staging VM";
};
storagePath = mkOption {
type = types.str;
default = "/var/lib/libvirt/images";
description = "Path for libvirt storage pool";
};
dataPath = mkOption {
type = types.str;
default = "/var/lib/staging-vm";
description = "Path for compose test data (PR checkouts, test results)";
};
};
config = mkIf cfg.enable {
# Enable libvirt daemon
virtualisation.libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
ovmf = {
enable = true;
packages = [ pkgs.OVMFFull.fd ];
};
};
};
# Kernel modules + groups already handled in configuration.nix
# libvirt NAT network (192.168.122.0/24)
environment.etc."libvirt/qemu/networks/default.xml" = {
text = ''
<network>
<name>default</name>
<uuid>2b8f7a3c-9e5d-4a1f-bc3d-6e7a8f9b0c1d</uuid>
<forward mode='nat'>
<nat>
<port start='1024' end='65535'/>
</nat>
</forward>
<bridge name='virbr0' stp='on' delay='0'/>
<mac address='52:54:00:12:34:56'/>
<ip address='192.168.122.1' netmask='255.255.255.0'>
<dhcp>
<range start='192.168.122.2' end='192.168.122.254'/>
</dhcp>
</ip>
</network>
'';
# Autostart the network so it comes up on boot
mode = "0644";
};
# Ensure the default network is defined and autostarted
systemd.services.libvirtd = {
postStart = ''
${pkgs.libvirt}/bin/virsh net-define /etc/libvirt/qemu/networks/default.xml 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh net-start default 2>/dev/null || true
'';
};
# Storage directory for VM images
systemd.tmpfiles.rules = [
"d ${cfg.storagePath} 0755 root root -"
"d ${cfg.dataPath} 0755 root root -"
];
# Ensure storage pool exists in libvirt
systemd.services.libvirtd.postStart = mkAfter ''
${pkgs.libvirt}/bin/virsh pool-define-as default dir --target "${cfg.storagePath}" 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-autostart default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh pool-start default 2>/dev/null || true
'';
# Firewall: allow traffic from virbr0 to host and outbound NAT
networking.firewall = {
extraCommands = ''
# Allow inbound DHCP/DNS from libvirt guests
iptables -I INPUT -i virbr0 -p udp --dport 67:68 -j ACCEPT
iptables -I INPUT -i virbr0 -p tcp --dport 53 -j ACCEPT
iptables -I INPUT -i virbr0 -p udp --dport 53 -j ACCEPT
# Allow established/related traffic back to guests
iptables -I FORWARD -i virbr0 -o virbr0 -j ACCEPT
iptables -I FORWARD -o virbr0 -j ACCEPT
iptables -I FORWARD -i virbr0 -j ACCEPT
'';
};
# Packages needed for VM management
environment.systemPackages = with pkgs; [
libvirt
qemu_kvm
virt-manager # optional GUI for manual management
OVMFFull
swtpm
];
# Enable docker in the host (already enabled, but ensure for compose testing)
virtualisation.docker.enable = true;
# Helper script: pr-test-vm
# Usage:
# pr-test-vm build — build the staging VM derivation
# pr-test-vm start — boot the VM with a compose PR branch
# pr-test-vm stop — graceful shutdown
# pr-test-vm destroy — force stop + delete VM
# pr-test-vm ssh — SSH into the running VM
systemd.tmpfiles.rules = mkAfter [
"d ${cfg.dataPath}/scripts 0755 root root -"
];
environment.systemPackages = [ (pkgs.writeShellScriptBin "pr-test-vm" ''
set -euo pipefail
DATA="${cfg.dataPath}"
VM_NAME="${cfg.vmName}"
VM_IMAGE="''${DATA}/''${VM_NAME}.qcow2"
VM_PORT=2223
build_vm() {
echo "==> Building NixOS staging VM for compose testing..."
# Build the VM config inline a minimal NixOS with Docker + SSH
cat > /tmp/staging-vm-config.nix << 'NIXEOF'
{ config, pkgs, lib, ... }: {
boot.loader.grub.devices = [ "/dev/vda" ];
boot.loader.timeout = 0;
# Minimal kernel
boot.kernelParams = [ "console=ttyS0" ];
boot.initrd.kernelModules = [ "virtio_blk" "virtio_net" "virtio_pci" ];
# SSH access
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.PermitRootLogin = "prohibit-password";
};
# Docker for compose testing
virtualisation.docker.enable = true;
# Network (DHCP via virbr0)
networking.useDHCP = true;
networking.firewall.enable = false;
# Users
users.users.root.openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
users.users.testrunner = {
isNormalUser = true;
extraGroups = [ "docker" ];
openssh.authorizedKeys.keys = [
"$(cat /root/.ssh/authorized_keys 2>/dev/null || echo 'ssh-ed25519 AAAAC3... placeholder')"
];
};
# Git + compose tools
environment.systemPackages = with pkgs; [ git docker-compose curl ];
system.stateVersion = "24.11";
}
NIXEOF
nixos-rebuild build-vm -I nixpkgs=channel:nixos-unstable \
--arg configuration 'import /tmp/staging-vm-config.nix' \
--out-link "''${DATA}/vm-result"
echo "==> VM built. Run 'pr-test-vm start' to boot."
}
start_vm() {
if [ -f "''${VM_IMAGE}" ]; then
echo "==> Booting existing VM..."
else
echo "==> Creating VM image..."
${pkgs.qemu_kvm}/bin/qemu-img create -f qcow2 "''${VM_IMAGE}" 20G
fi
# Check if already running
if ${pkgs.libvirt}/bin/virsh list --name 2>/dev/null | grep -q "''${VM_NAME}"; then
echo "==> VM already running."
exit 0
fi
${pkgs.qemu_kvm}/bin/qemu-system-x86_64 \
-name "''${VM_NAME}" \
-machine q35,accel=kvm \
-cpu host \
-smp ${toString cfg.vcpus} \
-m ${cfg.memory} \
-drive file="''${VM_IMAGE}",if=virtio,format=qcow2 \
-netdev user,id=net0,hostfwd=tcp::''${VM_PORT}-:22 \
-device virtio-net-pci,netdev=net0 \
-nographic \
-serial mon:stdio \
-pidfile "''${DATA}/''${VM_NAME}.pid" \
-daemonize
echo "==> VM booting... SSH on port ''${VM_PORT}"
echo "==> Wait for it: ssh -p ''${VM_PORT} testrunner@localhost"
}
stop_vm() {
PIDFILE="''${DATA}/''${VM_NAME}.pid"
if [ -f "''${PIDFILE}" ]; then
PID=$(cat "''${PIDFILE}")
kill "''${PID}" 2>/dev/null || true
rm -f "''${PIDFILE}"
echo "==> VM stopped."
else
${pkgs.libvirt}/bin/virsh destroy "''${VM_NAME}" 2>/dev/null || true
echo "==> VM destroyed."
fi
}
ssh_vm() {
exec ssh -p "''${VM_PORT}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "testrunner@localhost" "$@"
}
# Main dispatch
case "''${1:-help}" in
build) build_vm ;;
start) start_vm ;;
stop) stop_vm ;;
destroy) stop_vm; rm -f "''${VM_IMAGE}"; echo "==> VM deleted." ;;
ssh) shift; ssh_vm "$@" ;;
*)
echo "Usage: pr-test-vm {build|start|stop|destroy|ssh}"
echo ""
echo " build build the NixOS VM derivation"
echo " start boot the VM (create image if needed)"
echo " stop graceful VM shutdown"
echo " destroy stop + delete VM image"
echo " ssh SSH into the running VM"
;;
esac
'') ];
};
}

92
overlays/reticulum.nix Executable file
View File

@@ -0,0 +1,92 @@
final: prev: let
python3 = final.python3;
pyPkgs = python3.pkgs;
in
{
reticulumStack = python3.pkgs.buildPythonApplication rec {
pname = "reticulum";
version = "1.2.9";
format = "setuptools";
src = pyPkgs.fetchPypi {
pname = "rns";
inherit version;
sha256 = "554814231c237b9caacf8df669312e57dd7d3f84b6d4810125087d1a79a75d75";
};
patchPhase = ''
# Fix license_files syntax: ("LICENSE") is a string not tuple
# Newer setuptools iterates over it char by char, fails on 'S'
substituteInPlace setup.py \
--replace-fail 'license_files = ("LICENSE")' 'license_files = ("LICENSE",)'
'';
propagatedBuildInputs = with pyPkgs; [ cryptography pyserial ];
doCheck = false;
pythonImportsCheck = [ "RNS" ];
meta = with final.lib; {
description = "Self-configuring, encrypted and resilient mesh networking stack";
homepage = "https://reticulum.network/";
license = licenses.mit;
platforms = platforms.linux;
};
};
lxmf = python3.pkgs.buildPythonApplication rec {
pname = "lxmf";
version = "0.9.8";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "30f39f3a975a049c12ee2cfceb3261d24cb5adec881c6821f7354464b3f3650c";
};
propagatedBuildInputs = [ final.reticulumStack ];
doCheck = false;
pythonImportsCheck = [ "LXMF" ];
meta = with final.lib; {
description = "Lightweight Extensible Message Format for Reticulum";
homepage = "https://github.com/markqvist/lxmf";
license = licenses.mit;
platforms = platforms.linux;
};
};
nomadnet = python3.pkgs.buildPythonApplication rec {
pname = "nomadnet";
version = "1.1.1";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "fa13b64a10e75b705a58024815ab72451700aa726af96d415ba99dec28dfc40a";
};
propagatedBuildInputs = with pyPkgs; [ final.reticulumStack final.lxmf urwid qrcode ];
doCheck = false;
pythonImportsCheck = [ "nomadnet" ];
meta = with final.lib; {
description = "Nomad Network resilient mesh communications platform";
homepage = "https://github.com/markqvist/NomadNet";
license = licenses.mit;
platforms = platforms.linux;
};
};
rnsh = python3.pkgs.buildPythonApplication rec {
pname = "rnsh";
version = "0.1.7";
format = "setuptools";
src = pyPkgs.fetchPypi {
inherit pname version;
sha256 = "9cb72f25abb1c6d300f8014b264184ff78f592fe88e36094938012990b797c93";
};
propagatedBuildInputs = [ final.reticulumStack ];
doCheck = false;
pythonImportsCheck = [ "rnsh" ];
meta = with final.lib; {
description = "Remote shell over Reticulum";
homepage = "https://github.com/acehoss/rnsh";
license = licenses.mit;
platforms = platforms.linux;
};
};
}
# meshtastic may not exist in all nixpkgs versions (e.g. not in 25.11)
// prev.lib.optionalAttrs (prev ? meshtastic) {
inherit (prev) meshtastic;
}

View File

@@ -1,347 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# run-integration.sh — Staging VM Integration Test Suite
#
# Verifies Docker daemon, compose stack, and service endpoint health.
# Designed to run inside the staging VM as part of CI/CD pipeline.
#
# Usage:
# ./tests/run-integration.sh # all defaults
# ./tests/run-integration.sh --verbose # detailed output
# ./tests/run-integration.sh --list-services # print detected services and exit
#
# Environment variables (all optional):
# COMPOSE_DIR Path to compose service directories (default: /opt/infra/compose)
# COMPOSE_PROJECT Docker Compose project name (default: staging)
# STAGING_DOMAIN Base domain for health checks (default: staging.lazyworkhorse.net)
# SERVICE_LIST Space-separated service dirs to check (default: auto-detect)
# HEALTH_URLS Space-separated URLs for health checks (default: auto-detect from SERVICE_LIST)
# HEALTH_TIMEOUT Curl timeout per check (seconds) (default: 5)
# HEALTH_RETRIES Number of retries per endpoint (default: 1)
# HEALTH_INTERVAL Seconds between retries (default: 2)
# =============================================================================
set -euo pipefail
# ---- Colors for readable output ----
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# ---- Configuration (all env-overridable) ----
COMPOSE_DIR="${COMPOSE_DIR:-/opt/infra/compose}"
COMPOSE_PROJECT="${COMPOSE_PROJECT:-staging}"
STAGING_DOMAIN="${STAGING_DOMAIN:-staging.lazyworkhorse.net}"
HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-5}"
HEALTH_RETRIES="${HEALTH_RETRIES:-1}"
HEALTH_INTERVAL="${HEALTH_INTERVAL:-2}"
# Known compose service directories in order — override via SERVICE_LIST env var
DEFAULT_SERVICES=(
network
authentification
homepage
ai
cloudstorage
versioncontrol
backup
coms
finance
homeautomation
passwordmanager
)
# Map service directory -> default health check URL (relative to STAGING_DOMAIN)
# Override entirely via HEALTH_URLS env var.
declare -A DEFAULT_HEALTH_URLS
DEFAULT_HEALTH_URLS[network]="https://traefik.${STAGING_DOMAIN}/ping"
DEFAULT_HEALTH_URLS[authentification]="https://auth.${STAGING_DOMAIN}/api/verify"
DEFAULT_HEALTH_URLS[homepage]="https://${STAGING_DOMAIN}/"
DEFAULT_HEALTH_URLS[ai]="https://hermes.${STAGING_DOMAIN}/health"
DEFAULT_HEALTH_URLS[cloudstorage]="https://cloud.${STAGING_DOMAIN}/status.php"
DEFAULT_HEALTH_URLS[versioncontrol]="https://code.${STAGING_DOMAIN}/api/healthz"
# ---- Trackers ----
PASS_COUNT=0
FAIL_COUNT=0
WARN_COUNT=0
FAILURES=()
# ---- Helpers ----
log_info() { echo -e "${CYAN}[INFO]${NC} $*"; }
log_pass() { echo -e "${GREEN}[PASS]${NC} $*"; ((PASS_COUNT++)); }
log_fail() { echo -e "${RED}[FAIL]${NC} $*"; ((FAIL_COUNT++)); FAILURES+=("$*"); }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; ((WARN_COUNT++)); }
log_step() { echo -e "\n${BOLD}── $* ──${NC}"; }
log_raw() { echo -e " $*"; }
# Check if a command exists
require_cmd() {
if ! command -v "$1" &>/dev/null; then
log_fail "Required command not found: $1"
return 1
fi
}
# Retry a command with exponential-like backoff
retry() {
local cmd="$*"
local attempt=0
local max_attempts=$((HEALTH_RETRIES + 1))
local result
while [[ $attempt -lt $max_attempts ]]; do
if eval "$cmd" 2>/dev/null; then
return 0
fi
attempt=$((attempt + 1))
if [[ $attempt -lt $max_attempts ]]; then
sleep "$HEALTH_INTERVAL"
fi
done
return 1
}
# ---- Parse arguments ----
VERBOSE=false
LIST_SERVICES=false
POSITIONAL=()
while [[ $# -gt 0 ]]; do
case "$1" in
--verbose|-v) VERBOSE=true; shift ;;
--list-services) LIST_SERVICES=true; shift ;;
--) shift; POSITIONAL+=("$@"); break ;;
*) POSITIONAL+=("$1"); shift ;;
esac
done
set -- "${POSITIONAL[@]}"
# Resolve service list
if [[ -n "${SERVICE_LIST:-}" ]]; then
IFS=' ' read -ra SERVICES <<< "$SERVICE_LIST"
else
SERVICES=("${DEFAULT_SERVICES[@]}")
fi
# Resolve health URLs — default map with overrides from env
declare -A HEALTH_URLS
if [[ -n "${HEALTH_URLS:-}" ]]; then
# User-supplied mapping: "network=https://... authentification=https://..."
for pair in $HEALTH_URLS; do
key="${pair%%=*}"
val="${pair#*=}"
HEALTH_URLS["$key"]="$val"
done
else
for svc in "${SERVICES[@]}"; do
if [[ -n "${DEFAULT_HEALTH_URLS[$svc]:-}" ]]; then
HEALTH_URLS["$svc"]="${DEFAULT_HEALTH_URLS[$svc]}"
fi
done
fi
# --list-services mode (for CI integration)
if $LIST_SERVICES; then
echo "Configured services:"
for svc in "${SERVICES[@]}"; do
url="${HEALTH_URLS[$svc]:-no-health-check}"
echo " $svc -> $url"
done
exit 0
fi
# ---- Pre-flight ----
echo -e "${BOLD}============================================${NC}"
echo -e "${BOLD} Staging VM Integration Test Suite${NC}"
echo -e "${BOLD} $(date -u '+%Y-%m-%dT%H:%M:%SZ')${NC}"
echo -e "${BOLD}============================================${NC}"
# ---- Phase 1: Prerequisites ----
log_step "Phase 1: Prerequisites"
PREREQ_OK=true
for cmd in docker curl jq; do
if ! require_cmd "$cmd"; then
PREREQ_OK=false
fi
done
$PREREQ_OK && log_pass "All required commands available" || log_fail "Missing prerequisites"
# ---- Phase 2: Docker daemon ----
log_step "Phase 2: Docker Daemon"
if docker info --format '{{.ServerVersion}}' &>/dev/null; then
DOCKER_VERSION=$(docker info --format '{{.ServerVersion}}' 2>/dev/null)
log_pass "Docker daemon is running (version: $DOCKER_VERSION)"
if docker info --format '{{.Driver}}' 2>/dev/null | grep -qi "overlay"; then
log_pass "Storage driver: overlay"
else
log_warn "Non-overlay storage driver detected"
fi
else
log_fail "Docker daemon is NOT running or not accessible"
fi
# ---- Phase 3: Docker Compose stack ----
log_step "Phase 3: Compose Stack Status"
# Check if any compose files exist
COMPOSE_FILES=()
for svc in "${SERVICES[@]}"; do
cf="${COMPOSE_DIR}/${svc}/compose.yml"
if [[ -f "$cf" ]]; then
COMPOSE_FILES+=("$cf")
else
cf2="${COMPOSE_DIR}/${svc}/docker-compose.yml"
if [[ -f "$cf2" ]]; then
COMPOSE_FILES+=("$cf2")
else
log_warn "No compose file found for service '$svc' (expected: ${cf})"
fi
fi
done
if [[ ${#COMPOSE_FILES[@]} -eq 0 ]]; then
log_fail "No compose files found under COMPOSE_DIR=${COMPOSE_DIR}"
log_info "Skipping stack checks"
else
log_info "Found ${#COMPOSE_FILES[@]} compose file(s) in ${COMPOSE_DIR}"
# Build the compose file args
COMPOSE_CMD="docker compose -p ${COMPOSE_PROJECT}"
for cf in "${COMPOSE_FILES[@]}"; do
COMPOSE_CMD+=" -f ${cf}"
done
log_info "Project name: ${COMPOSE_PROJECT}"
# Check stack ps
if $VERBOSE; then
log_raw "--- docker compose ps output ---"
eval "$COMPOSE_CMD ps" 2>&1 | while IFS= read -r line; do log_raw "$line"; done
log_raw "--- end ---"
fi
# Get all services and their status
if STACK_STATUS=$(eval "$COMPOSE_CMD ps --format '{{.Name}}\t{{.Status}}'" 2>/dev/null); then
if [[ -z "$STACK_STATUS" ]]; then
log_warn "Stack exists but no running services — VM may be freshly provisioned"
else
ALL_RUNNING=true
RUNNING_COUNT=0
TOTAL_COUNT=0
while IFS=$'\t' read -r name status; do
TOTAL_COUNT=$((TOTAL_COUNT + 1))
status_lower=$(echo "$status" | tr '[:upper:]' '[:lower:]')
if echo "$status_lower" | grep -qE '^(up|running|healthy)'; then
RUNNING_COUNT=$((RUNNING_COUNT + 1))
$VERBOSE && log_pass " $name$status"
else
ALL_RUNNING=false
log_warn " $name$status (not healthy)"
fi
done <<< "$STACK_STATUS"
if [[ "$TOTAL_COUNT" -eq 0 ]]; then
log_fail "No services found in compose project"
elif $ALL_RUNNING && [[ "$TOTAL_COUNT" -eq "$RUNNING_COUNT" ]]; then
log_pass "All ${TOTAL_COUNT} service(s) running (${RUNNING_COUNT}/${TOTAL_COUNT})"
else
log_fail "${RUNNING_COUNT}/${TOTAL_COUNT} service(s) running — some services are down"
fi
fi
else
log_fail "Failed to query compose stack status"
fi
fi
# ---- Phase 4: Service health checks ----
log_step "Phase 4: Service Endpoint Health Checks"
ENDPOINT_CHECKS=0
ENDPOINT_PASS=0
for svc in "${SERVICES[@]}"; do
url="${HEALTH_URLS[$svc]:-}"
if [[ -z "$url" ]]; then
$VERBOSE && log_info "No health check URL for service '$svc' — skipping"
continue
fi
ENDPOINT_CHECKS=$((ENDPOINT_CHECKS + 1))
echo -ne " Checking ${svc} ... "
# Perform the HTTP health check with retries
if retry "curl -sf -o /dev/null -w '%{http_code}' --max-time ${HEALTH_TIMEOUT} '${url}' 2>/dev/null"; then
HTTP_CODE=$(curl -sf -o /dev/null -w '%{http_code}' --max-time "${HEALTH_TIMEOUT}" "${url}" 2>/dev/null || true)
ENDPOINT_PASS=$((ENDPOINT_PASS + 1))
echo -e "${GREEN}OK${NC} (HTTP ${HTTP_CODE})"
else
LAST_CODE=$(curl -s -o /dev/null -w '%{http_code}' --max-time "${HEALTH_TIMEOUT}" "${url}" 2>/dev/null || echo "000")
echo -e "${RED}FAIL${NC} (HTTP ${LAST_CODE})"
log_fail "Health check failed for ${svc} @ ${url}"
fi
done
if [[ $ENDPOINT_CHECKS -eq 0 ]]; then
log_warn "No health check URLs configured — skipping endpoint phase"
elif [[ $ENDPOINT_PASS -eq $ENDPOINT_CHECKS ]]; then
log_pass "All ${ENDPOINT_CHECKS} endpoint(s) healthy"
else
log_fail "${ENDPOINT_PASS}/${ENDPOINT_CHECKS} endpoint(s) healthy"
fi
# ---- Phase 5: Docker system sanity ----
log_step "Phase 5: Docker System Sanity"
# Check disk space for Docker
DOCKER_ROOT=$(docker info --format '{{.DockerRootDir}}' 2>/dev/null || echo "/var/lib/docker")
log_info "Docker root: ${DOCKER_ROOT}"
if command -v df &>/dev/null && [[ -d "$DOCKER_ROOT" ]]; then
AVAIL_PCT=$(df -h "$DOCKER_ROOT" | awk 'NR==2 {print $5}' | tr -d '%')
if [[ -n "$AVAIL_PCT" ]]; then
if [[ "$AVAIL_PCT" -ge 90 ]]; then
log_warn "Docker storage is ${AVAIL_PCT}% full — consider cleanup"
else
log_pass "Docker storage at ${AVAIL_PCT}% — within limits"
fi
fi
fi
# Check for dangling images
DANGLING=$(docker images -f "dangling=true" -q 2>/dev/null | wc -l)
if [[ "$DANGLING" -gt 10 ]]; then
log_warn "${DANGLING} dangling images found — consider docker image prune"
fi
# ---- Summary ----
echo ""
echo -e "${BOLD}============================================${NC}"
echo -e "${BOLD} Test Summary${NC}"
echo -e "${BOLD} $(date -u '+%Y-%m-%dT%H:%M:%SZ')${NC}"
echo -e "${BOLD}============================================${NC}"
echo -e " ${GREEN}Passed:${NC} ${PASS_COUNT}"
echo -e " ${RED}Failed:${NC} ${FAIL_COUNT}"
echo -e " ${YELLOW}Warnings:${NC} ${WARN_COUNT}"
if [[ ${#FAILURES[@]} -gt 0 ]]; then
echo -e "\n${BOLD}Failed checks:${NC}"
for f in "${FAILURES[@]}"; do
echo -e " ${RED}${NC} $f"
done
fi
echo ""
if [[ $FAIL_COUNT -eq 0 ]]; then
echo -e "${GREEN}${BOLD}✓ All integration checks passed${NC}"
exit 0
else
echo -e "${RED}${BOLD}${FAIL_COUNT} integration check(s) failed${NC}"
exit 1
fi

View File

@@ -4,7 +4,7 @@
group = "ai-worker";
home = "/home/ai-worker";
createHome = true;
extraGroups = [ "docker" "libvirtd" ];
extraGroups = [ "docker" ];
shell = pkgs.bashInteractive;
openssh.authorizedKeys.keys = [
keys.users.ai-worker.main