Add DHCP pool support.

This commit is contained in:
Yaro Kasear 2026-05-01 13:33:44 -05:00
parent f35e402b4d
commit bf6ba269af
3 changed files with 141 additions and 39 deletions

View file

@ -83,6 +83,11 @@ let
ci = 23; ci = 23;
media = 24; media = 24;
homeAutomation = 25; homeAutomation = 25;
dhcpStart = 29;
dhcpPool = 30;
dhcpEnd = 31;
# Legacy alias. New generated DHCP ranges use dhcpStart/dhcpEnd.
pool = 31; pool = 31;
}; };
@ -124,6 +129,9 @@ let
ci = "ci"; ci = "ci";
media = "media"; media = "media";
homeAutomation = "home-auto"; homeAutomation = "home-auto";
dhcpStart = "dhcp-start";
dhcpPool = "dhcp-pool";
dhcpEnd = "dhcp-end";
pool = "dhcp-pool"; pool = "dhcp-pool";
}; };
@ -283,27 +291,44 @@ let
inherit (scope) domain; inherit (scope) domain;
}; };
# locationName -> numeric id (0, 1, 2, ...) # All networks across all locations, in deterministic attr-name order.
locationIdForSpec = # Network name, not location name, owns the second octet:
# 10.{network_id}.{subnet_type * 32 + host_role}.{host_id}
networkNamesForSpec =
spec: spec:
let 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; locationNames = attrNames locations;
networkNamesNested = builtins.map
(locationName: attrNames (locations.${locationName}.networks or { }))
locationNames;
in
unique (builtins.concatLists networkNamesNested);
# networkName -> numeric id (0, 1, 2, ...)
networkIdForSpec =
spec:
let
networkNames = networkNamesForSpec spec;
in in
name: name:
let let
idx = elemIndex name locationNames; idx = elemIndex name networkNames;
in in
if idx == null then if idx == null then
throw "metatron-addressing: unknown location '${name}'" throw "metatron-addressing: unknown network '${name}'"
else else
idx; idx;
mkSubnetKey =
{ networkName, typeName }:
"${networkName}-${typeName}";
hostRole = path: hostCfg: hostRole = path: hostCfg:
hostCfg.role or (throw "metatron-addressing: host '${path}' is missing required field 'role' for address generation"); hostCfg.role or (throw "metatron-addressing: host '${path}' is missing required field 'role' for address generation");
hostMapForSubnet = hostMapForSubnet =
{ loc, locCode, locationName, networkName, typeName, subnetCfg, scope }: { networkId, networkCode, locationName, networkName, typeName, subnetCfg, scope }:
let let
hostsForSubnet = subnetCfg.hosts or { }; hostsForSubnet = subnetCfg.hosts or { };
hostNames = filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet); hostNames = filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet);
@ -333,7 +358,7 @@ let
hostId = hostCfg.hostId or roleIndex; hostId = hostCfg.hostId or roleIndex;
ip = mkIpNamed { ip = mkIpNamed {
location = loc; location = networkId;
inherit typeName; inherit typeName;
roleName = roleName; roleName = roleName;
host = hostId; host = hostId;
@ -355,10 +380,10 @@ let
location = locationName; location = locationName;
network = networkName; network = networkName;
subnetType = typeName; subnetType = typeName;
subnetKey = "${locationName}-${networkName}-${typeName}"; subnetKey = mkSubnetKey { inherit networkName typeName; };
} }
// optionalAttrs (hostScope.domain != null) { // optionalAttrs (hostScope.domain != null) {
fqdn = "${hostname}.${subnetSlug}.${locCode}.${hostScope.domain}"; fqdn = "${hostname}.${subnetSlug}.${networkCode}.${hostScope.domain}";
} }
// materializeScope hostScope // materializeScope hostScope
// optionalAttrs (hostCfg ? hw-address) { // optionalAttrs (hostCfg ? hw-address) {
@ -383,19 +408,19 @@ let
spec: spec:
let 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; networkId = networkIdForSpec spec;
in in
concatMapAttrs concatMapAttrs
(locationName: locationCfg: (locationName: locationCfg:
let let
loc = locationId locationName;
locCode = mkLocationCode locationName;
locationScope = mergeScopes emptyScope locationCfg; locationScope = mergeScopes emptyScope locationCfg;
networks = locationCfg.networks or { }; networks = locationCfg.networks or { };
in in
concatMapAttrs concatMapAttrs
(networkName: networkCfg: (networkName: networkCfg:
let let
net = networkId networkName;
networkCode = mkLocationCode networkName;
networkScope = mergeScopes locationScope networkCfg; networkScope = mergeScopes locationScope networkCfg;
subnets = networkCfg.subnets or { }; subnets = networkCfg.subnets or { };
in in
@ -405,7 +430,8 @@ let
subnetScope = mergeScopes networkScope subnetCfg; subnetScope = mergeScopes networkScope subnetCfg;
in in
hostMapForSubnet { hostMapForSubnet {
inherit loc locCode locationName networkName typeName subnetCfg; networkId = net;
inherit networkCode locationName networkName typeName subnetCfg;
scope = subnetScope; scope = subnetScope;
}) })
subnets) subnets)
@ -416,19 +442,19 @@ let
spec: spec:
let 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; networkId = networkIdForSpec spec;
in in
concatMapAttrs concatMapAttrs
(locationName: locationCfg: (locationName: locationCfg:
let let
loc = locationId locationName;
locCode = mkLocationCode locationName;
locationScope = mergeScopes emptyScope locationCfg; locationScope = mergeScopes emptyScope locationCfg;
networks = locationCfg.networks or { }; networks = locationCfg.networks or { };
in in
concatMapAttrs concatMapAttrs
(networkName: networkCfg: (networkName: networkCfg:
let let
net = networkId networkName;
networkCode = mkLocationCode networkName;
networkScope = mergeScopes locationScope networkCfg; networkScope = mergeScopes locationScope networkCfg;
subnets = networkCfg.subnets or { }; subnets = networkCfg.subnets or { };
in in
@ -436,8 +462,9 @@ let
(typeName: subnetCfg: (typeName: subnetCfg:
let let
subnetScope = mergeScopes networkScope subnetCfg; subnetScope = mergeScopes networkScope subnetCfg;
subnetKey = mkSubnetKey { inherit networkName typeName; };
cidr = mkSubnetNamed { location = loc; inherit typeName; }; cidr = mkSubnetNamed { location = net; inherit typeName; };
subnetSlug = subnetSlug =
if builtins.hasAttr typeName subnetSlugs if builtins.hasAttr typeName subnetSlugs
@ -447,38 +474,73 @@ let
vlan = vlan =
subnetCfg.vlan or subnetCfg.vlan or
subnetCfg._vlan or subnetCfg._vlan or
(mkVlanNamed { location = loc; inherit typeName; }); (mkVlanNamed { location = net; inherit typeName; });
dhcpCfg = subnetCfg.dhcp or null; # DHCP is generated by default for every subnet unless explicitly disabled.
# Default range:
# start = 10.{network_id}.{subnet_type * 32 + 29}.1
# end = 10.{network_id}.{subnet_type * 32 + 31}.250
# Override options:
# dhcp = false;
# dhcpRange = { startIp = "..."; endIp = "..."; };
# dhcp = {
# startRole = "dhcpStart";
# startHostId = 1;
# endRole = "dhcpEnd";
# endHostId = 250;
# };
dhcpRaw = subnetCfg.dhcp or true;
dhcpEnabled = dhcpRaw != false;
dhcpCfg =
if builtins.isAttrs dhcpRaw
then dhcpRaw
else { };
dhcpRange = dhcpStartRole = dhcpCfg.startRole or "dhcpStart";
if dhcpCfg == null then null else { dhcpEndRole = dhcpCfg.endRole or "dhcpEnd";
# start/end are kept as compatibility aliases for the old dhcp = { start = ...; end = ...; } shape.
dhcpStartHostId = dhcpCfg.startHostId or dhcpCfg.start or 1;
dhcpEndHostId = dhcpCfg.endHostId or dhcpCfg.end or 250;
generatedDhcpRange = {
startIp = mkIpNamed { startIp = mkIpNamed {
location = loc; location = net;
inherit typeName; inherit typeName;
roleName = "pool"; roleName = dhcpStartRole;
host = dhcpCfg.start; host = dhcpStartHostId;
}; };
endIp = mkIpNamed { endIp = mkIpNamed {
location = loc; location = net;
inherit typeName; inherit typeName;
roleName = "pool"; roleName = dhcpEndRole;
host = dhcpCfg.end; host = dhcpEndHostId;
}; };
}; };
dhcpRange =
if !dhcpEnabled then null
else subnetCfg.dhcpRange or generatedDhcpRange;
effectiveDhcp = {
startRole = dhcpStartRole;
startHostId = dhcpStartHostId;
endRole = dhcpEndRole;
endHostId = dhcpEndHostId;
};
in in
nameValuePair nameValuePair
"${locationName}-${networkName}-${typeName}" subnetKey
( (
{ {
inherit cidr locationName networkName typeName vlan; inherit cidr locationName networkName typeName subnetKey vlan;
} }
// optionalAttrs (subnetScope.domain != null) { // optionalAttrs (subnetScope.domain != null) {
zone = "${subnetSlug}.${locCode}.${subnetScope.domain}"; zone = "${subnetSlug}.${networkCode}.${subnetScope.domain}";
} }
// materializeScope subnetScope // materializeScope subnetScope
// optionalAttrs (dhcpCfg != null) { // optionalAttrs (dhcpEnabled && dhcpRange != null) {
dhcp = dhcpCfg; dhcp = effectiveDhcp;
dhcpRange = dhcpRange; dhcpRange = dhcpRange;
} }
)) ))
@ -509,6 +571,9 @@ in
mkNetworkFromSpec mkNetworkFromSpec
mkRoleRange mkRoleRange
mkRoleRangeNamed mkRoleRangeNamed
networkNamesForSpec
networkIdForSpec
mkSubnetKey
mergeScopes; mergeScopes;
# Shorthand; you were calling mkHosts before. # Shorthand; you were calling mkHosts before.

View file

@ -5,7 +5,7 @@
cloud = { cloud = {
domain = "kasear.net"; domain = "kasear.net";
networks.default.subnets = { networks.cloud.subnets = {
dmz.hosts = { dmz.hosts = {
eris = { eris = {
role = "router"; role = "router";
@ -47,7 +47,7 @@
norfolk = { norfolk = {
domain = "kasear.net"; domain = "kasear.net";
networks.default.subnets = { networks.norfolk.subnets = {
dmz.hosts = { dmz.hosts = {
io = { io = {
role = "router"; role = "router";

View file

@ -0,0 +1,37 @@
let
lib = import <nixpkgs/lib>;
addressing = import ../lib/addressing {
inherit lib;
};
spec = {
locations = {
cloud = {
domain = "kasear.net";
networks.cloud.subnets.dmz.hosts.eris.role = "router";
};
norfolk = {
domain = "kasear.net";
networks.norfolk.subnets = {
main.hosts.loki.role = "adminWorkstation";
iot = {
dhcpRange = {
startIp = "10.1.125.10";
endIp = "10.1.127.200";
};
hosts.thermostat.role = "homeAutomation";
};
lab = {
dhcp = false;
hosts.testbox.role = "labDevice";
};
};
};
};
};
in
addressing.mkNetworkFromSpec spec