From 9d0fb194a218332084a08f09ed3e88cea6102e2e Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:29:19 -0400 Subject: [PATCH] feat(module): sourceos-syncd NixOS module and builder-aarch64 wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - modules/nixos/sourceos-syncd/default.nix: declarative systemd service for the sync daemon + post-boot health-check timer with auto-rollback on failure. Supports katelloPasswordFile for SOPS-managed credential injection. - flake.nix: exposes nixosModules.sourceos-syncd; wires it into builder-aarch64. - hosts/builder-aarch64/default.nix: enables sourceos.syncd with local Katello defaults, 300s poll, health-check with rollback-on-failure after 120s. katelloPasswordFile placeholder left commented — set via sops-nix before first activation. --- flake.nix | 5 + hosts/builder-aarch64/default.nix | 21 +++ modules/nixos/sourceos-syncd/default.nix | 217 +++++++++++++++++++++++ 3 files changed, 243 insertions(+) create mode 100644 modules/nixos/sourceos-syncd/default.nix diff --git a/flake.nix b/flake.nix index 1b12432..2bb8d76 100644 --- a/flake.nix +++ b/flake.nix @@ -75,12 +75,17 @@ }; }); + nixosModules = { + sourceos-syncd = import ./modules/nixos/sourceos-syncd/default.nix; + }; + nixosConfigurations = { builder-aarch64 = lib.nixosSystem { system = "aarch64-linux"; specialArgs = { inherit self; }; modules = [ nixos-apple-silicon.nixosModules.apple-silicon-support + self.nixosModules.sourceos-syncd ./hosts/builder-aarch64/default.nix ]; }; diff --git a/hosts/builder-aarch64/default.nix b/hosts/builder-aarch64/default.nix index 6d1645d..49d724d 100644 --- a/hosts/builder-aarch64/default.nix +++ b/hosts/builder-aarch64/default.nix @@ -51,5 +51,26 @@ security.sudo.wheelNeedsPassword = false; + # sourceos-syncd daemon: polls local Katello (127.0.0.1:8443) every 5 min + # and applies new content view versions automatically. + # katelloPassword is loaded from the SOPS-managed secrets file at runtime; + # set sourceos.syncd.katelloPasswordFile before activating. + sourceos.syncd = { + enable = true; + katelloUrl = "https://127.0.0.1:8443"; + lifecycleEnv = "stable"; + locus = "local"; + flakeRef = "github:SociOS-Linux/source-os#builder-aarch64"; + pollInterval = 300; + noVerifySsl = true; # local self-signed cert; disable when real cert is provisioned + healthCheck = { + enable = true; + delayAfterBootSec = 120; + rollbackOnFailure = true; + }; + # katelloPasswordFile = "/run/secrets/katello-password"; + # Set katelloPasswordFile (via sops-nix) before first activation. + }; + system.stateVersion = "25.05"; } diff --git a/modules/nixos/sourceos-syncd/default.nix b/modules/nixos/sourceos-syncd/default.nix new file mode 100644 index 0000000..358690b --- /dev/null +++ b/modules/nixos/sourceos-syncd/default.nix @@ -0,0 +1,217 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.sourceos.syncd; + + # sourceos-syncd is installed as a Python package on the device. + # The package derivation lives in packages/sourceos-syncd.nix; this module + # just wires it into systemd. If the package is not yet in the flake's + # packages output, set cfg.package to pkgs.sourceos-syncd once it's built. + defaultPackage = cfg.package; + + syncEnv = lib.optionalAttrs (cfg.katelloPassword != "") { + KATELLO_PASSWORD = cfg.katelloPassword; + } // lib.optionalAttrs (cfg.katelloPasswordFile != "") { + # prefer the secrets file over inline value + }; + + credentialFile = lib.mkIf (cfg.katelloPasswordFile != "") cfg.katelloPasswordFile; + +in +{ + options.sourceos.syncd = { + enable = lib.mkEnableOption "sourceos-syncd content-view sync daemon"; + + package = lib.mkOption { + type = lib.types.package; + description = "The sourceos-syncd package to use."; + }; + + katelloUrl = lib.mkOption { + type = lib.types.str; + default = "https://127.0.0.1:8443"; + description = "Foreman+Katello base URL."; + }; + + katelloUser = lib.mkOption { + type = lib.types.str; + default = "admin"; + description = "Katello admin username."; + }; + + katelloPassword = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Katello password (plaintext — prefer katelloPasswordFile)."; + }; + + katelloPasswordFile = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Path to file containing KATELLO_PASSWORD. Loaded via systemd LoadCredential."; + }; + + org = lib.mkOption { + type = lib.types.str; + default = "SocioProphet"; + description = "Katello organization name."; + }; + + contentView = lib.mkOption { + type = lib.types.str; + default = "sourceos-builder-aarch64"; + description = "Katello content view name."; + }; + + lifecycleEnv = lib.mkOption { + type = lib.types.enum [ "dev" "candidate" "stable" ]; + default = "stable"; + description = "Katello lifecycle environment to track."; + }; + + locus = lib.mkOption { + type = lib.types.enum [ "local" "trusted_private" ]; + default = "local"; + description = "Execution locus. Must be 'local' or 'trusted_private' for Phase 0."; + }; + + flakeRef = lib.mkOption { + type = lib.types.str; + default = "github:SociOS-Linux/source-os#builder-aarch64"; + description = "NixOS flake ref passed to nixos-rebuild switch."; + }; + + pollInterval = lib.mkOption { + type = lib.types.ints.positive; + default = 300; + description = "Katello poll interval in seconds."; + }; + + storeRoot = lib.mkOption { + type = lib.types.str; + default = "/var/lib/sourceos-syncd"; + description = "Directory for state (current-version) and receipts."; + }; + + noVerifySsl = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Skip TLS certificate verification (local dev only)."; + }; + + # Health check options + + healthCheck = { + enable = lib.mkEnableOption "post-boot health check with auto-rollback" // { default = true; }; + + delayAfterBootSec = lib.mkOption { + type = lib.types.ints.positive; + default = 120; + description = "Seconds after boot-complete before running the health check."; + }; + + rollbackOnFailure = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Execute nixos-rebuild --rollback if health check fails."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + + # ── state directory ────────────────────────────────────────────────────── + systemd.tmpfiles.rules = [ + "d ${cfg.storeRoot} 0750 sourceos-syncd sourceos-syncd -" + "d ${cfg.storeRoot}/receipts 0750 sourceos-syncd sourceos-syncd -" + ]; + + users.users.sourceos-syncd = { + isSystemUser = true; + group = "sourceos-syncd"; + description = "sourceos-syncd daemon user"; + home = cfg.storeRoot; + }; + users.groups.sourceos-syncd = {}; + + # ── sync daemon ────────────────────────────────────────────────────────── + systemd.services.sourceos-syncd = { + description = "SourceOS content-view sync daemon"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + User = "sourceos-syncd"; + Group = "sourceos-syncd"; + StateDirectory = "sourceos-syncd"; + Restart = "on-failure"; + RestartSec = "30s"; + + ExecStart = lib.concatStringsSep " " ( + [ "${defaultPackage}/bin/sourceos-syncd" "sync" "daemon" ] + ++ [ "--katello-url" cfg.katelloUrl ] + ++ [ "--katello-user" cfg.katelloUser ] + ++ [ "--org" cfg.org ] + ++ [ "--content-view" cfg.contentView ] + ++ [ "--lifecycle-env" cfg.lifecycleEnv ] + ++ [ "--locus" cfg.locus ] + ++ [ "--flake-ref" cfg.flakeRef ] + ++ [ "--poll-interval" (toString cfg.pollInterval) ] + ++ [ "--store-root" cfg.storeRoot ] + ++ lib.optional cfg.noVerifySsl "--no-verify-ssl" + ); + + Environment = lib.mapAttrsToList (k: v: "${k}=${v}") ( + lib.optionalAttrs (cfg.katelloPassword != "") { + KATELLO_PASSWORD = cfg.katelloPassword; + } + ); + + LoadCredential = lib.optional (cfg.katelloPasswordFile != "") + "KATELLO_PASSWORD:${cfg.katelloPasswordFile}"; + + EnvironmentFiles = lib.optional (cfg.katelloPasswordFile != "") + "/run/credentials/sourceos-syncd.service/KATELLO_PASSWORD"; + }; + }; + + # ── health check + auto-rollback ───────────────────────────────────────── + systemd.services.sourceos-health-check = lib.mkIf cfg.healthCheck.enable { + description = "SourceOS post-boot health check"; + after = [ "network-online.target" "sourceos-syncd.service" ]; + wants = [ "network-online.target" ]; + + serviceConfig = { + Type = "oneshot"; + User = "sourceos-syncd"; + RemainAfterExit = false; + + ExecStart = pkgs.writeShellScript "sourceos-health-check" '' + set -euo pipefail + ${defaultPackage}/bin/sourceos-syncd sync check-health \ + --store-root ${lib.escapeShellArg cfg.storeRoot} \ + --katello-url ${lib.escapeShellArg cfg.katelloUrl} \ + ${lib.optionalString cfg.noVerifySsl "--no-verify-ssl"} \ + && exit 0 + + echo "sourceos-health-check: UNHEALTHY" >&2 + ${lib.optionalString cfg.healthCheck.rollbackOnFailure '' + echo "sourceos-health-check: triggering rollback" >&2 + ${pkgs.sourceos-boot or defaultPackage}/bin/sourceos-boot rollback execute --execute || true + ''} + exit 2 + ''; + }; + }; + + systemd.timers.sourceos-health-check = lib.mkIf cfg.healthCheck.enable { + description = "SourceOS post-boot health check timer"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnBootSec = "${toString cfg.healthCheck.delayAfterBootSec}s"; + Unit = "sourceos-health-check.service"; + }; + }; + }; +}