Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
];
};
Expand Down
21 changes: 21 additions & 0 deletions hosts/builder-aarch64/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
217 changes: 217 additions & 0 deletions modules/nixos/sourceos-syncd/default.nix
Original file line number Diff line number Diff line change
@@ -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";
};
};
};
}