metanix/lib/addressing/default.nix
2025-11-28 16:12:13 -06:00

432 lines
11 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{ lib ? import <nixpkgs/lib> { } }:
let
inherit (lib)
assertMsg
attrNames
concatMapAttrs
concatStringsSep
listToAttrs
nameValuePair
optionalAttrs
imap1
mapAttrs'
imap0
length
filter
findFirst
toLower;#
mkInitials =
name:
let
lower = toLower name; # was builtins.toLower
noApos = builtins.replaceStrings [ "'" ] [ " " ] lower;
words = filter (w: w != "") (builtins.split " +" noApos);
letters =
concatStringsSep ""
(builtins.map (w: builtins.substring 0 1 w) words);
in
if letters == "" then "loc" else builtins.substring 0 3 letters;
mkLocationCode =
name:
let
base = mkInitials name;
hash = builtins.substring 0 4 (builtins.hashString "sha256" name);
in
"${base}-${hash}";
# Manual elemIndex so we don't care what nixpkgs version you're on
elemIndex =
name: list:
let
idxs = imap0 (i: v: if v == name then i else null) list;
in
findFirst (i: i != null) null idxs;
subnetTypes = {
dmz = 0;
main = 1;
guest = 2;
iot = 3;
storage = 4;
management = 5;
infra = 6;
lab = 7;
};
roles = {
coreServer = 0;
router = 1;
infraDevice = 2;
server = 3;
workstation = 4;
thinClient = 5;
printer = 6;
nas = 7;
phone = 8;
camera = 9;
mobile = 10;
guestClient = 11;
appliance = 12;
automation = 13;
oobMgmt = 14;
hypervisor = 15;
containerHost = 16;
monitoring = 17;
logging = 18;
identity = 19;
labDevice = 20;
adminWorkstation = 21;
repoMirror = 22;
ci = 23;
media = 24;
homeAutomation = 25;
pool = 31;
};
subnetSlugs = {
dmz = "dmz";
main = "main";
guest = "guest";
iot = "iot";
storage = "storage";
management = "mgmt";
infra = "infra";
lab = "lab";
};
roleSlugs = {
coreServer = "core";
router = "router";
infraDevice = "infra-dev";
server = "server";
workstation = "ws";
thinClient = "thin";
printer = "printer";
nas = "nas";
phone = "phone";
camera = "camera";
mobile = "mobile";
guestClient = "guest";
appliance = "appliance";
automation = "automation";
oobMgmt = "oobm";
hypervisor = "hypervisor";
containerHost = "container";
monitoring = "monitoring";
logging = "logging";
identity = "identity";
labDevice = "lab-dev";
adminWorkstation = "admin-ws";
repoMirror = "mirror";
ci = "ci";
media = "media";
homeAutomation = "home-auto";
pool = "dhcp-pool";
};
thirdOctet =
{ type, role }:
let
_ = assertMsg (type >= 0 && type <= 7)
"metatron-addressing: type must be in 0..7, got ${toString type}";
__ = assertMsg (role >= 0 && role <= 31)
"metatron-addressing: role must be in 0..31, got ${toString role}";
in
type * 32 + role;
decodeThirdOctet =
octet:
let
_ = assertMsg (octet >= 0 && octet <= 255)
"metatron-addressing: octet must be in 0..255, got ${toString octet}";
type = builtins.div octet 32;
role = octet - (type * 32);
in
{ inherit type role; };
mkIp =
{ location, type, role, host }:
let
_ = assertMsg (location >= 0 && location <= 255)
"metatron-addressing: location must be in 0..255, got ${toString location}";
__ = assertMsg (host >= 0 && host <= 255)
"metatron-addressing: host must be in 0..255, got ${toString host}";
oct3 = thirdOctet { inherit type role; };
in
"10.${toString location}.${toString oct3}.${toString host}";
mkRoleRange =
{ location, type, role }:
let
_ = assertMsg (location >= 0 && location <= 255)
"metatron-addressing: location must be in 0..255, got ${toString location}";
__ = assertMsg (type >= 0 && type <= 7)
"metatron-addressing: type must be in 0..7, got ${toString type}";
___ = assertMsg (role >= 0 && role <= 31)
"metatron-addressing: role must be in 0..31, got ${toString role}";
oct3 = thirdOctet { inherit type role; };
base = "10.${toString location}.${toString oct3}";
in
{
start = "${base}.0";
end = "${base}.255";
};
mkRoleRangeNamed =
{ location, typeName, roleName }:
let
_ = assertMsg (subnetTypes ? typeName)
"metatron-addressing: unknown subnet type '${typeName}'";
__ = assertMsg (roles ? roleName)
"metatron-addressing: unknown role '${roleName}'";
type = subnetTypes.${typeName};
role = roles.${roleName};
in
mkRoleRange { inherit location type role; };
mkIpNamed =
{ location, typeName, roleName, host }:
let
_ = assertMsg (subnetTypes ? typeName)
"metatron-addressing: unknown subnet type '${typeName}'";
__ = assertMsg (roles ? roleName)
"metatron-addressing: unknown role '${roleName}'";
type = subnetTypes.${typeName};
role = roles.${roleName};
in
mkIp { inherit location host type role; };
mkSubnet =
{ location, type }:
let
_ = assertMsg (location >= 0 && location <= 255)
"metatron-addressing: location must be in 0..255, got ${toString location}";
__ = assertMsg (type >= 0 && type <= 7)
"metatron-addressing: type must be in 0..7, got ${toString type}";
oct3 = type * 32;
in
"10.${toString location}.${toString oct3}.0/19";
mkSubnetNamed =
{ location, typeName }:
let
_ = assertMsg (subnetTypes ? typeName)
"metatron-addressing: unknown subnet type '${typeName}'";
type = subnetTypes.${typeName};
in
mkSubnet { inherit location type; };
mkVlan =
{ location, type }:
let
_ = assertMsg (location >= 0 && location <= 255)
"metatron-addressing: location must be in 0..255, got ${toString location}";
__ = assertMsg (type >= 0 && type <= 7)
"metatron-addressing: type must be in 0..7, got ${toString type}";
in
(location * 8) + type + 2;
mkVlanNamed =
{ location, typeName }:
let
_ = assertMsg (subnetTypes ? typeName)
"metatron-addressing: unknown subnet type '${typeName}'";
type = subnetTypes.${typeName};
in
mkVlan { inherit location type; };
# locationName → numeric id (0, 1, 2, …)
locationIdForSpec =
spec:
let
locations =
spec.locations or(throw "metatron-addressing: spec.locations is required");
locationNames = attrNames locations;
in
name:
let
idx = elemIndex name locationNames;
in
if idx == null then
throw "metatron-addressing: unknown location '${name}'"
else
idx;
# Just the host map: { deimos = { ip-address = "..."; fqdn = "..."; ... }; ... }
mkHostsFromSpec =
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 for FQDN generation");
in
concatMapAttrs
(locationName: locCfg:
let
loc = locationId locationName;
locCode = mkLocationCode locationName;
# Strip meta-level hints; whats left are subnets (main, dmz, etc.)
subnets =
builtins.removeAttrs locCfg [ "owner" "admins" "users" ];
in
concatMapAttrs
(typeName: subnetCfg:
let
# New shape: subnetCfg.hosts = { hostName = { role = "..."; ... }; ... }
hostsForSubnet = subnetCfg.hosts or { };
# Only treat non-underscore-prefixed keys as hosts
hostNames =
filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet);
in
listToAttrs (imap1
(idx: hostName:
let
hostCfg = hostsForSubnet.${hostName};
roleName = hostCfg.role;
# 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 = hostCfg.role;
host = hostId;
};
subnetSlug =
if builtins.hasAttr typeName subnetSlugs
then subnetSlugs.${typeName}
else typeName;
roleSlug =
if builtins.hasAttr roleName roleSlugs
then roleSlugs.${roleName}
else roleName;
hostnameBase = "${roleSlug}-${builtins.toString hostId}";
hostname = hostCfg.hostname or hostnameBase;
fqdn = "${hostname}.${subnetSlug}.${locCode}.${domain}";
in
nameValuePair hostName (
{
ip-address = ip;
inherit hostname fqdn;
}
// optionalAttrs (hostCfg ? hw-address) {
inherit (hostCfg) hw-address;
}
// optionalAttrs (hostCfg ? dns) {
inherit (hostCfg) dns;
}
// optionalAttrs (hostCfg ? aliases) {
inherit (hostCfg) aliases;
}
))
hostNames))
subnets)
locations;
# Subnet map: { "home-dmz" = { cidr = "..."; vlan = ...; zone = "..."; ... }; ... }
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: locCfg:
let
loc = locationId locationName;
locCode = mkLocationCode locationName;
# Strip meta-level hints; remaining attrs are subnets
subnets =
builtins.removeAttrs locCfg [ "owner" "admins" "users" ];
in
mapAttrs'
(typeName: subnetCfg:
let
hostsForSubnet = subnetCfg.hosts or { };
cidr = mkSubnetNamed { location = loc; inherit typeName; };
subnetSlug =
if builtins.hasAttr typeName subnetSlugs
then subnetSlugs.${typeName}
else typeName;
zone = "${subnetSlug}.${locCode}.${domain}";
vlan =
# Priority: explicit vlan on subnet > old-style _vlan > computed
subnetCfg.vlan or
hostsForSubnet._vlan or
(mkVlanNamed { location = loc; inherit typeName; });
in
nameValuePair
"${locationName}-${typeName}"
{
inherit cidr locationName typeName zone vlan;
})
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;
};
in
{
inherit
subnetTypes
roles
thirdOctet
decodeThirdOctet
mkIp
mkIpNamed
mkSubnet
mkSubnetNamed
mkHostsFromSpec
mkSubnetsFromSpec
mkNetworkFromSpec
mkRoleRange
mkRoleRangeNamed;
# Shorthand; you were calling mkHosts before
mkHosts = mkNetworkFromSpec;
}