feat(topology): add auto-aggregation and dual-definition of options

This commit is contained in:
oddlama 2024-03-15 17:17:03 +01:00
parent 67063adf24
commit a737071162
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
13 changed files with 223 additions and 132 deletions

View file

@ -4,9 +4,9 @@ inputs: {
}: }:
inputs.nixpkgs.lib.evalModules { inputs.nixpkgs.lib.evalModules {
prefix = ["topology"]; prefix = ["topology"];
modules = [./modules] ++ modules; modules = [./topology] ++ modules;
specialArgs = { specialArgs = {
modulesPath = builtins.toString ./modules; modulesPath = builtins.toString ./topology;
inherit pkgs; inherit pkgs;
}; };
} }

View file

@ -5,17 +5,33 @@
}: let }: let
inherit inherit
(lib) (lib)
attrNames
flip flip
filterAttrs mkAliasOptionModule
mapAttrs
mapAttrs'
mapAttrsToList
mkMerge
mkOption mkOption
nameValuePair
types types
; ;
in { in {
imports =
[
# Allow simple alias to set/get attributes of this node
(mkAliasOptionModule ["topology" "self"] ["topology" "nodes" config.topology.id])
]
++ flip map (attrNames (builtins.readDir ../options)) (x:
import ../options/${x} (
module:
module
// {
# Move options to subpath
options.topology = module.options or {};
# Set the correct filename for diagnostics
_file = ../options/${x};
# The config should only be applied on the toplevel topology module,
# not for each nixos node.
config = {};
}
));
options.topology = { options.topology = {
id = mkOption { id = mkOption {
description = '' description = ''
@ -24,134 +40,57 @@ in {
unique or don't reflect the name you use to refer to that node. unique or don't reflect the name you use to refer to that node.
''; '';
default = config.networking.hostName; default = config.networking.hostName;
# TODO ensure unique across the board
type = types.str; type = types.str;
}; };
type = mkOption {
description = "TODO";
default = "normal";
type = types.enum ["normal" "microvm" "nixos-container"];
};
guests = mkOption {
description = "TODO guests ids (topology.node.<name>.id) ensure exists";
default = [];
type = types.listOf types.str;
};
disks = mkOption {
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this disk";
default = submod.config._module.args.name;
readOnly = true;
type = types.str;
};
};
}));
};
interfaces = mkOption {
description = "TODO";
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this interface";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
mac = mkOption {
description = "The MAC address of this interface, if known.";
default = null;
type = types.nullOr types.str;
};
addresses = mkOption {
description = "The configured address(es), or a descriptive string (like DHCP).";
type = types.listOf types.str;
};
network = mkOption {
description = ''
The global name of the attached/spanned network.
If this is given, this interface can be shown in the network graph.
'';
default = null;
type = types.nullOr types.str;
};
};
}));
};
firewallRules = mkOption {
description = "TODO";
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this firewall rule";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
contents = mkOption {
description = "A human readable summary of this rule's effects";
type = types.lines;
};
};
}));
};
}; };
config.topology = mkMerge [ config.topology = {
{ # Ensure a node exists for this host
################### TODO user config! ################# nodes.${config.topology.id} = {};
id = config.node.name; };
################### END user config #################
guests = #config.topology = mkMerge [
flip mapAttrsToList (config.microvm.vms or {}) # {
(_: vmCfg: vmCfg.config.config.topology.id); # ################### TODO user config! #################
# TODO: container # id = config.node.name;
# ################### END user config #################
disks = # guests =
flip mapAttrs (config.disko.devices.disk or {}) # flip mapAttrsToList (config.microvm.vms or {})
(_: _: {}); # (_: vmCfg: vmCfg.config.config.topology.id);
# TODO: zfs pools from disko / fileSystems # # TODO: container
# TODO: microvm shares
# TODO: container shares
# TODO: OCI containers shares
interfaces = let # disks =
isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null); # flip mapAttrs (config.disko.devices.disk or {})
macsByName = mapAttrs' (flip nameValuePair) (config.networking.renameInterfacesByMac or {}); # (_: _: {});
netNameFor = netName: netDef: # # TODO: zfs pools from disko / fileSystems
netDef.matchConfig.Name # # TODO: microvm shares
or ( # # TODO: container shares
if netDef ? matchConfig.MACAddress && macsByName ? ${netDef.matchConfig.MACAddress} # # TODO: OCI containers shares
then macsByName.${netDef.matchConfig.MACAddress}
else lib.trace "Could not derive network name for systemd network ${netName} on host ${config.node.name}, using unit name as fallback." netName
);
netMACFor = netDef: netDef.matchConfig.MACAddress or null;
networks = filterAttrs (_: isNetwork) (config.systemd.network.networks or {});
in
flip mapAttrs' networks (netName: netDef:
nameValuePair (netNameFor netName netDef) {
mac = netMACFor netDef;
addresses =
if netDef.address != []
then netDef.address
else ["DHCP"];
});
# TODO: for each nftable zone show open ports # interfaces = let
} # isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null);
]; # macsByName = mapAttrs' (flip nameValuePair) (config.networking.renameInterfacesByMac or {});
# netNameFor = netName: netDef:
# netDef.matchConfig.Name
# or (
# if netDef ? matchConfig.MACAddress && macsByName ? ${netDef.matchConfig.MACAddress}
# then macsByName.${netDef.matchConfig.MACAddress}
# else lib.trace "Could not derive network name for systemd network ${netName} on host ${config.node.name}, using unit name as fallback." netName
# );
# netMACFor = netDef: netDef.matchConfig.MACAddress or null;
# networks = filterAttrs (_: isNetwork) (config.systemd.network.networks or {});
# in
# flip mapAttrs' networks (netName: netDef:
# nameValuePair (netNameFor netName netDef) {
# mac = netMACFor netDef;
# addresses =
# if netDef.address != []
# then netDef.address
# else ["DHCP"];
# });
# # TODO: for each nftable zone show open ports
# }
#];
} }

View file

@ -0,0 +1,6 @@
f: {
lib,
config,
...
}: f {
}

View file

@ -0,0 +1,6 @@
f: {
lib,
config,
...
}: f {
}

112
topology/options/nodes.nix Normal file
View file

@ -0,0 +1,112 @@
f: {
lib,
config,
...
}: let
inherit
(lib)
mkOption
types
;
in f {
options.nodes = mkOption {
default = {};
description = ''
'';
type = types.attrsOf (types.submodule (nodeSubmod: {
options = {
name = mkOption {
description = "The name of this node";
default = nodeSubmod.config._module.args.name;
readOnly = true;
type = types.str;
};
type = mkOption {
description = "TODO";
default = "normal";
type = types.enum ["normal" "microvm" "nixos-container"];
};
parent = mkOption {
description = "TODO guests ids (topology.node.<name>.id) ensure exists";
default = [];
type = types.listOf types.str;
};
disks = mkOption {
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this disk";
default = submod.config._module.args.name;
readOnly = true;
type = types.str;
};
};
}));
};
interfaces = mkOption {
description = "TODO";
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this interface";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
mac = mkOption {
description = "The MAC address of this interface, if known.";
default = null;
type = types.nullOr types.str;
};
addresses = mkOption {
description = "The configured address(es), or a descriptive string (like DHCP).";
type = types.listOf types.str;
};
network = mkOption {
description = ''
The global name of the attached/spanned network.
If this is given, this interface can be shown in the network graph.
'';
default = null;
type = types.nullOr types.str;
};
};
}));
};
firewallRules = mkOption {
description = "TODO";
default = {};
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this firewall rule";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
contents = mkOption {
description = "A human readable summary of this rule's effects";
type = types.lines;
};
};
}));
};
};
}));
};
config = {
# TODO: assertions = []
};
}

View file

@ -6,17 +6,32 @@
inherit inherit
(lib) (lib)
attrNames attrNames
concatLists
filterAttrs filterAttrs
flip
getAttrFromPath
literalExpression literalExpression
mapAttrsToList
mkDefault mkDefault
mkIf mkIf
mkMerge
mkOption mkOption
types types
; ;
availableRenderers = attrNames (filterAttrs (_: v: v == "directory") (builtins.readDir ./renderers)); availableRenderers = attrNames (filterAttrs (_: v: v == "directory") (builtins.readDir ./renderers));
in { in {
imports = map (x: ./renderers/${x}) (attrNames (builtins.readDir ./renderers)); imports =
map (x: ./renderers/${x}) (attrNames (builtins.readDir ./renderers))
++ flip map (attrNames (builtins.readDir ../options)) (x:
import ../options/${x} (
module:
module
// {
# Set the correct filename for diagnostics
_file = ../options/${x};
}
));
options = { options = {
nixosConfigurations = mkOption { nixosConfigurations = mkOption {
@ -41,9 +56,22 @@ in {
}; };
}; };
config = { config = let
# Aggregates all values for an option from each host
aggregate = optionPath:
mkMerge (
concatLists (flip mapAttrsToList config.nixosConfigurations (
name: cfg:
builtins.addErrorContext "while aggregating topology definitions from self.nixosConfigurations.${name} into toplevel topology:" (
getAttrFromPath (["options" "topology"] ++ optionPath ++ ["definitions"]) cfg
)
))
);
in {
output = output =
mkIf (config.renderer != null) mkIf (config.renderer != null)
(mkDefault config.renderers.${config.renderer}.output); (mkDefault config.renderers.${config.renderer}.output);
nodes = aggregate ["nodes"];
}; };
} }