From f35e402b4df4ffbaf351547855f4949af7c9fda6 Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 1 May 2026 13:07:30 -0500 Subject: [PATCH] Better addressing? --- flake.nix | 42 +--- lib/addressing/default.nix | 395 +++++++++++++++++++++---------------- meta.nix | 218 ++++++++++++++------ 3 files changed, 394 insertions(+), 261 deletions(-) diff --git a/flake.nix b/flake.nix index d47bda5..5c2a318 100644 --- a/flake.nix +++ b/flake.nix @@ -10,67 +10,48 @@ outputs = { self, nixpkgs, deploy-rs, disko, nixos-anywhere, ... }: let - # Default architecture if a system doesn’t override it defaultSystem = "x86_64-linux"; - # pkgs per-system so cross-arch doesn’t 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 ; + 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 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"; diff --git a/lib/addressing/default.nix b/lib/addressing/default.nix index 445da81..daa236c 100644 --- a/lib/addressing/default.nix +++ b/lib/addressing/default.nix @@ -1,4 +1,4 @@ -{ lib ? import { } }: +{ 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; } diff --git a/meta.nix b/meta.nix index 9a18c76..acbf7b4 100644 --- a/meta.nix +++ b/meta.nix @@ -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 = { }; }