Compare commits

..

39 Commits

Author SHA1 Message Date
f9aec103c7 fix: update compose submodule — use native s6 multi-profile supervision
Updates the compose submodule to commit 3f82c9d on branch
fix/hermes-entrypoint-simple, which drops all custom entrypoint
scripts and uses the image's native s6-overlay profile supervision.

The five profiles (ashley, claire, finn, matt, paul) auto-start
on boot via the s6 reconciler reading their gateway_state.json.
2026-07-07 22:22:41 -04:00
60a90d5f98 Merge pull request 'fix: update compose submodule pin to include USER hermes fix' (#77) from local_changes into master
Reviewed-on: #77
2026-07-07 19:41:59 +00:00
2338eaf74c fix: update compose submodule to include USER hermes fix
The compose submodule now points to commit 54e0661 which adds the missing
USER hermes at the end of the Hermes Dockerfile. This ensures all container
processes run as uid 10000 instead of root, fixing the privilege leak where
background subprocesses escaped the gosu drop and created root-owned files
in ExoKortex that broke Syncthing sync.
2026-07-07 14:59:26 -04:00
Robert
df91a909b6 More local changes, mostly compose update and nixmate 2026-07-07 14:15:26 -04:00
Robert
2e236581f0 update lock 2026-06-15 13:20:15 -04:00
Robert
65587809aa Added opencode api key 2026-06-15 13:17:49 -04:00
Robert
2b1df578ac Deleted wrongly created file... 2026-06-15 13:16:58 -04:00
Robert
1c806e6390 Updated the compose pin 2026-06-15 13:15:12 -04:00
Robert
78ab440727 Split container secret in preparation to switching seperated env 2026-06-15 13:12:39 -04:00
Robert
ab7da92626 Added tmux, removed opencode, openclaw node 2026-06-11 15:27:08 -04:00
Robert
e3f7d18d8e Added paperclip and honcho secrets 2026-06-11 15:02:16 -04:00
5aca97e057 Merge pull request 'feat: add uConsole CM5 host with Reticulum mesh network support' (#61) from feat/uconsole-cm5-v3 into master
Reviewed-on: #61
2026-06-06 13:19:19 +00: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
20 changed files with 728 additions and 932 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."

View File

@@ -1,106 +0,0 @@
# ollama-gfx906/Dockerfile
#
# Custom ollama image with ROCm 6.1 + gfx906 (MI50) support.
# The official ollama/rocm image ships ROCm 7.2 which dropped gfx906.
# This uses v0.23.2's native CMake build system with AMDGPU_TARGETS including gfx906.
#
# Build: docker build -t ollama/ollama:rocm-gfx906 ai/ollama
FROM rocm/dev-ubuntu-22.04:6.1.2-complete AS builder
# Build dependencies (CMake, Ninja, Go)
ARG CMAKEVERSION=3.31.2
ARG NINJAVERSION=1.12.1
ARG GOLANG_VERSION=1.22.0
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
curl git ccache build-essential pkg-config unzip \
&& rm -rf /var/lib/apt/lists/*
# Install CMake from official binaries
RUN curl -fsSL https://github.com/Kitware/CMake/releases/download/v${CMAKEVERSION}/cmake-${CMAKEVERSION}-linux-x86_64.tar.gz \
| tar xz -C /usr/local --strip-components 1
# Install Ninja
RUN curl -fsSL -o /tmp/ninja.zip \
https://github.com/ninja-build/ninja/releases/download/v${NINJAVERSION}/ninja-linux.zip \
&& unzip /tmp/ninja.zip -d /usr/local/bin && rm /tmp/ninja.zip
# Install Go
RUN curl -fsSL https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \
| tar xz -C /usr/local
ENV PATH=/usr/local/go/bin:$PATH
ARG OLLAMA_VERSION=v0.23.2
RUN git clone --depth 1 --branch ${OLLAMA_VERSION} https://github.com/ollama/ollama.git /build
WORKDIR /build
# ROCm paths
ENV HIP_PATH=/opt/rocm
ENV ROCM_PATH=/opt/rocm
ENV CMAKE_GENERATOR=Ninja
ENV LDFLAGS=-s
# Step 1: Build CPU backends with GCC (no ROCm preset)
# Pre-set CMAKE_HIP_COMPILER="" to prevent check_language(HIP) from
# finding a HIP compiler (it searches /opt/rocm even without PATH).
# Remove /opt/rocm from PATH to prevent find_program from finding hipcc.
RUN mkdir -p build-cpu && \
PATH=/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
cmake -B build-cpu -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_HIP_COMPILER="" \
-DCMAKE_INSTALL_PREFIX=/build/dist && \
cmake --build build-cpu --target ggml-cpu -- -l $(nproc) && \
cmake --install build-cpu --component CPU --strip && \
echo "=== CPU install ===" && \
(find /build/dist/lib/ollama -type f -o -type l 2>&1 | head -20 || echo "empty")
# Step 2: Build HIP backend with ROCm preset + gfx906 target only
# The ROCm 6 preset enables HIP language detection (enable_language(HIP))
# which ensures GPU kernels are properly compiled for gfx906.
# OLLAMA_RUNNER_DIR=rocm from the preset, so HIP goes to lib/ollama/rocm/
# Need CMAKE_PREFIX_PATH so find_package(hip) finds hip-config.cmake
# at /opt/rocm/lib/cmake/hip/hip-config.cmake.
RUN mkdir -p build-hip && \
cmake -B build-hip \
--preset 'ROCm 6' \
-DAMDGPU_TARGETS="gfx906:xnack-" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="/opt/rocm" && \
cmake --build build-hip --target ggml-hip -- -l $(nproc) && \
cmake --install build-hip --component HIP --strip && \
echo "=== HIP install ===" && \
find /build/dist/lib/ollama -type f -o -type l | head -20
# Step 3: Build Go binary (GCC for CGo linking)
ENV CGO_ENABLED=1
RUN go build -trimpath -ldflags="-X=github.com/ollama/ollama/version.Version=${OLLAMA_VERSION}" -o /build/dist/ollama .
# ---------- Runtime image ----------
FROM ubuntu:24.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
ca-certificates curl libstdc++6 libgomp1 libvulkan1 libopenblas0 \
&& rm -rf /var/lib/apt/lists/*
# Copy ROCm 6.1 runtime libraries
# These are needed at runtime by ggml-hip via LD_LIBRARY_PATH
COPY --from=builder /opt/rocm/lib/ /opt/rocm/lib/
COPY --from=builder /opt/rocm/share/ /opt/rocm/share/
# Copy ollama binary + all backends (CPU + HIP)
# CPU install: /build/dist/lib/ollama/libggml-*.so
# HIP install: /build/dist/lib/ollama/rocm/libggml-hip.so
COPY --from=builder /build/dist/ollama /usr/bin/ollama
COPY --from=builder /build/dist/lib/ollama/ /usr/lib/ollama/
RUN ldconfig
ENV LD_LIBRARY_PATH=/opt/rocm/lib:/usr/lib/ollama/rocm:/usr/lib/ollama
ENV HSA_OVERRIDE_GFX_VERSION=9.0.6
ENV HCC_AMDGPU_TARGET=gfx906
ENV HSA_ENABLE_SDMA=0
EXPOSE 11434
ENTRYPOINT ["/bin/ollama"]
CMD ["serve"]

281
flake.lock generated
View File

@@ -23,6 +23,38 @@
"type": "github"
}
},
"argononed": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"argononed_2": {
"flake": false,
"locked": {
"lastModified": 1729566243,
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
"owner": "nvmd",
"repo": "argononed",
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
"type": "github"
},
"original": {
"owner": "nvmd",
"repo": "argononed",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
@@ -37,6 +69,54 @@
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
}
},
"flake-compat_2": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_3": {
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -144,6 +224,150 @@
"type": "github"
}
},
"nixmate": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1771085058,
"narHash": "sha256-w9qE5zZ8mEHPbR84OuJoFWFvmMuKpvq/ANwyDUEvbPo=",
"owner": "daskladas",
"repo": "nixmate",
"rev": "9030841d4c3ec72359643d7af68bfdac428f1636",
"type": "github"
},
"original": {
"owner": "daskladas",
"repo": "nixmate",
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-stable": [
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-images_2": {
"inputs": {
"nixos-stable": [
"nixos-uconsole",
"nixos-raspberrypi",
"nixpkgs"
],
"nixos-unstable": [
"nixos-uconsole",
"nixos-raspberrypi",
"nixpkgs"
]
},
"locked": {
"lastModified": 1747747741,
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
"owner": "nvmd",
"repo": "nixos-images",
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "sdimage-installer",
"repo": "nixos-images",
"type": "github"
}
},
"nixos-raspberrypi": {
"inputs": {
"argononed": "argononed",
"flake-compat": "flake-compat_2",
"nixos-images": "nixos-images",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1779023229,
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "v1.20260517.0",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-raspberrypi_2": {
"inputs": {
"argononed": "argononed_2",
"flake-compat": "flake-compat_3",
"nixos-images": "nixos-images_2",
"nixpkgs": [
"nixos-uconsole",
"nixpkgs"
]
},
"locked": {
"lastModified": 1779023229,
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
"owner": "nvmd",
"repo": "nixos-raspberrypi",
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
"type": "github"
},
"original": {
"owner": "nvmd",
"ref": "v1.20260517.0",
"repo": "nixos-raspberrypi",
"type": "github"
}
},
"nixos-uconsole": {
"inputs": {
"nixos-raspberrypi": "nixos-raspberrypi_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1780333979,
"narHash": "sha256-yjp/gWRqWCxalYLe2+27gbxhttL+46Ozm7qvccpFbv4=",
"owner": "nixos-uconsole",
"repo": "nixos-uconsole",
"rev": "1e6a8dc51c7b26e15850688a0b01121c3fea4b30",
"type": "github"
},
"original": {
"owner": "nixos-uconsole",
"repo": "nixos-uconsole",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705033721,
@@ -177,6 +401,22 @@
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770562336,
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1774386573,
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
@@ -212,7 +452,31 @@
"inputs": {
"agenix": "agenix",
"lix": "lix",
"nixpkgs": "nixpkgs_2"
"nixmate": "nixmate",
"nixos-raspberrypi": "nixos-raspberrypi",
"nixos-uconsole": "nixos-uconsole",
"nixpkgs": "nixpkgs_3"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixmate",
"nixpkgs"
]
},
"locked": {
"lastModified": 1770606655,
"narHash": "sha256-rpJf+kxvLWv32ivcgu8d+JeJooog3boJCT8J3joJvvM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "11a396520bf911e4ed01e78e11633d3fc63b350e",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
@@ -229,6 +493,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -2,6 +2,7 @@
description = "Gortium infra flake";
inputs = {
nixmate.url = "github:daskladas/nixmate";
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
agenix = {
url = "github:ryantm/agenix";
@@ -12,10 +13,18 @@
url = "git+https://git.lix.systems/lix-project/lix?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-uconsole = {
url = "github:nixos-uconsole/nixos-uconsole";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-raspberrypi = {
url = "github:nvmd/nixos-raspberrypi/v1.20260517.0";
inputs.nixpkgs.follows = "nixpkgs";
};
self.submodules = true;
};
outputs = { self, nixpkgs, agenix, lix, ... }@inputs:
outputs = { self, nixmate, nixpkgs, agenix, lix, nixos-uconsole, nixos-raspberrypi, ... }@inputs:
let
system = "x86_64-linux";
keys = import ./lib/keys.nix;
@@ -26,7 +35,11 @@
"/etc/ssh/ssh_host_ed25519_key"
"/root/.age/bootstrap.key" ];
};
overlays = [ agenix.overlays.default ];
overlays = [
agenix.overlays.default
(import ./overlays/reticulum.nix)
(import ./overlays/nixmate.nix inputs)
];
pkgs = import nixpkgs {
inherit system overlays;
config.allowUnfree = true;
@@ -61,7 +74,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 +93,22 @@
./hosts/cyt-pi/hardware-configuration.nix
];
};
uConsole = nixos-raspberrypi.lib.nixosSystem {
specialArgs = { inherit self keys paths inputs nixos-raspberrypi; };
modules = [
{
nixpkgs.overlays = overlays;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-linux";
nix.package = lix.packages."aarch64-linux".default;
}
nixos-raspberrypi.nixosModules.raspberry-pi-5.base
nixos-uconsole.nixosModules.uconsole-cm5
./hosts/uConsole/configuration.nix
./hosts/uConsole/hardware-configuration.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
@@ -140,12 +144,14 @@
# You can use https://Search.nixos.org/ to find more packages (and options).
environment.systemPackages = with pkgs; [
neovim
tmux
docker-compose
wget
age
agenix
git
nh
nixmate
lm_sensors
rocmPackages.rocminfo
rocmPackages.rocm-smi
@@ -207,6 +213,7 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -251,12 +258,6 @@
# };
};
services.opencode = {
enable = true;
port = 4099;
ollamaUrl = "http://127.0.0.1:11434/v1";
};
# Private host ssh key managed by agenix
age = {
identityPaths = paths.identities;
@@ -306,16 +307,6 @@
};
};
# OpenClaw Node service (host-side execution for Docker gateway)
services.openclaw-node = {
enable = true;
user = "ai-worker";
gatewayHost = "127.0.0.1";
gatewayPort = 18789;
gatewayTokenFile = "/run/secrets/openclaw_gateway_token";
displayName = "lazyworkhorse-host";
};
# Public host ssh key (kept in sync with the private one)
environment.etc."ssh/ssh_host_ed25519_key.pub".text =
"${keys.hosts.lazyworkhorse.main}";
@@ -328,21 +319,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 +465,7 @@
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 10;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;

View File

@@ -0,0 +1,171 @@
{ config, lib, pkgs, paths, self, keys, ... }:
{
# Basic Host Info
networking.hostName = "uConsole";
time.timeZone = "America/Montreal";
i18n.defaultLocale = "en_CA.UTF-8";
# System State
system.stateVersion = "25.05";
# 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;
# Networking
networking.networkmanager.enable = true;
services.openssh = {
enable = true;
settings.PermitRootLogin = lib.mkForce "prohibit-password";
settings.PasswordAuthentication = lib.mkForce false;
};
# 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
# ===== 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 # Python CLI for Meshtastic devices
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 and radio
# ============================================================
boot.kernelModules = [
"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
];
boot.blacklistedKernelModules = [ ];
# ============================================================
# 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.
}

View File

@@ -0,0 +1,26 @@
{ 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 = [ ];
# uConsole CM5 uses NVMe or eMMC for boot storage
# The uconsole-cm5 module sets up /boot/firmware and default /
# Override device label here if using different storage
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/NIXOS_UCM5";
fsType = "ext4";
options = [ "noatime" ];
};
swapDevices = [ ];
nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
hardware.enableRedistributableFirmware = true;
powerManagement.cpuFreqGovernor = lib.mkDefault "ondemand";
}

View File

@@ -1,363 +0,0 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.stagingVm;
# ── pr-test-vm helper script ──────────────────────────────────────────
pr-test-vm = pkgs.writeShellScriptBin "pr-test-vm" ''
set -euo pipefail
LIBVIRT_URI="qemu:///system"
VM_DIR="${cfg.dataPath}"
NETWORK="default"
SCRIPT_NAME="$(basename "$0")"
usage() {
cat <<EOF
Usage: $SCRIPT_NAME <command> [options]
Commands:
build <nixos-config> [--name <name>] Build VM image from a NixOS config
start <vm-name> Start a VM
stop <vm-name> Gracefully shut down a VM
destroy <vm-name> Force-power-off and undefine a VM
ssh [user@]<vm-name> SSH into a running VM
console <vm-name> Connect to VM serial console
list List all staging VMs
status <vm-name> Show VM status
Examples:
$SCRIPT_NAME build ./vm-config.nix --name my-test
$SCRIPT_NAME start my-test
$SCRIPT_NAME ssh root@my-test
EOF
exit 1
}
# Find the VM's IP address from the DHCP lease
vm_ip() {
local name="$1"
local mac
mac=$(${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" domiflist "$name" 2>/dev/null \
| ${pkgs.gawk}/bin/awk 'NR>2 && $1 ~ /^vnet/ {print $NF; exit}')
[ -z "$mac" ] && { echo "error: cannot find MAC for VM '$name'"; exit 1; }
local ip
ip=$(${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" net-dhcp-leases "$NETWORK" 2>/dev/null \
| ${pkgs.gawk}/bin/awk -v mac="$mac" '$0 ~ mac {gsub(/-.*/, "", $3); print $3; exit}')
[ -z "$ip" ] && { echo "error: no DHCP lease found for VM '$name' (MAC: $mac)"; exit 1; }
echo "$ip"
}
case "''${1:-help}" in
build)
shift
CONFIG="''${1:?Missing NixOS config path}"
VM_NAME="''${2:-}"
[ -f "$CONFIG" ] || { echo "error: config file not found: $CONFIG"; exit 1; }
# Extract name from --name flag or config basename
if [ "''${2:-}" = "--name" ] && [ -n "''${3:-}" ]; then
VM_NAME="$3"
elif [ -z "$VM_NAME" ] || [ "''${VM_NAME#--}" != "$VM_NAME" ]; then
VM_NAME="$(basename "$CONFIG" .nix)"
fi
BUILD_DIR="$VM_DIR/$VM_NAME"
echo "==> Building VM '$VM_NAME' from config: $CONFIG"
mkdir -p "$BUILD_DIR"
# Build the NixOS VM derivation
nix build --no-link -f "$CONFIG" vm 2>&1 || {
echo "Trying flake build..."
nix build "''${CONFIG%/.nix}#nixosConfigurations.$VM_NAME.config.system.build.vm" --no-link 2>&1 || {
echo "error: failed to build VM (tried both import and flake)"
exit 1
}
}
echo "==> Build complete. Run 'pr-test-vm start $VM_NAME' to launch."
;;
start)
VM_NAME="''${1:?Missing VM name}"
IMAGE="$VM_DIR/$VM_NAME/disk-image.qcow2"
[ -f "$IMAGE" ] || { echo "error: no disk image found at $IMAGE. Build first."; exit 1; }
# Check if already running
STATE=$(${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" domstate "$VM_NAME" 2>/dev/null || echo "undefined")
if [ "$STATE" = "running" ]; then
echo "VM '$VM_NAME' is already running."
exit 0
fi
echo "==> Starting VM '$VM_NAME'..."
# Undefine if defined but not running
if [ "$STATE" != "undefined" ]; then
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" 2>/dev/null || true
fi
# Define and start with virt-install
${pkgs.virt-manager}/bin/virt-install \
--connect "$LIBVIRT_URI" \
--name "$VM_NAME" \
--memory "${toString cfg.memory}" \
--vcpus "${toString cfg.vcpus}" \
--disk "$IMAGE",bus=virtio \
--import \
--network network="$NETWORK",model=virtio \
--graphics none \
--console pty,target_type=virtio \
--serial pty \
--memballoon virtio \
--rng /dev/urandom \
--noautoconsole \
--os-variant detect=on,name=generic
echo "==> VM '$VM_NAME' started. Get IP with: pr-test-vm status $VM_NAME"
;;
stop)
VM_NAME="''${1:?Missing VM name}"
echo "==> Stoping VM '$VM_NAME'..."
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" shutdown "$VM_NAME" 2>/dev/null && {
echo "Waiting for VM to shut down..."
for i in $(seq 1 30); do
STATE=$(${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" domstate "$VM_NAME" 2>/dev/null || echo "undefined")
[ "$STATE" != "running" ] && { echo "VM stopped."; exit 0; }
sleep 2
done
echo "warning: VM did not shut down gracefully, use 'destroy' for force"
} || {
echo "VM '$VM_NAME' not running or does not exist."
}
;;
destroy)
VM_NAME="''${1:?Missing VM name}"
echo "==> Destroying VM '$VM_NAME'..."
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" destroy "$VM_NAME" 2>/dev/null || true
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" 2>/dev/null || true
echo "==> VM '$VM_NAME' destroyed and undefined."
;;
ssh)
TARGET="''${1:?Usage: $SCRIPT_NAME ssh [user@]<vm-name>}"
# Split user@hostname if present
if echo "$TARGET" | ${pkgs.gnugrep}/bin/grep -q '@'; then
USER="''${TARGET%@*}"
VM_NAME="''${TARGET#*@}"
else
VM_NAME="$TARGET"
USER=""
fi
IP=$(vm_ip "$VM_NAME") || exit 1
if [ -n "$USER" ]; then
exec ${pkgs.openssh}/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "''${USER}@''${IP}"
else
exec ${pkgs.openssh}/bin/ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$IP"
fi
;;
console)
VM_NAME="''${1:?Missing VM name}"
exec ${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" console "$VM_NAME"
;;
list)
echo "Staging VMs:"
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" list --all
echo ""
echo "Active networks:"
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" net-list
echo ""
echo "Storage pools:"
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" pool-list
;;
status)
VM_NAME="''${1:?Missing VM name}"
echo "VM: $VM_NAME"
STATE=$(${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" domstate "$VM_NAME" 2>/dev/null || echo "not found")
echo "State: $STATE"
if [ "$STATE" = "running" ]; then
IP=$(vm_ip "$VM_NAME" 2>/dev/null || echo "N/A")
echo "IP: $IP"
${pkgs.libvirt}/bin/virsh -c "$LIBVIRT_URI" dommemstat "$VM_NAME" 2>/dev/null | head -3 || true
fi
;;
help|--help|-h)
usage
;;
*)
usage
;;
esac
'';
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 {
# ── libvirtd with QEMU/KVM ──────────────────────────────────────────
virtualisation.libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
ovmf = {
enable = true;
packages = [ pkgs.OVMF ];
};
};
};
# ── System packages ─────────────────────────────────────────────────
environment.systemPackages = with pkgs; [
libvirt # virsh, virt-admin
qemu_kvm # QEMU/KVM
swtpm # Software TPM
OVMF # UEFI firmware for VMs
virt-manager # GUI + virt-install
virt-viewer # SPICE/VNC viewer
libguestfs # virt-customize, guestfish
cdrtools # genisoimage for cloud-init ISOs
jq # JSON parsing
gawk # awk for DHCP lease parsing
gnugrep # grep
];
# ── User permissions ────────────────────────────────────────────────
users.users.gortium.extraGroups = [ "libvirtd" ];
# ── Directories ─────────────────────────────────────────────────────
systemd.tmpfiles.rules = [
"d ${cfg.storagePath} 0755 root root -"
"d ${cfg.dataPath} 0755 root root -"
];
# ── Default NAT network (192.168.122.0/24) ──────────────────────────
# Define the default libvirt NAT network using virsh postStart hook
systemd.services.libvirtd = {
postStart = ''
set -e
# Define the NAT network if it doesn't exist
${pkgs.libvirt}/bin/virsh -c qemu:///system net-info default 2>/dev/null && {
echo "Network 'default' already exists"
} || {
echo "Defining default NAT network (192.168.122.0/24)..."
${pkgs.libvirt}/bin/virsh -c qemu:///system net-define /etc/libvirt/qemu/networks/default.xml
}
${pkgs.libvirt}/bin/virsh -c qemu:///system net-autostart default 2>/dev/null || true
# Start the network if not active
STATE=$(${pkgs.libvirt}/bin/virsh -c qemu:///system net-state default 2>/dev/null || echo "inactive")
if [ "$STATE" != "active" ]; then
${pkgs.libvirt}/bin/virsh -c qemu:///system net-start default 2>/dev/null || true
fi
echo "Default network ready."
'';
};
# Define the default network as an XML config file
environment.etc."libvirt/qemu/networks/default.xml" = {
text = ''
<network>
<name>default</name>
<forward mode='nat'/>
<bridge name='virbr0' stp='on' delay='0'/>
<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>
'';
mode = "0644";
};
# ── Storage pool ────────────────────────────────────────────────────
systemd.services.libvirtd.postStart = mkAfter ''
set -e
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-info default 2>/dev/null && {
echo "Storage pool 'default' already exists"
} || {
echo "Defining storage pool at ${cfg.storagePath}..."
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-define-as \
--name default --type dir --target "${cfg.storagePath}"
}
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-autostart default 2>/dev/null || true
STATE=$(${pkgs.libvirt}/bin/virsh -c qemu:///system pool-state default 2>/dev/null || echo "inactive")
if [ "$STATE" != "running" ]; then
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-build default 2>/dev/null || true
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-start default 2>/dev/null || true
fi
echo "Storage pool ready."
'';
# ── Firewall rules for libvirt guests ───────────────────────────────
networking.firewall = {
trustedInterfaces = [ "virbr0" ];
extraCommands = mkAfter ''
# Allow DHCP (port 67/68) and DNS (port 53) to libvirt guests
iptables -I INPUT -i virbr0 -p udp --dport 67:68 -j ACCEPT 2>/dev/null || true
iptables -I INPUT -i virbr0 -p tcp --dport 53 -j ACCEPT 2>/dev/null || true
iptables -I INPUT -i virbr0 -p udp --dport 53 -j ACCEPT 2>/dev/null || true
# Allow forwarding between the bridge and the outside world
iptables -I FORWARD -i virbr0 -o virbr0 -j ACCEPT 2>/dev/null || true
iptables -I FORWARD -o virbr0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
iptables -I FORWARD -i virbr0 -j ACCEPT 2>/dev/null || true
# NAT for guest outbound traffic
iptables -t nat -I POSTROUTING -s 192.168.122.0/24 -j MASQUERADE 2>/dev/null || true
'';
};
# ── pr-test-vm helper script ────────────────────────────────────────
environment.systemPackages = [ pr-test-vm ];
};
}

6
overlays/nixmate.nix Normal file
View File

@@ -0,0 +1,6 @@
# ./overlays/nixmate.nix
inputs: # We pass inputs here so the overlay can see the nixmate flake
final: prev: {
nixmate = inputs.nixmate.packages.${prev.system}.default;
}

81
overlays/reticulum.nix Normal file
View File

@@ -0,0 +1,81 @@
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";
};
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;
};
};
}

19
secrets/ai.age Normal file
View File

@@ -0,0 +1,19 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSA2QUFR
RmlnY2pYUFBHZGN3dzNpNVc0UW14V2NaVWZHWDJmcU1iWStsSHpJCnRYQUtEalNt
SzFYWUF6VWRyYWF5VCtsSGduRU96MHA0WlI0b0lpUGFram8KLT4gM0stZ3JlYXNl
IC5DPExhJQpCMlZRR0E1NGdsYlhNRXZUTVJqTTNHZUZQVUJoYkhheWd3TktvVEFp
WnFqbS9xWUxJSmI0U0hqQkdzQU1EVy9BCkhWc3A0Mk45YVBxcnJFWU5qa01LRTVw
WlNoWm40cjgyb1RXS3hGdlpRSEs5V0x0cHNvcVBOek0KLS0tIFhiOFExNnRSbEgz
M3FmS1owQUdHSXpiWVhLQ0R6emtuK2hZSkkrSTFtTTAKi2hXxTslGLlneDJlvHUm
OFsvgsHmiADRaiwDC/iCfWyEFzHyupXKX79B6rjt4Up2yUWHLHy4pyCvJiezAh1J
uxvtVmUYrqYOUoUHCEMLjjVydh5tt+22yXezV0ecU2ePiAgb0gv9EwH8t+70An6Z
fgvtZZqJ1hK6dTyTf4oROUjPD8RgfjDnRTTvTM1COMvwgpK+cboYAvJ4BhUsn70V
B3Gveg7++1U2ECF8YHKZGfSuWn0ajT5RzdgRXTveGAINiW4G98oyXnOVhRHmXo9i
8T/41afy4xhUbNCRsaAApFjS6y8xrpGtrOcpw0u3y3T/bjYHq8VDi4ttYWIF8hJD
HPo44+SDj0g9lr/UPQDWXaiZjdD6xdioUqnHnaAdyOPPf5D6C5I7WIQPPIjnJ1yc
PmQkIV4rtb+e7cWrGbnd9stCBZ2F+HcORfjgnJzKI5asy9rmdWPIIHkcZoPFZ/K8
2ZWXUVgAZvwzcFxSjh5uXKvkOgYixdVLRfa88+KM++vwz0y3O5naq3d1SaLxV4dk
mRifLNhF9y2MKjX0bZ86Wm74NlhGGQSnUuUHpPLSA6McVOfleN/yJ9gMsosxUc9U
xIsKHYn8gN0lPVtyTSBTomK6kxoKJXxia50x/XAhJwI9X3ydLg==
-----END AGE ENCRYPTED FILE-----

10
secrets/cloudstorage.age Normal file
View File

@@ -0,0 +1,10 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBaT3h1
K28zWlE3N3BMdHRFaldkZ0F2YVZjT3kxK2dpV1RxZUVOSElsbkdJCnF2M1ZkUWky
cGtPaXdzMmU1cllsWWtQK3R4Z0xhUExxSW9xUVh3eUo5RjQKLT4geHEtZ3JlYXNl
IEQ5JWBWM0QKS1N5WlpnbEtLTFNxQVFvbkVEa1BOTDhQRDV0SDhsdWFEdE5xMDRa
b3loMDZEeGZTZ1NzaFM5UlZYT0NjMHVvWAordjh4cFhnb2pOc3JPZlFLbFU0eC9O
WWFFVlUKLS0tIEJnMm9LM09KQ3VXNXpNNU1kS3V5VDNOTGozdDdGdWQxcFc3Mk5j
R0p0WVkKIX6p7MGDZOxS5qVgYs5K5sKC31IPIEMaWAqA5Gygegj5rVnKtN/jn7+9
L5CBbzaSXzNwRce0WPVi0mQp56lL12mRlXXamOx/E9Q+r2rznFYdQA==
-----END AGE ENCRYPTED FILE-----

View File

@@ -1,36 +1,43 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBWNEpt
cGFNeVBBaDRqb3pLSEZGQW0wb3VmVnBoZCswUFkzbnBLUnJ0QTNrClRqVkk4RUVO
d29KYjd5YUcwankvaTFmVHUxQVpDT2ZYWHRaY3JXTUtQMU0KLT4gKXBtQ3UsXi1n
cmVhc2UgNnwxYCBXVyA/KCQmIHt9NAo3OTZVUHR2UXkvaEFwY0ZBdEJsaFpsbHJ0
cklKcDVHcEdWMEdPSkpnN0FiRU43RW5hUWFMdjR3WFRRSFBLSGlmClM3cTNJWlNM
TExkdHdXUHJISkNIaE1TTUxUc1NUWkV1a09HeFU3bVZwQXMKLS0tIGhOcXFTUElS
azJJNnEreUhMWTJBaVZGSTJPRUFqQkVYS01KRENUVVpZSDgK8+8onFejroBo7MeO
dW+so4lOsq4zJKn3f0cxmCFg1f0X8zt6h4Uc3A5Cvr1uU+6yw1FWmJ7xa3jJz3lO
EEaKQJXYC+xIIKGcA7qILa0SFp4a/4OuYjcg27HrlPhg7u5wDhQrd0LdVEe1Xngp
ZivX7P7HwIna3X8C+TL+K2v/AG2N/z86cdKfRvxyMKNbHhYw+CfHEnWgh8tJ++4h
G9evNniuNqte6cQaRe7jODfPNW4FuY/Sb7barlJ/M9iAQdYAdyLAzU1LABeHeUfD
wtHjxy9DUZ55Vg8bB8M2JJU9MkoRT4ewiVd9LeC1GWeVmKsm93wsmrov714i7U2j
wHtDkjqEF2MmzuQc18sjNaAHiwz8j6o5xU2L/Q4+Q707yISWG7RGZYh389Cr1rnw
siUq/Vunqw2wk13+J/4vu9nqt5mMktBaCtp+QiWIurjwB5LUAyChrSm+dg5lb0Mt
UhSc0lq1+E3vxAXM2Hmk+vP86VD+6WJvAU82VFApF1s6zG2FU1/AcOVVf54nan/q
f+rgSFfASHQCYSblUJHyEtwLNsWEmTGmOEn1buUKD/H0zatPQnc0rYpjlx2V0Sjd
6yB5+wPrZ0AkN1pjcsPKOv8Kaog2DzqIjib+SaSTaRxWHQEb9uzvaReAcYI5HOpE
gkC040HN33BItATbo4+hz70Im8Ni/VXD+g6yzM6Hj1hJL+PinTKeg5keQRFIZjMx
grzievB2wVBBgLgN3qMdTFmpplaL7iL702JjXZUTTK9Izp+9wiCsV1fTa53FWDht
ylFL5SWElqXjK+QBXxAe+Jk6VQov5HI21YDXL67S554ABeRok23wxrQ31TCI4xq9
PQV7VtNRjyVud7S29m3OwpWOsgTZhn+JclHj2v4bNJzJkJnZRTmcvGPktzRI5+R4
e5vxVhGnJDzI71txaHl8+xS1lu9VzCQUrxX6TXyTRV4KjIOz0g06JOBgmBRBvJca
7MZbC65xpisl/gyLRbgkVga3t94dPV+dpZsn8eq6427IyRbKslJefatggR9//c6I
5N5fl0fR3gJQMB+HRbipBH2YsdbdWJyb4Nn6STZxIfrqoG/xC6C1raF0xK7hUx6i
4DUDSPohM8fOIswQPfE+FH3eygfzu/Ln5+ghsgHTEhgFvmgMvyxaAt6kHIzIUhMX
M3dASr4VPDpIXuXsRWwYLEifhzxsuvwVxfwtsnCaR6XKijsYECWGDdYOWHdleeqx
wDPhxEesfFVhKxhrKY9Ir8k9/FFBKQU/3GjW4+SMAg5Al1YEzxshP9vKuVcsei7W
JDwAwotNXaCm6NBckiyZJE53ou6+gckPY7V9cOfnuH74Z9ywkFzB3HW3ZlonaGyM
oGmLGcccavFtyhg5s/As4i6X8ARIpDiwe59Pn3GNXMctySqIrrr2ogUoXgrfFCie
6GOTdeMW7GeOSdJUxCofghlspS/nq01Og77VI/beWYrIwLubSka6Zaltww9zgObk
/FGEMgFkEpq7iyCvYSPA8F46pJKvnMP3S84AWCPmcTcHeg4lwGPvs6btexXBGdoz
nkCyq7wdH5Nngm7jUbl88LtaLZPAQkuqXphBVTnrF9Ofbnb4iRZ2Op4xpx9rGyvx
mO6UEhL6V1i2YZFNkNMg/W8aoMiUgBdqbkxaxblT9L0aNdlFU9+LbWYolURVEadd
Qjv0Z1gMA+tsuBbVszwsMfneZ5+B9Q==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBVYWFV
U1dWcVVoZVJ2a1Bmd00zbnRvYnNTb0RvUlhJSVZheVlZcGlKRFgwCmVNTVNzVk8z
ZSszVUN2enhIV09UMkpGNG9vZlhTTVZrd1U1QnB6OVpnOU0KLT4gSCgtZ3JlYXNl
IFtVXTlQIGl6YXRHCjdqNS9oa3p5dGZseWV5NDNadjN2VDVheCtkRm1jRWpuY0Y3
Sk1aWHhyQQotLS0gaFo5TjM1bFdKOEJVenZHK3RXOHExeXV0dFIrTUNMc0pnTXBM
YWIrNHdHNAqkUzENsQawFRPRh6K13pJN9pkDGpvpx07kTPkUWbocUQoJMxeXdAlh
Syw6Ca2X2Xb2QGnO6EIoMsfs6Tg78cEiDH4DEcdcNdHoSZlaNGLrUHnvE/Ygc/G0
vz0D3uxTTvRBFYPNSONt+uN9j2p3JEZK5Hllt1GRmo6c8xnktDQpkwYLgegPYYxW
5Ge/ywwPlKp1WzqVVBQv2FH1/qm0QcBUhtjrkTTPFOjk3vlrXqbSM7n4cMZJYWRi
kMK51q2H49anwT8gkaeJ7hwlJpmzNIocDpD5sxxXj1NGc+uogEfaZrP7IRsACSTa
ggub/LYjPyajj4CLjt+6O3KNepNsOTEiGH11TZ6cyjipDerIcZx1rO/6cnOPpN40
TZQc1IW+oGrbJk7WNNX4gjX2t0+ElVeDVQ2wEe862Yh4OxyMVUkCyh9DGFu5RxL4
veKJfvs6VSjS4GtaOOXAxYra5GXGaNAx9w53MRSWXd7T9eh4VnYr0pvdRPMYyQAe
H/aNl9lqtQt1r4DtfF/RMOuqIiKFlnnhKlVfp5uCAn5Bgn4nCBMvQrAlghXWzI+N
rou2kBuzI3yKDO+8VJlacEVXM9vwv/O15XYpY/Mn1TYzbsOQi7FgnFatAWr6Uinh
gmay4WDxikIlKdhV1CqOJek5AAC1ptVCDgDcOpERDdZ+whBHUDTJ4NjMEPPcmlyw
9sejEf7mjuqKY5VGAVy2CRqV0wIMJ3nIo8Um2NNQE2BKCVyucmxhPVA6NieRgG8F
NvqIE3Iji1xFiUC+twjOkTcVAl7hKk2yLjZxF6KqoP7rWr787qr3qNA3cXhxgSUN
ZX9kzepy7pTyHADqK4j1Hum8o6/OjKaGQy/HIiywMjfk7zl3+Cs0PhjKG9HBReIQ
MqbJqa2fyAqOuHk/44Lpja1CI7LGWxOrctTixG3+Gy3JmTrZh8pDqzzNmPrByrHO
+nN3HzUZ/4sw5ONUz/GQ89WcSjez1TNr+lwRsYMf8tUCRHgdxUT9HRXUeOd8eD4d
CDyoAYpab01ZFkHzRhBOWJlqjqhDNIDEzMjImU4ieBsgCh9ZyIZF2HetLov6xESo
FYCDb7UqIXsw7n04wSTbwcYV2iPQ0EowPrADcSvqq+9YVr8Tsw6pmTaxC18Wi+zY
7R9ALANy9qgxLnBAAXIbp2iyJayFQDNONkOlp4NV2RiVN/UrHcZNsZsSfM0DFzqh
spuqt3czmAdvUzWpnghjMRUC49kkW2XNJxS9dREHmEPVZnzQo457CCCFsAqmrRB6
c/wvyKD86Z89l/tkrLVz+sLQJMYfc5zED+EjyZyghHprkqZ6iDjFaEQUO8Fk2FwX
wCDqJhJGXs3MT7tJnfIrwy4AvJv9FgUnMYYpQiyv+ToDqgAWGVDfxGieQo6r59El
ZpTx+qt3jT+jI5Z6v4OVRODU2FFlee+YcF3ooF3yiwNdvuRhIgOVCNXkD/9QBOyq
MiOcCcGNxWx3COGIdshg/HVjGfxm+zZUYW0ktn3/YNkUHPIPxIWPNRdlQ9xbu3kD
vd1Ivx8wsUfo7soMffjdYKoAbAWS/3Ce/Sgpk15uo7CzGoO7Egzj+cb4GtvgojDD
BwG3CSzUJcXV2zbRwxfHRanko0RN/Qpskc/P1DiSMduxP8nZT+/r2HCykX+CRkM7
PtnG0moKD3i5u3X/jQAAPA1X9BPbbwgTE/nmHpgMxG1BD5GlLmeY1hkBRwEUlnSr
UsN8FaWGBKyPMtMhK8EcjzmH259NJYguDgpBgMiIpO3sXst4yK5RuxR22+S8AI6X
VucMZWa+BzyHc3Oej/T5UrCIZ/tDzktD0Xp76EZxPD7ADw2z0+7TQ5YSYFal3+X1
fcdKTphts2YRvtXLQuv+8SCIcf8VKMVjIkRCtE5ez1Isk+ZHEtprKsp9ZFsj9cFV
R6c2d1YqoW7IGWfcZL4snG09DaifPtwiwJBdYxRfGXgOq1fxtszzAxdlIoT3HRVo
pBHZp+7zcAXFyjgPDGOP5fcfGbMD41Vr9W9DVzioBIZDDQLRs4M6DZDgqgWv32tO
P7s2TivIEoAnirtic+tQZe5lXwndPUou0qYTO28diLSCTj29S+Vw9/ftah7qOqy6
iDdf2rhbMNSRoywZOwQer6aLHePkSq7yDRRnQburIaUyy4FWjSYumx3O/2++JrkW
uFhLRIOu4M3gkgyqb4xOscxl/aEGZADR8tLIeqn/Bnt/Bub8vTph1H/ZCYEvjZA5
afmShsiIKw==
-----END AGE ENCRYPTED FILE-----

View File

@@ -0,0 +1,19 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBOSk9J
RDF0bk4zNmdWRXhFTU1xUlVKeDJkTU51UDRiNjcvWlVKT3cxNzBvCk00ZEtMdlI2
L2xiS3BnT1g0aEd3VmgydCtSVERrTjNraHNmSHZicENLODAKLT4gI0crX0A0Zmst
Z3JlYXNlIE8wNCBfPz5uSy4KSGxzcnJlM1FRamI0VWlZbTJvRnRuanlwSjN0eisy
bnlHSDZZbHpIbGNZUngvS1Fqb2dzd1p6UjZKZGowOUhONgoKLS0tIE1NMUV3QlpV
dEFXMUVOTFBGSlpVSjFzRlpjVTk2dkx1VURUaFl3R014a2cKTnWIRQg4txvZZ4A5
XdwnXZqmYzOX7l2UrjmP/TJA++EURZzYtTS+q3FV9QTIj+7k0DOLf9nKq7XHlGM8
Ptkaf/s1Ly7ke+v9l9C2uf9oNI6b+m3d7QqZC5WWvRXWvl9ImaJ3xVi9AM5f5NWH
Likn6GZDir4ihAec8pAi3H2SCCjkhqF/TlLe99tp+7AAUiOjNuNlej1OUOmJNxEr
GnHePwZZqa6/F9YnYQ0tKKRsZH5whxUPYUwhHSuWa/NidBK7lqKqdaPBevfdqWLg
59gkaL9BeGG5FUXg1cPEWAvOQ5Ajae/kVH6aRi5LXJPZfA/DetWgm9mmxJ2qPpFD
/n/JCsQUOnpk66j38bWMW5uok51s61oG1OZWGpPYstoLtivKOlVpG7xgioGqIIuc
c2ZsKK/iCauwX3sGQtpX9DbILrtOnzxU2B/dclcXe2EDfSJH0u2RrefxcqPG87pI
BAosRdq7GUUISSCTccaswwG555a7/Z5gzXOwXeQYNlK1HRnkF9oBsKTUoAI2b+VO
Hdv1uw9u5JLcZt+5qSB6AxJuk8yDF0KXIqkaCiwl2fV3bBkehPru7oCJFTtVMFVZ
u0A0h+n8KJCvyRKe/Tl+lT+JHM6Mk7SkFrmsaepZPnAtfLht+bP6b0oHrUsVx/iI
r7o=
-----END AGE ENCRYPTED FILE-----

12
secrets/network.age Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBVMEhO
RWdybFg4VWJaMjRWYm1ac2pzZ3R5aFZEM1o4dUtFZ0M4QnVKRzIwCnNGRFRjb01X
QU5mQTRSb21MV3lMMERIMGI1RHFQVFFZNlVlMnk4aTdna1kKLT4gM24xLWdyZWFz
ZSA/bVhubQplcXNVN3kxOEpqY2hUbkV3MG5PNVJtRGhORVVLdFVCRnFFVjRneEFI
VWFyZTRNS280RXlnckJpdFR4V1NBSTUzCjlxZkZ3UllpZXUwczR0T3JGRzUzMEhV
U1hobC84eWtEWWZOYyt6M2c3OUZIUTlKTm41dVp5UEZVSmVwWHBmR00KCi0tLSBF
ZnJ5QzN2VXpsY3JQQ3lmbklxcldydm1MSlFlejJ2S3hVVUtka0hTM044CvqhtnFn
pZsgP6w5uKh9CtVQpcyST8hA9P24e1opfu1JN3m/8g9XiJrMT5qVYtUXkV+pr4s6
pVGsp9KScaS0T8TpLxvZoUAAdvVtdHh6GSbbszR6gZbStbWXmb6THYKHOghpK9HD
cDEoRmzcmFdwi3m26Xppmi2R6NmeKI4S6pbY95bfh2E6jn+7
-----END AGE ENCRYPTED FILE-----

View File

@@ -11,4 +11,8 @@ in
"lazyworkhorse_host_ssh_key.age".publicKeys = authorizedKeys;
"n8n_ssh_key.age".publicKeys = authorizedKeys;
"openclaw_gateway_token.age".publicKeys = authorizedKeys;
"ai.age".publicKeys = authorizedKeys;
"cloudstorege.age".publicKeys = authorizedKeys;
"homeautomation.age".age".publicKeys = authorizedKeys;
"network.age".age".publicKeys = authorizedKeys;
}

12
secrets/tak.age Normal file
View File

@@ -0,0 +1,12 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBsYXVu
Sk5CSjNsbHBRZkNXUGZVeGNva3FKMUdHV0RiQzJLNHVvSHZzd0dRClNnbzZvUFFR
Ty96OXZ4R2lCMHpnclpVaEZ1TEYvb0tTSVRwdWV1amdIL0UKLT4gYTtvOmAtZ3Jl
YXNlICk5WkE7USUgSyRLCjBmTWRTdEJJV2lNbVRlL3VidmZza1NTTWhIcG1vVFY5
OUUxcG4ycTMraUUKLS0tIEVlaUdlcUFPMkZSMUpJVEFzTksrVC9lZGlMNEJ1SFBv
SFNtc0wwQ05nQ0EKYlRGYGeKlfW0lTPdpCtcTfh2NWUuyZnDFe6VAWLYL3PW42od
a+bhlgxTi9KF5FLjVA3IIdQSsKiMya+PI0VfOLWkA24ZN+4uilsQkicxnnRR1z8o
hv9G8N3eYqBsLylg8rW2Xkn4RzT/S5TyJZPFG4HsUYWtHS+iomg4HbQrgIrpIde9
Th3V8WQB1HfKs9ilg8Q/DvDckFjevWYz2OXMYx4ZHWS53huQfE/oWPZfQid6Dd06
NXcvCHPVDUszhWvcoOsSpK6vaOnuY1B32/qExw==
-----END AGE ENCRYPTED FILE-----

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