diff --git a/nixos/flake.nix b/nixos/flake.nix index 5450b8d..bd6a5d1 100644 --- a/nixos/flake.nix +++ b/nixos/flake.nix @@ -70,7 +70,7 @@ }; hosts = { - # Hosts will be populated here as they are migrated to the v2 branch. + izanagi.modules = [ ./hosts/izanagi ]; }; }; } diff --git a/nixos/hosts/izanagi/default.nix b/nixos/hosts/izanagi/default.nix new file mode 100644 index 0000000..5403c50 --- /dev/null +++ b/nixos/hosts/izanagi/default.nix @@ -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"; +} diff --git a/nixos/hosts/izanagi/secrets.yaml b/nixos/hosts/izanagi/secrets.yaml new file mode 100644 index 0000000..6aabf9e --- /dev/null +++ b/nixos/hosts/izanagi/secrets.yaml @@ -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" diff --git a/nixos/modules/common/gitops.nix b/nixos/modules/common/gitops.nix index 224ef4c..c182365 100644 --- a/nixos/modules/common/gitops.nix +++ b/nixos/modules/common/gitops.nix @@ -6,42 +6,7 @@ let updateScript = pkgs.writeShellApplication { name = "homelab-gitops-update"; runtimeInputs = [ pkgs.git pkgs.nixos-rebuild pkgs.jq pkgs.coreutils ]; - text = '' - 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." - ''; + text = builtins.readFile ../../../../scripts/nixos-sync.sh; }; in { @@ -68,14 +33,13 @@ in { }; config = lib.mkIf cfg.enable { - # 1. Systemd Service and Timer for polling systemd.services.homelab-gitops = { description = "Homelab GitOps Update Service"; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig = { 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 User = "root"; }; @@ -91,7 +55,6 @@ in { }; }; - # 2. Webhook listener for instant trigger sops.secrets."webhook-secret" = {}; services.webhook = { @@ -121,7 +84,6 @@ in { # Inject the secret as an environment variable into the webhook service systemd.services.webhook.serviceConfig.EnvironmentFile = config.sops.secrets."webhook-secret".path; - # 3. Builder Configuration sops.secrets."builder-ssh-key" = lib.mkIf cfg.useBuilder {}; nix.buildMachines = lib.mkIf cfg.useBuilder [ diff --git a/nixos/modules/services/default.nix b/nixos/modules/services/default.nix index f70bc54..ec5cb63 100644 --- a/nixos/modules/services/default.nix +++ b/nixos/modules/services/default.nix @@ -2,5 +2,6 @@ imports = [ ./actions ./openssh + ./hypervisor-gitops ]; } diff --git a/nixos/modules/services/hypervisor-gitops/default.nix b/nixos/modules/services/hypervisor-gitops/default.nix new file mode 100644 index 0000000..ac8f49f --- /dev/null +++ b/nixos/modules/services/hypervisor-gitops/default.nix @@ -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; + }; + }; + }; +} diff --git a/nixos/users/admin/default.nix b/nixos/users/admin/default.nix index dc01c81..65410ff 100644 --- a/nixos/users/admin/default.nix +++ b/nixos/users/admin/default.nix @@ -9,7 +9,7 @@ in { type = lib.types.listOf lib.types.str; default = [ # HomeLab > NixOS > admin > ssh - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWIOOEqTy8cWKpENVbzD4p7bsQgQb/Dgpzk8i0dZ00T" + "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIHOoTp+e6qWGn4Sco5CZ6G0zrX5NAQBpLlDVirncJ/HqAAAABHNzaDo=" ]; }; }; diff --git a/opentofu/nodes/mikoshi/main.tf b/opentofu/nodes/mikoshi/main.tf index 08c10a9..93dee38 100644 --- a/opentofu/nodes/mikoshi/main.tf +++ b/opentofu/nodes/mikoshi/main.tf @@ -28,3 +28,57 @@ output "control_center_api_token" { sensitive = true 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=" + ] + } + } +} diff --git a/scripts/hypervisor-sync.sh b/scripts/hypervisor-sync.sh new file mode 100644 index 0000000..1a5536b --- /dev/null +++ b/scripts/hypervisor-sync.sh @@ -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=${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." diff --git a/scripts/nixos-sync.sh b/scripts/nixos-sync.sh new file mode 100644 index 0000000..18f0c0f --- /dev/null +++ b/scripts/nixos-sync.sh @@ -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 + +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."