diff --git a/lib/addressing/default.nix b/lib/addressing/default.nix index daa236c..4147448 100644 --- a/lib/addressing/default.nix +++ b/lib/addressing/default.nix @@ -83,6 +83,11 @@ let ci = 23; media = 24; homeAutomation = 25; + dhcpStart = 29; + dhcpPool = 30; + dhcpEnd = 31; + + # Legacy alias. New generated DHCP ranges use dhcpStart/dhcpEnd. pool = 31; }; @@ -124,6 +129,9 @@ let ci = "ci"; media = "media"; homeAutomation = "home-auto"; + dhcpStart = "dhcp-start"; + dhcpPool = "dhcp-pool"; + dhcpEnd = "dhcp-end"; pool = "dhcp-pool"; }; @@ -283,27 +291,44 @@ let inherit (scope) domain; }; - # locationName -> numeric id (0, 1, 2, ...) - locationIdForSpec = + # All networks across all locations, in deterministic attr-name order. + # Network name, not location name, owns the second octet: + # 10.{network_id}.{subnet_type * 32 + host_role}.{host_id} + networkNamesForSpec = spec: let locations = spec.locations or (throw "metatron-addressing: spec.locations is required"); 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 name: let - idx = elemIndex name locationNames; + idx = elemIndex name networkNames; in if idx == null then - throw "metatron-addressing: unknown location '${name}'" + throw "metatron-addressing: unknown network '${name}'" else idx; + mkSubnetKey = + { networkName, typeName }: + "${networkName}-${typeName}"; + 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 }: + { networkId, networkCode, locationName, networkName, typeName, subnetCfg, scope }: let hostsForSubnet = subnetCfg.hosts or { }; hostNames = filter (hn: builtins.substring 0 1 hn != "_") (attrNames hostsForSubnet); @@ -333,7 +358,7 @@ let hostId = hostCfg.hostId or roleIndex; ip = mkIpNamed { - location = loc; + location = networkId; inherit typeName; roleName = roleName; host = hostId; @@ -355,10 +380,10 @@ let location = locationName; network = networkName; subnetType = typeName; - subnetKey = "${locationName}-${networkName}-${typeName}"; + subnetKey = mkSubnetKey { inherit networkName typeName; }; } // optionalAttrs (hostScope.domain != null) { - fqdn = "${hostname}.${subnetSlug}.${locCode}.${hostScope.domain}"; + fqdn = "${hostname}.${subnetSlug}.${networkCode}.${hostScope.domain}"; } // materializeScope hostScope // optionalAttrs (hostCfg ? hw-address) { @@ -383,19 +408,19 @@ let spec: let locations = spec.locations or (throw "metatron-addressing: spec.locations is required"); - locationId = locationIdForSpec spec; + networkId = networkIdForSpec 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 + net = networkId networkName; + networkCode = mkLocationCode networkName; networkScope = mergeScopes locationScope networkCfg; subnets = networkCfg.subnets or { }; in @@ -405,7 +430,8 @@ let subnetScope = mergeScopes networkScope subnetCfg; in hostMapForSubnet { - inherit loc locCode locationName networkName typeName subnetCfg; + networkId = net; + inherit networkCode locationName networkName typeName subnetCfg; scope = subnetScope; }) subnets) @@ -416,19 +442,19 @@ let spec: let locations = spec.locations or (throw "metatron-addressing: spec.locations is required"); - locationId = locationIdForSpec spec; + networkId = networkIdForSpec 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 + net = networkId networkName; + networkCode = mkLocationCode networkName; networkScope = mergeScopes locationScope networkCfg; subnets = networkCfg.subnets or { }; in @@ -436,8 +462,9 @@ let (typeName: subnetCfg: let subnetScope = mergeScopes networkScope subnetCfg; + subnetKey = mkSubnetKey { inherit networkName typeName; }; - cidr = mkSubnetNamed { location = loc; inherit typeName; }; + cidr = mkSubnetNamed { location = net; inherit typeName; }; subnetSlug = if builtins.hasAttr typeName subnetSlugs @@ -447,38 +474,73 @@ let vlan = 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 { }; + + dhcpStartRole = dhcpCfg.startRole or "dhcpStart"; + 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 { + location = net; + inherit typeName; + roleName = dhcpStartRole; + host = dhcpStartHostId; + }; + endIp = mkIpNamed { + location = net; + inherit typeName; + roleName = dhcpEndRole; + host = dhcpEndHostId; + }; + }; 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; - }; - }; + if !dhcpEnabled then null + else subnetCfg.dhcpRange or generatedDhcpRange; + + effectiveDhcp = { + startRole = dhcpStartRole; + startHostId = dhcpStartHostId; + endRole = dhcpEndRole; + endHostId = dhcpEndHostId; + }; in nameValuePair - "${locationName}-${networkName}-${typeName}" + subnetKey ( { - inherit cidr locationName networkName typeName vlan; + inherit cidr locationName networkName typeName subnetKey vlan; } // optionalAttrs (subnetScope.domain != null) { - zone = "${subnetSlug}.${locCode}.${subnetScope.domain}"; + zone = "${subnetSlug}.${networkCode}.${subnetScope.domain}"; } // materializeScope subnetScope - // optionalAttrs (dhcpCfg != null) { - dhcp = dhcpCfg; + // optionalAttrs (dhcpEnabled && dhcpRange != null) { + dhcp = effectiveDhcp; dhcpRange = dhcpRange; } )) @@ -509,6 +571,9 @@ in mkNetworkFromSpec mkRoleRange mkRoleRangeNamed + networkNamesForSpec + networkIdForSpec + mkSubnetKey mergeScopes; # Shorthand; you were calling mkHosts before. diff --git a/meta.nix b/meta.nix index acbf7b4..d56d5c0 100644 --- a/meta.nix +++ b/meta.nix @@ -5,7 +5,7 @@ cloud = { domain = "kasear.net"; - networks.default.subnets = { + networks.cloud.subnets = { dmz.hosts = { eris = { role = "router"; @@ -47,7 +47,7 @@ norfolk = { domain = "kasear.net"; - networks.default.subnets = { + networks.norfolk.subnets = { dmz.hosts = { io = { role = "router"; diff --git a/tests/addressing-dhcp-test.nix b/tests/addressing-dhcp-test.nix new file mode 100644 index 0000000..b9a1738 --- /dev/null +++ b/tests/addressing-dhcp-test.nix @@ -0,0 +1,37 @@ +let + lib = import ; + + 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