From 93b08971cf817f8406b95d99422df864cc74a339 Mon Sep 17 00:00:00 2001 From: Patrick Date: Thu, 6 Feb 2025 21:14:42 +0100 Subject: [PATCH] feat: upstream node generation --- flake-modules/default.nix | 6 ++ flake-modules/globals.nix | 74 +++++++++++++++++ flake-modules/nodes.nix | 110 +++++++++++++++++++++++++ flake.nix | 4 + modules/default.nix | 4 +- modules/globals.nix | 9 ++ modules/guests/common-guest-config.nix | 38 +++++---- modules/guests/container.nix | 36 ++++---- modules/guests/microvm.nix | 35 +++++--- 9 files changed, 268 insertions(+), 48 deletions(-) create mode 100644 flake-modules/default.nix create mode 100644 flake-modules/globals.nix create mode 100644 flake-modules/nodes.nix create mode 100644 modules/globals.nix diff --git a/flake-modules/default.nix b/flake-modules/default.nix new file mode 100644 index 0000000..ec8a065 --- /dev/null +++ b/flake-modules/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./globals.nix + ./nodes.nix + ]; +} diff --git a/flake-modules/globals.nix b/flake-modules/globals.nix new file mode 100644 index 0000000..7575ad5 --- /dev/null +++ b/flake-modules/globals.nix @@ -0,0 +1,74 @@ +{ + inputs, + lib, + config, + ... +}: +let + inherit (lib) mkOption types; +in +{ + options.globals = { + optModules = mkOption { + type = types.listOf types.deferredModule; + default = [ ]; + description = '' + Modules defining global options. + These should not include any config only option declaration. + Will be included in the exported nixos Modules from this flake to be included + into the host evaluation. + ''; + }; + defModules = mkOption { + type = types.listOf types.deferredModule; + default = [ ]; + description = '' + Modules configuring global options. + These should not include any option declaration use {option}`optModules` for that. + Will not included in the exported nixos Modules. + ''; + }; + attrkeys = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + The toplevel attrNames for your globals. + Make sure the keys of this attrset are trivially evaluatable to avoid infinite recursion, + therefore we inherit relevant attributes from the config. + ''; + }; + }; + config.flake = flakeSubmod: { + globals = + let + globalsSystem = lib.evalModules { + prefix = [ "globals" ]; + specialArgs = { + inherit (inputs.self.pkgs.x86_64-linux) lib; + inherit inputs; + inherit (flakeSubmod.config) nodes; + }; + modules = + config.globals.optModules + ++ config.globals.defModules + ++ [ + ../modules/globals.nix + ( + { lib, ... }: + { + globals = lib.mkMerge ( + lib.concatLists ( + lib.flip lib.mapAttrsToList flakeSubmod.config.nodes ( + name: cfg: + builtins.addErrorContext "while aggregating globals from nixosConfigurations.${name} into flake-level globals:" cfg.config._globalsDefs + ) + ) + ); + } + ) + ]; + }; + in + lib.genAttrs config.globals.attrkeys (x: globalsSystem.config.globals.${x}); + }; +} diff --git a/flake-modules/nodes.nix b/flake-modules/nodes.nix new file mode 100644 index 0000000..c0695b1 --- /dev/null +++ b/flake-modules/nodes.nix @@ -0,0 +1,110 @@ +{ + inputs, + self, + lib, + config, + ... +}: +let + inherit (lib) mkOption types; + topConfig = config; +in +{ + options.node = { + path = mkOption { + type = types.path; + description = "The path containing your host definitions"; + }; + nixpkgs = mkOption { + type = types.path; + default = inputs.nixpkgs; + description = "The path to your nixpkgs."; + }; + }; + config.flake = + { + config, + lib, + ... + }: + let + inherit (lib) + concatMapAttrs + filterAttrs + flip + genAttrs + mapAttrs' + nameValuePair + ; + + # Creates a new nixosSystem with the correct specialArgs, pkgs and name definition + mkHost = + { minimal }: + name: + let + pkgs = config.pkgs.x86_64-linux; + in + (import "${topConfig.node.nixpkgs}/nixos/lib/eval-config.nix") { + system = null; + specialArgs = { + # Use the correct instance lib that has our overlays + inherit (pkgs) lib; + inherit (config) nodes globals; + inherit minimal; + extraModules = [ + ../modules + ] ++ topConfig.globals.optModules; + inputs = inputs // { + inherit (topConfig.node) nixpkgs; + }; + }; + modules = [ + ( + { config, ... }: + { + node.name = name; + node.secretsDir = topConfig.node.path + "/${name}/secrets"; + nixpkgs.pkgs = self.pkgs.${config.nixpkgs.hostPlatform.system}; + } + ) + (topConfig.node.path + "/${name}") + ../modules + ] ++ topConfig.globals.optModules; + }; + + # Load the list of hosts that this flake defines, which + # associates the minimum amount of metadata that is necessary + # to instanciate hosts correctly. + hosts = builtins.attrNames ( + filterAttrs (_: type: type == "directory") (builtins.readDir topConfig.node.path) + ); + in + # Process each nixosHosts declaration and generatea nixosSystem definitions + { + nixosConfigurations = genAttrs hosts (mkHost { + minimal = false; + }); + minimalConfigurations = genAttrs hosts (mkHost { + minimal = true; + }); + + # True NixOS nodes can define additional guest nodes that are built + # together with it. We collect all defined guests from each node here + # to allow accessing any node via the unified attribute `nodes`. + guestConfigurations = flip concatMapAttrs config.nixosConfigurations ( + _: node: + flip mapAttrs' (node.config.guests or { }) ( + guestName: guestDef: + nameValuePair guestDef.nodeName ( + if guestDef.backend == "microvm" then + node.config.microvm.vms.${guestName}.config + else + node.config.containers.${guestName}.nixosConfiguration + ) + ) + ); + # All nixosSystem instanciations are collected here, so that we can refer + # to any system via nodes. + nodes = config.nixosConfigurations // config.guestConfigurations; + }; +} diff --git a/flake.nix b/flake.nix index 378a562..744f523 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,10 @@ ]; flake.modules = { + flake = { + nixos-extra-modules = import ./flake-modules; + default = self.modules.flake.nixos-extra-modules; + }; nixos = { nixos-extra-modules = import ./modules; default = self.modules.nixos.nixos-extra-modules; diff --git a/modules/default.nix b/modules/default.nix index 92eef87..29441ef 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -1,8 +1,10 @@ -{inputs, ...}: { +{ inputs, ... }: +{ imports = [ inputs.microvm.nixosModules.host ./boot.nix + ./globals.nix ./guests/default.nix ./interface-naming.nix ./nginx.nix diff --git a/modules/globals.nix b/modules/globals.nix new file mode 100644 index 0000000..073b3e7 --- /dev/null +++ b/modules/globals.nix @@ -0,0 +1,9 @@ +{ lib, options, ... }: +{ + options._globalsDefs = lib.mkOption { + type = lib.types.unspecified; + default = options.globals.definitions; + readOnly = true; + internal = true; + }; +} diff --git a/modules/guests/common-guest-config.nix b/modules/guests/common-guest-config.nix index a88e6bf..5a62851 100644 --- a/modules/guests/common-guest-config.nix +++ b/modules/guests/common-guest-config.nix @@ -1,12 +1,14 @@ -_guestName: guestCfg: {lib, ...}: let - inherit - (lib) +_guestName: guestCfg: +{ lib, ... }: +let + inherit (lib) mkForce nameValuePair listToAttrs flip ; -in { +in +{ node.name = guestCfg.nodeName; node.type = guestCfg.backend; @@ -20,20 +22,20 @@ in { systemd.network.networks = listToAttrs ( flip map guestCfg.networking.links ( name: - nameValuePair "10-${name}" { - matchConfig.Name = name; - DHCP = "yes"; - # XXX: Do we really want this? - dhcpV4Config.UseDNS = false; - dhcpV6Config.UseDNS = false; - ipv6AcceptRAConfig.UseDNS = false; - networkConfig = { - IPv6PrivacyExtensions = "yes"; - MulticastDNS = true; - IPv6AcceptRA = true; - }; - linkConfig.RequiredForOnline = "routable"; - } + nameValuePair "10-${name}" { + matchConfig.Name = name; + DHCP = "yes"; + # XXX: Do we really want this? + dhcpV4Config.UseDNS = false; + dhcpV6Config.UseDNS = false; + ipv6AcceptRAConfig.UseDNS = false; + networkConfig = { + IPv6PrivacyExtensions = "yes"; + MulticastDNS = true; + IPv6AcceptRA = true; + }; + linkConfig.RequiredForOnline = "routable"; + } ) ); } diff --git a/modules/guests/container.nix b/modules/guests/container.nix index cee541b..4cb0610 100644 --- a/modules/guests/container.nix +++ b/modules/guests/container.nix @@ -1,17 +1,20 @@ -guestName: guestCfg: { +guestName: guestCfg: +{ config, inputs, lib, pkgs, + extraModules, ... -}: let - inherit - (lib) +}: +let + inherit (lib) flip mapAttrs' nameValuePair ; -in { +in +{ inherit (guestCfg.container) macvlans; ephemeral = true; privateNetwork = true; @@ -21,10 +24,10 @@ in { ]; bindMounts = flip mapAttrs' guestCfg.zfs ( _: zfsCfg: - nameValuePair zfsCfg.guestMountpoint { - hostPath = zfsCfg.hostMountpoint; - isReadOnly = false; - } + nameValuePair zfsCfg.guestMountpoint { + hostPath = zfsCfg.hostMountpoint; + isReadOnly = false; + } ); nixosConfiguration = (import "${inputs.nixpkgs}/nixos/lib/eval-config.nix") { specialArgs = guestCfg.extraSpecialArgs; @@ -55,16 +58,17 @@ in { # to the state fs). fileSystems = flip mapAttrs' guestCfg.zfs ( _: zfsCfg: - nameValuePair zfsCfg.guestMountpoint { - neededForBoot = true; - fsType = "none"; - device = zfsCfg.guestMountpoint; - options = ["bind"]; - } + nameValuePair zfsCfg.guestMountpoint { + neededForBoot = true; + fsType = "none"; + device = zfsCfg.guestMountpoint; + options = [ "bind" ]; + } ); } (import ./common-guest-config.nix guestName guestCfg) ] - ++ guestCfg.modules; + ++ guestCfg.modules + ++ extraModules; }; } diff --git a/modules/guests/microvm.nix b/modules/guests/microvm.nix index e1b74b8..4c3093b 100644 --- a/modules/guests/microvm.nix +++ b/modules/guests/microvm.nix @@ -1,10 +1,12 @@ -guestName: guestCfg: { +guestName: guestCfg: +{ inputs, lib, + extraModules, ... -}: let - inherit - (lib) +}: +let + inherit (lib) concatMapAttrs flip mapAttrs @@ -13,19 +15,22 @@ guestName: guestCfg: { mkForce replaceStrings ; -in { +in +{ specialArgs = guestCfg.extraSpecialArgs; pkgs = inputs.self.pkgs.${guestCfg.microvm.system}; inherit (guestCfg) autostart; config = { imports = - guestCfg.modules + extraModules + ++ guestCfg.modules ++ [ (import ./common-guest-config.nix guestName guestCfg) ( - {config, ...}: { + { config, ... }: + { # Set early hostname too, so we can associate those logs to this host and don't get "localhost" entries in loki - boot.kernelParams = ["systemd.hostname=${config.networking.hostName}"]; + boot.kernelParams = [ "systemd.hostname=${config.networking.hostName}" ]; } ) ]; @@ -47,13 +52,15 @@ in { # MACVTAP bridge to the host's network interfaces = flip mapAttrsToList guestCfg.microvm.interfaces ( - _: { + _: + { mac, hostLink, ... - }: { + }: + { type = "macvtap"; - id = "vm-${replaceStrings [":"] [""] mac}"; + id = "vm-${replaceStrings [ ":" ] [ "" ] mac}"; inherit mac; macvtap = { link = hostLink; @@ -82,9 +89,11 @@ in { ); }; - networking.renameInterfacesByMac = flip mapAttrs guestCfg.microvm.interfaces (_: {mac, ...}: mac); + networking.renameInterfacesByMac = flip mapAttrs guestCfg.microvm.interfaces (_: { mac, ... }: mac); systemd.network.networks = flip concatMapAttrs guestCfg.microvm.interfaces ( - name: {mac, ...}: { + name: + { mac, ... }: + { "10-${name}".matchConfig = mkForce { MACAddress = mac; };