301 lines
9.9 KiB
Nix
301 lines
9.9 KiB
Nix
{
|
|
inputs,
|
|
hosts ? import ../inventory.nix,
|
|
...
|
|
}:
|
|
|
|
# ============================================================================
|
|
# Host Generator
|
|
# ============================================================================
|
|
# This file contains the logic to generate NixOS configurations for all hosts
|
|
# defined in inventory.nix. It supports both hostname-based and count-based
|
|
# configurations with flexible type associations.
|
|
#
|
|
# Inventory format:
|
|
# {
|
|
# "my-hostname" = {
|
|
# type = "nix-desktop"; # Host type module to use
|
|
# system = "x86_64-linux"; # Optional
|
|
# # ... any athenix.* options or device-specific config
|
|
# };
|
|
#
|
|
# "lab-prefix" = {
|
|
# type = "nix-laptop";
|
|
# count = 5; # Generates lab-prefix1, lab-prefix2, ... lab-prefix5
|
|
# devices = {
|
|
# "machine-1" = { ... }; # Override for lab-prefix1
|
|
# };
|
|
# };
|
|
# }
|
|
|
|
let
|
|
nixpkgs = inputs.nixpkgs;
|
|
lib = nixpkgs.lib;
|
|
# Helper to create a single NixOS system configuration
|
|
mkHost =
|
|
{
|
|
hostName,
|
|
system ? "x86_64-linux",
|
|
hostType,
|
|
configOverrides ? { },
|
|
externalModulePath ? null,
|
|
}:
|
|
let
|
|
# Load users.nix to find external user modules
|
|
pkgs = nixpkgs.legacyPackages.${system};
|
|
usersData = import ../users.nix { inherit pkgs; };
|
|
accounts = usersData.athenix.users or { };
|
|
|
|
# Build a map of user names to their nixos module paths (if they exist)
|
|
# We'll use this to conditionally import modules based on user.enable
|
|
userNixosModulePaths = lib.filterAttrs (_: v: v != null) (
|
|
lib.mapAttrs (
|
|
name: user:
|
|
if (user ? external && user.external != null) then
|
|
let
|
|
externalPath =
|
|
if builtins.isAttrs user.external && user.external ? outPath then
|
|
user.external.outPath
|
|
else
|
|
user.external;
|
|
nixosModulePath = externalPath + "/nixos.nix";
|
|
in
|
|
if
|
|
(builtins.isPath externalPath || (builtins.isString externalPath && lib.hasPrefix "/" externalPath))
|
|
&& builtins.pathExists nixosModulePath
|
|
then
|
|
nixosModulePath
|
|
else
|
|
null
|
|
else
|
|
null
|
|
) accounts
|
|
);
|
|
|
|
# Create conditional wrapper modules for each user's nixos.nix
|
|
# Each wrapper checks if the user is enabled before applying the module content
|
|
userNixosModules = lib.mapAttrsToList (
|
|
name: modulePath:
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}@args:
|
|
let
|
|
# Import the user's nixos module - it returns a function or attrset
|
|
importedModuleFunc = import modulePath { inherit inputs; };
|
|
# If it's a function, call it with the module args; otherwise use as-is
|
|
importedModule =
|
|
if lib.isFunction importedModuleFunc then importedModuleFunc args else importedModuleFunc;
|
|
in
|
|
{
|
|
config = lib.mkIf (config.athenix.users.${name}.enable or false) importedModule;
|
|
}
|
|
) userNixosModulePaths;
|
|
|
|
# Load the host type module
|
|
typeFile = ./types + "/${hostType}.nix";
|
|
typeModule =
|
|
if builtins.pathExists typeFile then
|
|
import typeFile { inherit inputs; }
|
|
else
|
|
throw "Host type '${hostType}' not found in hosts/types/";
|
|
|
|
# External module from fetchGit/fetchurl
|
|
externalPathModule =
|
|
if externalModulePath != null then import externalModulePath { inherit inputs; } else { };
|
|
|
|
# Config override module - translate special keys to athenix options
|
|
overrideModule =
|
|
{ ... }:
|
|
let
|
|
cleanConfig = lib.removeAttrs configOverrides [
|
|
"type"
|
|
"count"
|
|
"devices"
|
|
"overrides"
|
|
"defaultCount"
|
|
"buildMethods"
|
|
];
|
|
specialConfig = lib.optionalAttrs (configOverrides ? buildMethods) {
|
|
athenix.host.buildMethods = configOverrides.buildMethods;
|
|
};
|
|
in
|
|
{
|
|
config = lib.mkMerge [
|
|
cleanConfig
|
|
specialConfig
|
|
];
|
|
};
|
|
|
|
allModules =
|
|
userNixosModules
|
|
++ [
|
|
typeModule
|
|
overrideModule
|
|
{ networking.hostName = hostName; }
|
|
]
|
|
++ lib.optional (externalModulePath != null) externalPathModule;
|
|
in
|
|
{
|
|
system = lib.nixosSystem {
|
|
inherit system;
|
|
specialArgs = { inherit inputs; };
|
|
modules = allModules;
|
|
};
|
|
modules = allModules;
|
|
};
|
|
|
|
# Process inventory entries - top-level keys are always prefixes
|
|
processInventory = lib.mapAttrs (
|
|
prefix: config:
|
|
let
|
|
hostType = config.type or prefix;
|
|
system = config.system or "x86_64-linux";
|
|
devices = config.devices or { };
|
|
hasCount = config ? count;
|
|
|
|
# Helper to generate hostname from prefix and suffix
|
|
# Numbers get no dash: "nix-surface1", "nix-surface2"
|
|
# Letters get dash: "nix-surface-alpha", "nix-surface-beta"
|
|
mkHostName =
|
|
prefix: suffix: usePrefix:
|
|
if !usePrefix then
|
|
suffix
|
|
else if lib.match "[0-9]+" suffix != null then
|
|
"${prefix}${suffix}" # numeric: no dash
|
|
else
|
|
"${prefix}-${suffix}"; # non-numeric: add dash
|
|
|
|
# Extract common overrides and default count
|
|
overrides = config.overrides or { };
|
|
defaultCount = config.defaultCount or 0;
|
|
|
|
# If devices is a number, treat it as count
|
|
devicesValue = config.devices or { };
|
|
actualDevices = if lib.isInt devicesValue then { } else devicesValue;
|
|
actualCount = if lib.isInt devicesValue then devicesValue else (config.count or 0);
|
|
|
|
# Clean base config - remove inventory management keys
|
|
baseConfig = lib.removeAttrs config [
|
|
"type"
|
|
"system"
|
|
"count"
|
|
"devices"
|
|
"overrides"
|
|
"defaultCount"
|
|
];
|
|
|
|
# Generate hosts from explicit device definitions
|
|
deviceHosts = lib.listToAttrs (
|
|
lib.mapAttrsToList (
|
|
deviceKey: deviceConfig:
|
|
let
|
|
# Check if deviceConfig is a path/derivation (from fetchGit, fetchurl, etc.)
|
|
# fetchGit/fetchTarball return an attrset with outPath attribute
|
|
isExternalModule =
|
|
(builtins.isPath deviceConfig)
|
|
|| (builtins.isString deviceConfig && lib.hasPrefix "/" deviceConfig)
|
|
|| (lib.isDerivation deviceConfig)
|
|
|| (builtins.isAttrs deviceConfig && deviceConfig ? outPath);
|
|
|
|
# Extract the actual path from fetchGit/fetchTarball results
|
|
extractedPath =
|
|
if builtins.isAttrs deviceConfig && deviceConfig ? outPath then
|
|
deviceConfig.outPath
|
|
else
|
|
deviceConfig;
|
|
|
|
# If external module, we use base config + overrides as the config
|
|
# and pass the module path separately
|
|
actualConfig =
|
|
if isExternalModule then (lib.recursiveUpdate baseConfig overrides) else deviceConfig;
|
|
|
|
# Merge: base config -> overrides -> device-specific config (only if not external module)
|
|
mergedConfig =
|
|
if isExternalModule then
|
|
actualConfig
|
|
else
|
|
lib.recursiveUpdate (lib.recursiveUpdate baseConfig overrides) deviceConfig;
|
|
|
|
# Check useHostPrefix from the merged config
|
|
usePrefix = mergedConfig.athenix.host.useHostPrefix or true;
|
|
hostName = mkHostName prefix deviceKey usePrefix;
|
|
|
|
# If external module, also add a default.nix path for import
|
|
externalModulePath =
|
|
if isExternalModule then
|
|
if builtins.isPath extractedPath then
|
|
extractedPath + "/default.nix"
|
|
else if lib.isDerivation extractedPath then
|
|
extractedPath + "/default.nix"
|
|
else
|
|
extractedPath + "/default.nix"
|
|
else
|
|
null;
|
|
in
|
|
{
|
|
name = hostName;
|
|
value = mkHost {
|
|
inherit
|
|
hostName
|
|
system
|
|
hostType
|
|
externalModulePath
|
|
;
|
|
configOverrides = mergedConfig;
|
|
};
|
|
}
|
|
) actualDevices
|
|
);
|
|
|
|
# Generate numbered hosts from count or defaultCount
|
|
# If devices are specified, defaultCount fills in the gaps
|
|
countToUse = if actualCount > 0 then actualCount else defaultCount;
|
|
|
|
# Get which numbered keys are already defined in devices
|
|
existingNumbers = lib.filter (k: lib.match "[0-9]+" k != null) (lib.attrNames actualDevices);
|
|
|
|
countHosts =
|
|
if countToUse > 0 then
|
|
lib.listToAttrs (
|
|
lib.filter (x: x != null) (
|
|
map (
|
|
i:
|
|
let
|
|
deviceKey = toString i;
|
|
# Skip if this number is already in explicit devices
|
|
alreadyDefined = lib.elem deviceKey existingNumbers;
|
|
in
|
|
if alreadyDefined then
|
|
null
|
|
else
|
|
let
|
|
hostName = mkHostName prefix deviceKey true;
|
|
mergedConfig = lib.recursiveUpdate baseConfig overrides;
|
|
in
|
|
{
|
|
name = hostName;
|
|
value = mkHost {
|
|
inherit hostName system hostType;
|
|
configOverrides = mergedConfig;
|
|
};
|
|
}
|
|
) (lib.range 1 countToUse)
|
|
)
|
|
)
|
|
else
|
|
{ };
|
|
in
|
|
lib.recursiveUpdate deviceHosts countHosts
|
|
) hosts;
|
|
|
|
# Flatten the nested structure
|
|
allHosts = lib.foldl' lib.recursiveUpdate { } (lib.attrValues processInventory);
|
|
in
|
|
{
|
|
nixosConfigurations = lib.mapAttrs (n: v: v.system) allHosts;
|
|
modules = lib.mapAttrs (n: v: v.modules) allHosts;
|
|
}
|