feat(nixos): add control center host with hypervisor gitops service

This commit is contained in:
Tibo De Peuter 2026-07-27 22:01:40 +02:00
parent 89cec4e0f3
commit dc9b7a30d9
Signed by: tdpeuter
SSH key fingerprint: SHA256:u/h/LVoqKF1Iz02uOyxe6hcjmoZASCGV2HM0TG9ZMoU
10 changed files with 259 additions and 42 deletions

View file

@ -70,7 +70,7 @@
}; };
hosts = { hosts = {
# Hosts will be populated here as they are migrated to the v2 branch. izanagi.modules = [ ./hosts/izanagi ];
}; };
}; };
} }

View file

@ -0,0 +1,52 @@
{ config, pkgs, ... }:
{
networking.hostName = "izanagi";
homelab = {
# Enable the standard guest VM configuration
virtualisation.guest.enable = true;
# Enable standard GitOps for the Control Center itself
common.gitops.enable = true;
# We might pull from our Forgejo instance eventually, but for bootstrap
# it might need to pull from Github or the local Gitea if it's up.
# common.gitops.repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git";
services = {
openssh.enable = true;
# Enable the Hypervisor GitOps service to provision OTHER VMs
hypervisor-gitops = {
enable = true;
repoUrl = "https://git.depeuter.dev/Bos55/nix-config.git";
pollInterval = "hourly";
};
};
};
sops = {
defaultSopsFile = ./secrets.yaml;
defaultSopsFormat = "yaml";
age.keyFile = "/var/lib/sops-age/keys.txt"; # Injected via Cloud-Init during Phase 4
secrets = {
proxmox_api_token.owner = "root";
forgejo_token.owner = "root";
};
templates."hypervisor-gitops.env".content = ''
PROXMOX_VE_API_TOKEN=${config.sops.placeholder.proxmox_api_token}
FORGEJO_TOKEN=${config.sops.placeholder.forgejo_token}
PROXMOX_VE_ENDPOINT=https://mikoshi:8006/
PROXMOX_VE_INSECURE=true
'';
};
# Make the secrets available to the hypervisor-gitops service via EnvironmentFile
systemd.services.hypervisor-gitops.serviceConfig.EnvironmentFile = [
config.sops.templates."hypervisor-gitops.env".path
];
system.stateVersion = "24.05";
}

View file

@ -0,0 +1,5 @@
# This is a placeholder SOPS file.
# You must encrypt it with sops using your age key before deploying.
# sops -e -i secrets.yaml
proxmox_api_token: "PLACEHOLDER_TOKEN"
forgejo_token: "PLACEHOLDER_TOKEN"

View file

@ -6,42 +6,7 @@ let
updateScript = pkgs.writeShellApplication { updateScript = pkgs.writeShellApplication {
name = "homelab-gitops-update"; name = "homelab-gitops-update";
runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ]; runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ];
text = '' text = builtins.readFile ../../../../scripts/nixos-sync.sh;
set -euo pipefail
REMOTE_URL="${cfg.repoUrl}"
BRANCH="${cfg.branch}"
echo "Checking remote hash for $REMOTE_URL branch $BRANCH..."
# Fetch remote hash, fallback to unknown if it fails
REMOTE_HASH=$(git ls-remote "$REMOTE_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true)
if [ -z "$REMOTE_HASH" ]; then
echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe."
REMOTE_HASH="unknown_remote"
fi
LOCAL_HASH="unknown_local"
if [ -f /run/current-system/configurationRevision ]; then
LOCAL_HASH=$(cat /run/current-system/configurationRevision)
fi
echo "Remote hash: $REMOTE_HASH"
echo "Local hash: $LOCAL_HASH"
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then
echo "Hashes match. No update needed."
exit 0
fi
echo "Hashes differ or unknown. Triggering nixos-rebuild..."
# Trigger the build and switch
nixos-rebuild switch --flake "git+$REMOTE_URL?dir=nixos&ref=$BRANCH"
echo "Update successful."
'';
}; };
in { in {
@ -68,14 +33,13 @@ in {
}; };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
# 1. Systemd Service and Timer for polling
systemd.services.homelab-gitops = { systemd.services.homelab-gitops = {
description = "Homelab GitOps Update Service"; description = "Homelab GitOps Update Service";
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
ExecStart = "${updateScript}/bin/homelab-gitops-update"; ExecStart = "${updateScript}/bin/homelab-gitops-update ${cfg.repoUrl} ${cfg.branch}";
# Must run as root to rebuild the system # Must run as root to rebuild the system
User = "root"; User = "root";
}; };
@ -91,7 +55,6 @@ in {
}; };
}; };
# 2. Webhook listener for instant trigger
sops.secrets."webhook-secret" = {}; sops.secrets."webhook-secret" = {};
services.webhook = { services.webhook = {
@ -121,7 +84,6 @@ in {
# Inject the secret as an environment variable into the webhook service # Inject the secret as an environment variable into the webhook service
systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path; systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path;
# 3. Builder Configuration
sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {}; sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {};
nix.buildMachines = lib.mkIf cfg.useBuilder [ nix.buildMachines = lib.mkIf cfg.useBuilder [

View file

@ -2,5 +2,6 @@
imports = [ imports = [
./actions ./actions
./openssh ./openssh
./hypervisor-gitops
]; ];
} }

View file

@ -0,0 +1,64 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.homelab.services.hypervisor-gitops;
hypervisorSyncScript = pkgs.writeShellApplication {
name = "hypervisor-sync";
runtimeInputs = with pkgs; [ git opentofu coreutils ];
text = builtins.readFile ../../../../scripts/hypervisor-sync.sh;
};
in {
options.homelab.services.hypervisor-gitops = {
enable = mkEnableOption "Hypervisor GitOps Service";
repoUrl = mkOption {
type = types.str;
description = "The URL of the git repository to pull";
};
# TODO Replace with webhooks
pollInterval = mkOption {
type = types.str;
default = "hourly";
description = "Systemd calendar event for polling interval";
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
git
opentofu
];
systemd.services.hypervisor-gitops = {
description = "Hypervisor GitOps Polling Service";
# We need network access to reach Forgejo and Proxmox API
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = "root"; # Needs root to read SOPS secrets potentially
# We will create a state directory for the repo
StateDirectory = "hypervisor-gitops";
WorkingDirectory = "/var/lib/hypervisor-gitops";
ExecStart = "${hypervisorSyncScript}/bin/hypervisor-sync ${cfg.repoUrl} opentofu/nodes/mikoshi";
};
};
systemd.timers.hypervisor-gitops = {
description = "Timer for Hypervisor GitOps Service";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.pollInterval;
Persistent = true;
};
};
};
}

View file

@ -9,7 +9,7 @@ in {
type = lib.types.listOf lib.types.str; type = lib.types.listOf lib.types.str;
default = [ default = [
# HomeLab > NixOS > admin > ssh # HomeLab > NixOS > admin > ssh
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHOoTp+e6qWGn4Sco5CZ6G0zrX5NAQBpLlDVirncJ/HqAAAABHNzaDo="
]; ];
}; };
}; };

View file

@ -28,3 +28,57 @@ output "control_center_api_token" {
sensitive = true sensitive = true
description = "The secret API token for control-center@pve." description = "The secret API token for control-center@pve."
} }
resource "proxmox_virtual_environment_vm" "control_center" {
depends_on = [module.proxmox_node]
name = "izanagi"
description = "Managed by OpenTofu - GitOps Control Center"
tags = ["infrastructure", "gitops"]
node_name = "mikoshi"
vm_id = 100001000
on_boot = true
pool_id = "core"
cpu {
cores = 2
type = "x86-64-v2-AES"
}
memory {
dedicated = 2048
}
agent {
enabled = true
}
network_device {
bridge = "vmbr0"
}
disk {
datastore_id = "data"
file_id = "local:iso/nixos-minimal.iso" # TODO Replace with actual ISO or use Clone
interface = "scsi0"
size = 20
file_format = "raw"
}
# Cloud-Init for initial SSH access and SOPS age key injection
initialization {
ip_config {
ipv4 {
address = "dhcp"
}
}
user_account {
username = "gh0st"
keys = [
"sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHOoTp+e6qWGn4Sco5CZ6G0zrX5NAQBpLlDVirncJ/HqAAAABHNzaDo="
]
}
}
}

View file

@ -0,0 +1,39 @@
#!/usr/bin/env bash
# This script pulls the latest changes from the Git repository
# and runs OpenTofu to provision the hypervisor state.
# Usage: ./hypervisor-sync.sh <REPO_URL> <OPENTOFU_DIR>
REPO_URL=${1:-"https://git.depeuter.dev/Bos55/nix-config.git"}
OPENTOFU_DIR=${2:-"opentofu/nodes/mikoshi"}
echo "Starting Hypervisor GitOps sync..."
if [ ! -d "nix-config" ]; then
echo "Cloning repository from $REPO_URL..."
git clone "$REPO_URL" nix-config
fi
cd nix-config || exit
git fetch origin main
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [ "$LOCAL" = "$REMOTE" ]; then
echo "Already up to date. Nothing to do."
exit 0
fi
echo "Changes detected. Updating from $LOCAL to $REMOTE..."
git reset --hard origin/main
echo "Applying OpenTofu changes in $OPENTOFU_DIR..."
cd "$OPENTOFU_DIR" || exit
tofu init -upgrade
tofu apply -auto-approve
echo "Hypervisor GitOps sync completed successfully."

40
scripts/nixos-sync.sh Normal file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
# This script checks the remote Git repository for changes
# and triggers a nixos-rebuild if a new commit is found.
# Usage: ./nixos-sync.sh <REPO_URL> <BRANCH>
set -euo pipefail
REPO_URL=${1:-"https://git.depeuter.dev/Bos55/nix-config.git"}
BRANCH=${2:-"main"}
echo "Checking remote hash for $REPO_URL branch $BRANCH..."
# Fetch remote hash, fallback to unknown if it fails
REMOTE_HASH=$(git ls-remote "$REPO_URL" "refs/heads/$BRANCH" | awk '{print $1}' || true)
if [ -z "$REMOTE_HASH" ]; then
echo "WARNING: Could not fetch remote hash. Forcing rebuild to be safe."
REMOTE_HASH="unknown_remote"
fi
LOCAL_HASH="unknown_local"
if [ -f /run/current-system/configurationRevision ]; then
LOCAL_HASH=$(cat /run/current-system/configurationRevision)
fi
echo "Remote hash: $REMOTE_HASH"
echo "Local hash: $LOCAL_HASH"
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ] && [ "$REMOTE_HASH" != "unknown_remote" ] && [ "$LOCAL_HASH" != "unknown" ]; then
echo "Hashes match. No update needed."
exit 0
fi
echo "Hashes differ or unknown. Triggering nixos-rebuild..."
# Trigger the build and switch
nixos-rebuild switch --flake "git+$REPO_URL?dir=nixos&ref=$BRANCH"
echo "Update successful."