Compare commits

..

19 Commits

Author SHA1 Message Date
697f40f1ed feat: add staging-vm module with libvirt/KVM infrastructure
Some checks failed
NixOS Build & Test / build (pull_request) Has been cancelled
2026-05-20 14:20:47 -04:00
e4b2886f54 ci: add NixOS build and test workflow
Creates .gitea/workflows/build-nixos.yml that triggers on push/PR to
master when Nix files, flake.lock, secrets, hosts, or modules change.

Includes:
- Setup step to enable Nix experimental features and install nh
- Build step for lazyworkhorse (x86_64-linux) via nh os build
- Build step for cyt-pi (aarch64-linux) via nh os build
- Placeholder integration test step

Runner label: nixos-builder (Docker image nixos/nix)
2026-05-20 14:19:13 -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
7 changed files with 602 additions and 49 deletions

View File

@@ -1,15 +1,19 @@
name: Build NixOS config
name: NixOS Build & Test
run-name: Build ${{ gitea.event_name == 'push' && gitea.ref_name || format('PR #{0}', gitea.event.pull_request.number) }}
on:
pull_request:
branches: [ master ]
push:
branches:
- master
paths:
- '**.nix'
- 'flake.lock'
- 'secrets/**'
- 'hosts/**'
- 'modules/**'
push:
branches: [ master ]
pull_request:
branches:
- master
paths:
- '**.nix'
- 'flake.lock'
@@ -22,12 +26,39 @@ jobs:
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
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build NixOS config (lazyworkhorse)
- name: Set up Nix environment
run: |
nix --version
nh os build .#lazyworkhorse 2>&1
echo "extra-experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
cat ~/.config/nix/nix.conf
- name: Install nh (nix helper)
run: |
nix --extra-experimental-features "nix-command flakes" \
profile add nixpkgs#nh
nh --version
- name: Build NixOS configuration (lazyworkhorse)
run: |
nh os build .#lazyworkhorse
env:
NIX_CONFIG: "extra-experimental-features = nix-command flakes"
- name: Build NixOS configuration (cyt-pi)
run: |
nh os build .#cyt-pi
env:
NIX_CONFIG: "extra-experimental-features = nix-command flakes"
- name: Integration tests (placeholder)
run: |
echo "TODO: Add integration tests here"
echo ""
echo "Suggested future checks:"
echo " - nix flake check (evaluate all NixOS configs)"
echo " - Validate agenix secrets are decryptable"
echo " - Check services are defined correctly"
echo " - Run VM test if nixos-test infrastructure exists"

106
assets/ollama/Dockerfile Normal file
View File

@@ -0,0 +1,106 @@
# 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"]

View File

@@ -59,6 +59,7 @@
./modules/nixos/filesystem/hoardingcow-mount.nix
./modules/nixos/services/docker_manager.nix
./modules/nixos/services/open_code_server.nix
./modules/nixos/services/staging-vm.nix
./modules/nixos/services/ollama_init_custom_models.nix
./modules/nixos/services/openclaw_node.nix
./modules/nixos/security/ai-worker-restricted.nix

View File

@@ -191,7 +191,6 @@
services.dockerStacks = {
versioncontrol = {
path = self + "/assets/compose/versioncontrol";
envFile = config.age.secrets.containers_env.path;
ports = [ 2222 ];
};
@@ -208,6 +207,7 @@
ai = {
path = self + "/assets/compose/ai";
envFile = config.age.secrets.containers_env.path;
ports = [ 22000 ]; # Syncthing TCP sync
};
cloudstorage = {
@@ -475,7 +475,7 @@
services.openssh.settings = {
PermitRootLogin = "no";
MaxAuthTries = 3;
MaxSessions = 10;
MaxSessions = 20;
LoginGraceTime = 30;
ClientAliveInterval = 300;
ClientAliveCountMax = 2;

View File

@@ -0,0 +1,415 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.staging-vm;
# Resolve the first IP in the subnet (the gateway address for the NAT network)
networkIp = head (splitString "/" cfg.network.subnet);
# ── pr-test-vm helper script ──────────────────────────────────────────
pr-test-vm = pkgs.writeShellScriptBin "pr-test-vm" ''
set -euo pipefail
LIBVIRT_URI="qemu:///system"
STORAGE_POOL="${cfg.storagePool}"
VM_DIR="${cfg.dataDir}"
NETWORK="${cfg.network.name}"
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
rebuild <vm-name> Redeploy the VM (stop + start)
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"
IMAGE="$BUILD_DIR/disk-image.qcow2"
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 || {
# Fallback: try as flake output
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
;;
rebuild)
VM_NAME="''${1:?Missing VM name}"
"$0" destroy "$VM_NAME"
"$0" start "$VM_NAME"
;;
help|--help|-h)
usage
;;
*)
usage
;;
esac
'';
in
{
options.services.staging-vm = {
enable = mkEnableOption "Staging VM infrastructure with libvirt/KVM";
network = {
name = mkOption {
type = types.str;
default = "staging";
description = "Name of the libvirt NAT network for staging VMs";
};
bridge = mkOption {
type = types.str;
default = "virbr1";
description = "Bridge interface name for the staging network";
};
subnet = mkOption {
type = types.str;
default = "192.168.122.0/24";
description = "NAT network subnet in CIDR notation";
};
dhcpStart = mkOption {
type = types.str;
default = "192.168.122.2";
description = "Start of the DHCP range";
};
dhcpEnd = mkOption {
type = types.str;
default = "192.168.122.254";
description = "End of the DHCP range";
};
};
storagePool = mkOption {
type = types.str;
default = "/var/lib/libvirt/images";
description = "Directory path for the libvirt storage pool";
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/staging-vm";
description = "Directory for staging VM test data (images, cloud-init configs)";
};
memory = mkOption {
type = types.int;
default = 2048;
description = "Default RAM in MiB for staging VMs";
};
vcpus = mkOption {
type = types.int;
default = 2;
description = "Default number of vCPUs for staging VMs";
};
};
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 ];
};
};
# Allow the staging bridge for guest networking
allowedBridges = [ cfg.network.bridge ];
};
# ── 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 management
virt-viewer # SPICE/VNC viewer
libguestfs # virt-install, 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.storagePool} 0755 root root -"
"d ${cfg.dataDir} 0755 root root -"
];
# ── Default NAT network definition ──────────────────────────────────
systemd.services.define-staging-network = {
description = "Define the staging libvirt NAT network";
after = [ "libvirtd.service" ];
requires = [ "libvirtd.service" ];
wantedBy = [ "multi-user.target" ];
before = [ "define-staging-pool.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = let
networkXml = pkgs.writeText "staging-network.xml" ''
<network>
<name>${cfg.network.name}</name>
<forward mode='nat'/>
<bridge name='${cfg.network.bridge}' stp='on' delay='0'/>
<ip address='${networkIp}' netmask='255.255.255.0'>
<dhcp>
<range start='${cfg.network.dhcpStart}' end='${cfg.network.dhcpEnd}'/>
</dhcp>
</ip>
</network>
'';
in ''
set -e
${pkgs.libvirt}/bin/virsh -c qemu:///system net-info "${cfg.network.name}" 2>/dev/null && {
echo "Network '${cfg.network.name}' already exists"
} || {
echo "Defining network '${cfg.network.name}'..."
${pkgs.libvirt}/bin/virsh -c qemu:///system net-define "${networkXml}"
}
${pkgs.libvirt}/bin/virsh -c qemu:///system net-autostart "${cfg.network.name}"
# Start the network if not already active
STATE=$(${pkgs.libvirt}/bin/virsh -c qemu:///system net-state "${cfg.network.name}" 2>/dev/null || echo "inactive")
if [ "$STATE" != "active" ]; then
${pkgs.libvirt}/bin/virsh -c qemu:///system net-start "${cfg.network.name}"
fi
echo "Network '${cfg.network.name}' is ready."
'';
};
# ── Storage pool definition ─────────────────────────────────────────
systemd.services.define-staging-pool = {
description = "Define the staging libvirt storage pool";
after = [ "libvirtd.service" "define-staging-network.service" ];
requires = [ "libvirtd.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -e
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-info staging 2>/dev/null && {
echo "Storage pool 'staging' already exists"
} || {
echo "Defining storage pool 'staging' at ${cfg.storagePool}..."
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-define-as \
--name staging --type dir --target "${cfg.storagePool}"
}
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-autostart staging
STATE=$(${pkgs.libvirt}/bin/virsh -c qemu:///system pool-state staging 2>/dev/null || echo "inactive")
if [ "$STATE" != "running" ]; then
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-build staging
${pkgs.libvirt}/bin/virsh -c qemu:///system pool-start staging
fi
echo "Storage pool 'staging' is ready."
'';
};
# ── Firewall rules for libvirt guests ───────────────────────────────
networking.firewall = {
# Trust the bridge interface to allow guest traffic
trustedInterfaces = [ cfg.network.bridge ];
extraCommands = mkAfter ''
# Allow forwarding between the staging bridge and the outside world
iptables -I FORWARD -i ${cfg.network.bridge} -j ACCEPT 2>/dev/null || true
iptables -I FORWARD -o ${cfg.network.bridge} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
# NAT for guest outbound traffic
iptables -t nat -I POSTROUTING -s ${cfg.network.subnet} -j MASQUERADE 2>/dev/null || true
'';
};
# ── pr-test-vm helper script ────────────────────────────────────────
environment.systemPackages = [ pr-test-vm ];
};
}

View File

@@ -1,36 +1,36 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IEdoTUQ4QSBMcVY2
eWRnb05sMEZVZUpicGVYN2ZSUm1mUmVsdUsrSHR4c2I2SXVRWDF3CnlhUkxoeEx5
M1NNcGR3bmx6ZW5RaHU3L2l4WEowdWYzQk0xa3E4ZUtwNkkKLT4gIi1ncmVhc2Ug
SllwSzlIIEdAKltUKSBjSWpmCnljSXlZTXZBL2xuMEtma1NUNjdCM0dMNHJuVjFS
enV5LzF1NlYzbFNURW1rTlI2aUxCRFFxK2ZJdktsTTU1ZSsKNUZiZmVQWQotLS0g
bTVrZEdWMHFWK0Q5RzZUc09PYmlEQjNweXRxU2FETWNWTXRUVEFKUUFpdwqtUbmM
kbnj4Q+9QlnJHuWBVu+BcWPl5HuiPUjrrxnWAuN6rFYd79H7qk9MagDB/2BAEk82
iuQeqS0r8wAmp9bTzhbiMQEbtN96huSGA2aO9I/RoPU1jv1Upi8bNy+KX0jSsfV9
+cGFm0JsBZ4o5acq4isanC+3YkNq+IHxyDqUdwWMIHiEkR26pwndyMAaJTCBAgTN
kmdWMK4PxR36ElY+8J0tiOv6VmGS73rVS4R/2Rdc5ICx6QO6oQQqETKYxqcmr8eD
8dgkZVpmBF/iuw7BYZ2U/p2PZSzgEVTVqfT3VO8WQ9ii2a4dw+2QEORgISbhxzoy
WGB0q4X33QWEYSp0FNyAG4Kc9yTphUq+hMHfNOtz3XmTZLltcVfA6XQbKwB/nDbq
G1gAfDwdEZ5OpN2IT0S6Zc2rKKeovqdFYWgqmDWiaBfqncl9qLH37KkpKqmUSQSe
zyriQt8nZCzVrh4EKohjeLogVBsn0tPTYqiFnV+ZFK/kOWIYWduWDJcM3zwRVjx7
Mr5l81gFv4WRHbnQB9eynGJLZYs1lI4/X9tKRUgUj0mN20Bt80NXGNPaJ3TAkrCO
rX0tgjsX/Sc59JTHaMOT419+ob9UtDoS7mGlxswKfyhvjzluu+EdHd0GRJ5PUsSq
8YlKYjRonBqhC0Ju9CRuZS0pk2bh72bG9z1Gb4LcJ0pLJ6lMfmF1j4zzfzsABe5G
0bJF3ikzNxo6Wzsf3rk45/FvMhKjkI5O3Btnfp+Vkw+iRqDMh7wiD6cczNSp+Skd
Et43NIobiLa6O95y2YEjqSkT5T0ug1nbLkygmxwffVn6RTWgScZSfBPvOKTINAVo
J/MU2c0DaBm4glLfm4IWaJNcEmZn8+FWG6m42WEWTMEfSeAo5XXaEb0FsK0Yqd3+
RlE/b6a/DdoNEAQOlEPSASsoQhTVvsiEwH4Pq5G0STHtSBBIT/xJow4pa/GOiQnn
yAFva9F7KFYWA7bjbRbv+B21bvss0T+BK9HRF+bklSzjxBNfDEWXW0GhwIiahwXW
Fxi3BUMZcfgoLg2NkhMb3irwJUoGqQFqn68e2jTJVyUATyVgGV5mzfjDBB8thcDt
ehuIL/Y1bUVGWZteU/JAF7z+Fb1yBO2FJqpCKIcfd+JgkWVtQKaqYGjcuzbI7ce4
fcrRDNCse2pBO1unE7HS160rK+dMWavlrsHHZKezsvNv7TwMj1SKjCKSVr8up7TC
NLW0I6uXGEcUBj+RNqF6frdpw/Ve1WTAkIbznMV6kZ8cTfAGhOzJ0wxHMBSQka8X
uMatPuGywu+dvjrJXTwD0gJD/Jrd88K685ahu86nSt/DYnxIYfQhwo/oZMR7E6kN
4NgGtYjRU0asmio6sF8D1uA2YgmxNPRw2GwTqpS2XyYaEh3B6yjqa4pJ1vfo6t/g
aPLhMuT4qt/eKMvSiR+sTaOMNkcLWmCpY762aYk4XUZFTYAAY5XsNSAU+Hs7o/Eu
9NSMrIrpqLAcgzr/nZZTLqswbsYtdVl5WUd9uqdQe+AhTbrkcvHibUrVgW/XU+oq
QdHXUyfn/IByp2uE7WZpfRVhwjXg/LQJh/ogksbzrh4/anOivku+n1ouciAAgnQn
04i6taBu+393ysMC/sp7wZkp//yAZj2SNPOTzbakG8xWoVGzbqxLCIOIE7I97KBv
G49jiEOS42vZZXSGLxQND+N0aOqosZfQ1WKpI9XjirB8qVOt/sr6uqEIx311V+BJ
wrdoMa9hWmDFzf3+ThqfUuHOtxxJXReL7vC4J7K8iU6nCVIGJN7axifk
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==
-----END AGE ENCRYPTED FILE-----