Better addressing?

This commit is contained in:
Yaro Kasear 2026-05-01 13:07:30 -05:00
parent b5c1a43863
commit f35e402b4d
3 changed files with 394 additions and 261 deletions

View file

@ -10,67 +10,48 @@
outputs = { self, nixpkgs, deploy-rs, disko, nixos-anywhere, ... }:
let
# Default architecture if a system doesnt override it
defaultSystem = "x86_64-linux";
# pkgs per-system so cross-arch doesnt explode later
pkgsFor = system: import nixpkgs { inherit system; };
lib = nixpkgs.lib;
# World spec & addressing
meta = import ./meta.nix;
addressing = import ./lib/addressing { inherit lib; };
# Optional for now; meta.systems can be {} until the systems submodule lands
systemsFromMeta = meta.systems or { };
# Precomputed full network view
network = addressing.mkNetworkFromSpec meta;
in
{
########################
# Library exports
########################
lib.metanix = {
inherit meta addressing network;
};
########################
# NixOS configurations
########################
nixosConfigurations =
lib.mapAttrs
(name: sysCfg:
let
# System for this host, fallback to default
system = sysCfg.system or defaultSystem;
pkgs = pkgsFor system;
in
lib.nixosSystem {
inherit system;
# Make meta/addressing/world visible to modules
specialArgs = {
inherit lib pkgs meta addressing system;
modulesPath = builtins.toString <nixpkgs/nixos/modules>;
inherit lib meta addressing system network;
modulesPath = "${nixpkgs}/nixos/modules";
};
modules =
(sysCfg.modules or [ ]) ++ [
# Core Metanix wiring
./modules/metanix/core.nix
./modules/metanix/networkd.nix
# Identity binding: “this box is <name> in Metanix”
{
networking.hostName = sysCfg.hostName or name;
metanix.thisHost = sysCfg.metanixName or name;
}
# Optional Disko integration per host
(if sysCfg ? diskoConfig then
{
imports = [
@ -84,10 +65,6 @@
})
systemsFromMeta;
########################
# deploy-rs
########################
deploy = {
nodes =
lib.mapAttrs
@ -97,7 +74,10 @@
hostInfo = if hasNetworkHost then network.hosts.${name} else null;
defaultHostname =
if hasNetworkHost then hostInfo.fqdn else "${name}.${meta.domain}";
if hasNetworkHost && hostInfo ? fqdn then
hostInfo.fqdn
else
name;
in
{
hostname = sysCfg.deployHost or defaultHostname;
@ -112,12 +92,10 @@
systemsFromMeta;
};
checks.${defaultSystem}.deploy =
deploy-rs.lib.${defaultSystem}.deployChecks self.deploy;
########################
# nixos-anywhere helper
########################
checks =
builtins.mapAttrs
(system: deployLib: deployLib.deployChecks self.deploy)
deploy-rs.lib;
apps.${defaultSystem}.nixos-anywhere = {
type = "app";

View file

@ -1,4 +1,4 @@
{ lib ? import <nixpkgs/lib> { } }:
{ lib, ... }:
let
inherit (lib)
@ -6,27 +6,26 @@ let
attrNames
concatMapAttrs
concatStringsSep
filterAttrs
listToAttrs
mapAttrs'
nameValuePair
optionalAttrs
imap1
mapAttrs'
imap0
length
filter
findFirst
toLower;#
toLower
unique;
mkInitials =
name:
let
lower = toLower name; # was builtins.toLower
lower = toLower name;
noApos = builtins.replaceStrings [ "'" ] [ " " ] lower;
words = filter (w: w != "") (builtins.split " +" noApos);
letters =
concatStringsSep ""
(builtins.map (w: builtins.substring 0 1 w) words);
letters = concatStringsSep "" (builtins.map (w: builtins.substring 0 1 w) words);
in
if letters == "" then "loc" else builtins.substring 0 3 letters;
@ -38,7 +37,7 @@ let
in
"${base}-${hash}";
# Manual elemIndex so we don't care what nixpkgs version you're on
# Manual elemIndex so we do not care what nixpkgs version you are on.
elemIndex =
name: list:
let
@ -180,9 +179,9 @@ let
mkRoleRangeNamed =
{ location, typeName, roleName }:
let
_ = assertMsg (subnetTypes ? typeName)
_ = assertMsg (builtins.hasAttr typeName subnetTypes)
"metatron-addressing: unknown subnet type '${typeName}'";
__ = assertMsg (roles ? roleName)
__ = assertMsg (builtins.hasAttr roleName roles)
"metatron-addressing: unknown role '${roleName}'";
type = subnetTypes.${typeName};
@ -193,9 +192,9 @@ let
mkIpNamed =
{ location, typeName, roleName, host }:
let
_ = assertMsg (subnetTypes ? typeName)
_ = assertMsg (builtins.hasAttr typeName subnetTypes)
"metatron-addressing: unknown subnet type '${typeName}'";
__ = assertMsg (roles ? roleName)
__ = assertMsg (builtins.hasAttr roleName roles)
"metatron-addressing: unknown role '${roleName}'";
type = subnetTypes.${typeName};
@ -217,7 +216,7 @@ let
mkSubnetNamed =
{ location, typeName }:
let
_ = assertMsg (subnetTypes ? typeName)
_ = assertMsg (builtins.hasAttr typeName subnetTypes)
"metatron-addressing: unknown subnet type '${typeName}'";
type = subnetTypes.${typeName};
in
@ -236,18 +235,59 @@ let
mkVlanNamed =
{ location, typeName }:
let
_ = assertMsg (subnetTypes ? typeName)
_ = assertMsg (builtins.hasAttr typeName subnetTypes)
"metatron-addressing: unknown subnet type '${typeName}'";
type = subnetTypes.${typeName};
in
mkVlan { inherit location type; };
# locationName → numeric id (0, 1, 2, …)
asList = value:
if value == null then [ ]
else if builtins.isList value then value
else [ value ];
scopeFromCfg = cfg: {
owners =
asList (cfg.owners or null)
++ asList (cfg.owner or null);
admins =
asList (cfg.admins or null)
++ asList (cfg.admin or null);
users = asList (cfg.users or null);
domain = cfg.domain or null;
};
mergeScopes = parent: child:
let
c = scopeFromCfg child;
in
{
owners = unique (parent.owners ++ c.owners);
admins = unique (parent.admins ++ c.admins);
users = unique (parent.users ++ c.users);
domain = if c.domain != null then c.domain else parent.domain;
};
emptyScope = {
owners = [ ];
admins = [ ];
users = [ ];
domain = null;
};
materializeScope = scope:
{
inherit (scope) owners admins users;
}
// optionalAttrs (scope.domain != null) {
inherit (scope) domain;
};
# locationName -> numeric id (0, 1, 2, ...)
locationIdForSpec =
spec:
let
locations =
spec.locations or(throw "metatron-addressing: spec.locations is required");
locations = spec.locations or (throw "metatron-addressing: spec.locations is required");
locationNames = attrNames locations;
in
name:
@ -259,183 +299,197 @@ let
else
idx;
# Just the host map: { deimos = { ip-address = "..."; fqdn = "..."; ... }; ... }
hostRole = path: hostCfg:
hostCfg.role or (throw "metatron-addressing: host '${path}' is missing required field 'role' for address generation");
hostMapForSubnet =
{ loc, locCode, locationName, networkName, typeName, subnetCfg, scope }:
let
hostsForSubnet = subnetCfg.hosts or { };
hostNames = filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet);
subnetSlug =
if builtins.hasAttr typeName subnetSlugs
then subnetSlugs.${typeName}
else typeName;
in
listToAttrs (imap1
(idx: hostName:
let
path = "${locationName}.${networkName}.${typeName}.${hostName}";
hostCfg = hostsForSubnet.${hostName};
hostScope = mergeScopes scope hostCfg;
roleName = hostRole path hostCfg;
# Count how many hosts of this role exist up to and including this one.
roleIndex =
let
prefix = lib.lists.sublist 0 idx hostNames;
in
length (filter
(hn: (hostsForSubnet.${hn}.role or null) == roleName)
prefix);
hostId = hostCfg.hostId or roleIndex;
ip = mkIpNamed {
location = loc;
inherit typeName;
roleName = roleName;
host = hostId;
};
roleSlug =
if builtins.hasAttr roleName roleSlugs
then roleSlugs.${roleName}
else roleName;
hostnameBase = "${roleSlug}-${builtins.toString hostId}";
hostname = hostCfg.hostname or hostnameBase;
in
nameValuePair hostName (
{
ip-address = ip;
inherit hostname;
location = locationName;
network = networkName;
subnetType = typeName;
subnetKey = "${locationName}-${networkName}-${typeName}";
}
// optionalAttrs (hostScope.domain != null) {
fqdn = "${hostname}.${subnetSlug}.${locCode}.${hostScope.domain}";
}
// materializeScope hostScope
// optionalAttrs (hostCfg ? hw-address) {
inherit (hostCfg) hw-address;
}
// optionalAttrs (hostCfg ? dns) {
inherit (hostCfg) dns;
}
// optionalAttrs (hostCfg ? aliases) {
inherit (hostCfg) aliases;
}
// optionalAttrs (hostCfg ? interface) {
inherit (hostCfg) interface;
}
// optionalAttrs (hostCfg ? tags) {
inherit (hostCfg) tags;
}
))
hostNames);
mkHostsFromSpec =
spec:
let
locations =
spec.locations or (throw "metatron-addressing: spec.locations is required");
locations = spec.locations or (throw "metatron-addressing: spec.locations is required");
locationId = locationIdForSpec spec;
domain =
spec.domain or (throw "metatron-addressing: spec.domain is required for FQDN generation");
in
concatMapAttrs
(locationName: locationCfg:
let
loc = locationId locationName;
locCode = mkLocationCode locationName;
# Only treat attributes that are attrsets with a `hosts` attr as subnets
subnets =
lib.filterAttrs (_: v: builtins.isAttrs v && v ? hosts) locationCfg;
locationScope = mergeScopes emptyScope locationCfg;
networks = locationCfg.networks or { };
in
concatMapAttrs
(typeName: subnetCfg:
(networkName: networkCfg:
let
hostsForSubnet = subnetCfg.hosts or { };
# Only treat non-underscore-prefixed keys as hosts
hostNames =
filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet);
networkScope = mergeScopes locationScope networkCfg;
subnets = networkCfg.subnets or { };
in
listToAttrs (imap1
(idx: hostName:
concatMapAttrs
(typeName: subnetCfg:
let
hostCfg = hostsForSubnet.${hostName};
roleName = hostCfg.role;
subnetScope = mergeScopes networkScope subnetCfg;
in
hostMapForSubnet {
inherit loc locCode locationName networkName typeName subnetCfg;
scope = subnetScope;
})
subnets)
networks)
locations;
# Count how many hosts of this role exist up to *and including* this one
roleIndex =
let
prefix = lib.lists.sublist 0 idx hostNames;
in
length (filter
(hn: (hostsForSubnet.${hn}.role or null) == roleName)
prefix);
mkSubnetsFromSpec =
spec:
let
locations = spec.locations or (throw "metatron-addressing: spec.locations is required");
locationId = locationIdForSpec spec;
in
concatMapAttrs
(locationName: locationCfg:
let
loc = locationId locationName;
locCode = mkLocationCode locationName;
locationScope = mergeScopes emptyScope locationCfg;
networks = locationCfg.networks or { };
in
concatMapAttrs
(networkName: networkCfg:
let
networkScope = mergeScopes locationScope networkCfg;
subnets = networkCfg.subnets or { };
in
mapAttrs'
(typeName: subnetCfg:
let
subnetScope = mergeScopes networkScope subnetCfg;
hostId = hostCfg.hostId or roleIndex;
ip = mkIpNamed {
location = loc;
inherit typeName;
roleName = hostCfg.role;
host = hostId;
};
cidr = mkSubnetNamed { location = loc; inherit typeName; };
subnetSlug =
if builtins.hasAttr typeName subnetSlugs
then subnetSlugs.${typeName}
else typeName;
roleSlug =
if builtins.hasAttr roleName roleSlugs
then roleSlugs.${roleName}
else roleName;
vlan =
subnetCfg.vlan or
subnetCfg._vlan or
(mkVlanNamed { location = loc; inherit typeName; });
hostnameBase = "${roleSlug}-${builtins.toString hostId}";
hostname = hostCfg.hostname or hostnameBase;
dhcpCfg = subnetCfg.dhcp or null;
fqdn = "${hostname}.${subnetSlug}.${locCode}.${domain}";
dhcpRange =
if dhcpCfg == null then null else {
startIp = mkIpNamed {
location = loc;
inherit typeName;
roleName = "pool";
host = dhcpCfg.start;
};
endIp = mkIpNamed {
location = loc;
inherit typeName;
roleName = "pool";
host = dhcpCfg.end;
};
};
in
nameValuePair hostName (
{
ip-address = ip;
inherit hostname fqdn;
# New context fields:
location = locationName;
subnetType = typeName;
subnetKey = "${locationName}-${typeName}";
}
// optionalAttrs (hostCfg ? hw-address) {
inherit (hostCfg) hw-address;
}
// optionalAttrs (hostCfg ? dns) {
inherit (hostCfg) dns;
}
// optionalAttrs (hostCfg ? aliases) {
inherit (hostCfg) aliases;
}
// optionalAttrs (hostCfg ? interface) {
inherit (hostCfg) interface;
}
)
)
hostNames))
subnets)
nameValuePair
"${locationName}-${networkName}-${typeName}"
(
{
inherit cidr locationName networkName typeName vlan;
}
// optionalAttrs (subnetScope.domain != null) {
zone = "${subnetSlug}.${locCode}.${subnetScope.domain}";
}
// materializeScope subnetScope
// optionalAttrs (dhcpCfg != null) {
dhcp = dhcpCfg;
dhcpRange = dhcpRange;
}
))
subnets)
networks)
locations;
# Subnet map: { "home-dmz" = "10.1.0.0/19"; "home-main" = "..."; ... }
mkSubnetsFromSpec =
spec:
let
locations =
spec.locations or (throw "metatron-addressing: spec.locations is required");
locationId = locationIdForSpec spec;
domain =
spec.domain or (throw "metatron-addressing: spec.domain is required");
in
concatMapAttrs
(locationName: locationCfg:
let
loc = locationId locationName;
locCode = mkLocationCode locationName;
# Only attributes with `hosts` are subnets
subnets =
lib.filterAttrs (_: v: builtins.isAttrs v && v ? hosts) locationCfg;
in
mapAttrs'
(typeName: subnetCfg:
let
cidr = mkSubnetNamed { location = loc; inherit typeName; };
subnetSlug =
if builtins.hasAttr typeName subnetSlugs
then subnetSlugs.${typeName}
else typeName;
zone = "${subnetSlug}.${locCode}.${domain}";
vlan =
subnetCfg.vlan or
subnetCfg._vlan or
(mkVlanNamed { location = loc; inherit typeName; });
dhcpCfg = subnetCfg.dhcp or null;
dhcpRange =
if dhcpCfg == null then null else {
startIp = mkIpNamed {
location = loc;
inherit typeName;
roleName = "pool";
host = dhcpCfg.start;
};
endIp = mkIpNamed {
location = loc;
inherit typeName;
roleName = "pool";
host = dhcpCfg.end;
};
};
in
nameValuePair
"${locationName}-${typeName}"
(
{
inherit cidr locationName typeName zone vlan;
}
// optionalAttrs (dhcpCfg != null) {
dhcp = dhcpCfg;
dhcpRange = dhcpRange;
}
))
subnets)
locations;
# Combined view, if you want both
mkNetworkFromSpec =
spec:
let
domain =
spec.domain or (throw "metatron-addressing: spec.domain is required");
in
{
inherit domain;
hosts = mkHostsFromSpec spec;
subnets = mkSubnetsFromSpec spec;
};
mkNetworkFromSpec = spec: {
hosts = mkHostsFromSpec spec;
subnets = mkSubnetsFromSpec spec;
};
in
{
@ -448,12 +502,15 @@ in
mkIpNamed
mkSubnet
mkSubnetNamed
mkVlan
mkVlanNamed
mkHostsFromSpec
mkSubnetsFromSpec
mkNetworkFromSpec
mkRoleRange
mkRoleRangeNamed;
mkRoleRangeNamed
mergeScopes;
# Shorthand; you were calling mkHosts before
# Shorthand; you were calling mkHosts before.
mkHosts = mkNetworkFromSpec;
}

218
meta.nix
View file

@ -3,22 +3,23 @@
locations = {
cloud = {
owner = "yaro";
dmz = {
hosts = {
domain = "kasear.net";
networks.default.subnets = {
dmz.hosts = {
eris = {
role = "router";
aliases = [ "frontend.kasear.net" ];
aliases = [
"frontend.kasear.net"
];
};
deimos-cloud = {
role = "server";
};
};
};
infra = {
hosts = {
infra.hosts = {
metatron = {
role = "coreServer";
};
@ -26,15 +27,28 @@
loki-cloud = {
role = "adminWorkstation";
};
io-cloud = {
role = "router";
};
europa-cloud = {
role = "router";
};
vpn-container = {
role = "server";
dns = false;
};
};
};
};
home = {
dmz = {
vlan = 1;
norfolk = {
domain = "kasear.net";
hosts = {
networks.default.subnets = {
dmz.hosts = {
io = {
role = "router";
aliases = [ "external.kasear.net" ];
@ -58,7 +72,6 @@
"test.kasear.net"
"vault.kasear.net"
"vikali.kasear.net"
"vpn.kasear.net"
"www.kasear.net"
"yaro.kasear.net"
];
@ -69,27 +82,60 @@
dns = false;
};
cloud-container = { role = "server"; dns = false; };
default-container = { role = "server"; dns = false; };
foregejo-container = { role = "server"; dns = false; };
majike-container = { role = "server"; dns = false; };
media-container = { role = "server"; dns = false; };
vault-container = { role = "server"; dns = false; };
vikali-container = { role = "server"; dns = false; };
vpn-container = { role = "server"; dns = false; };
yaro-container = { role = "server"; dns = false; };
};
};
cloud-container = {
role = "server";
dns = false;
};
main = {
vlan = 10;
default-container = {
role = "server";
dns = false;
};
dhcp = {
start = 1;
end = 250;
foregejo-container = {
role = "server";
dns = false;
};
majike-container = {
role = "server";
dns = false;
};
media-container = {
role = "server";
dns = false;
};
vault-container = {
role = "server";
dns = false;
};
vikali-container = {
role = "server";
dns = false;
};
yaro-container = {
role = "server";
dns = false;
};
norfolk-dmz-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
norfolk-dmz-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
hosts = {
main.hosts = {
europa = {
role = "router";
aliases = [ "internal.kasear.net" ];
@ -106,6 +152,11 @@
hw-address = "54:af:97:02:2f:15";
};
loki = {
role = "adminWorkstation";
hw-address = "70:85:c2:f4:1a:58";
};
luna = {
role = "infraDevice";
hw-address = "30:23:03:48:4c:75";
@ -114,7 +165,6 @@
phobos = {
role = "server";
hw-address = "10:98:36:a9:4a:26";
interface = "eno2";
aliases = [
"pbx.kasear.net"
"private.kasear.net"
@ -137,28 +187,39 @@
role = "phone";
hw-address = "80:5e:c0:de:3d:66";
};
};
};
guest = {
vlan = 20;
norfolk-main-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
dhcp = {
start = 1;
end = 250;
norfolk-main-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
hosts = {
guest.hosts = {
europa-guest = {
role = "router";
};
norfolk-guest-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
norfolk-guest-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
};
iot = {
vlan = 30;
hosts = {
iot.hosts = {
europa-iot = {
role = "router";
};
@ -183,6 +244,11 @@
hw-address = "08:84:9d:74:4d:c6";
};
loki-iot = {
role = "adminWorkstation";
hw-address = "70:85:c2:f4:1a:58";
};
camera1 = {
role = "camera";
hw-address = "9c:8e:cd:38:95:1f";
@ -204,18 +270,26 @@
role = "appliance";
hw-address = "04:e4:b6:23:81:fc";
};
};
};
storage = {
vlan = 40;
mercury-iot = {
role = "mobile";
hw-address = "ac:3e:b1:77:65:2e";
};
dhcp = {
start = 1;
end = 250;
norfolk-iot-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
norfolk-iot-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
hosts = {
storage.hosts = {
europa-storage = {
role = "router";
};
@ -224,13 +298,30 @@
role = "nas";
aliases = [ "storage.kasear.net" ];
};
loki-storage = {
role = "adminWorkstation";
hw-address = "00:07:43:13:c4:90";
};
norfolk-storage-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
norfolk-storage-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
};
management = {
vlan = 70;
management.hosts = {
europa-management = {
role = "router";
};
hosts = {
deimos-idrac = {
role = "oobMgmt";
hw-address = "10:98:36:a0:2c:b3";
@ -245,13 +336,20 @@
role = "oobMgmt";
hw-address = "14:18:77:51:4b:b5";
};
norfolk-management-dhcp-start = {
role = "pool";
hostId = 1;
dns = false;
};
norfolk-management-dhcp-end = {
role = "pool";
hostId = 250;
dns = false;
};
};
};
};
};
# You can add these later if you want to match the bigger design:
# systems = { };
# consumers = { };
# policy = { };
}