From 551b401efaa349f869a171f36515c93936ffb20e Mon Sep 17 00:00:00 2001 From: Yaro Kasear Date: Fri, 28 Nov 2025 16:12:13 -0600 Subject: [PATCH] Adding mkip. --- flake.nix | 139 ++++++++++++ lib/addressing/default.nix | 432 +++++++++++++++++++++++++++++++++++++ 2 files changed, 571 insertions(+) create mode 100644 flake.nix create mode 100644 lib/addressing/default.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6f990a1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,139 @@ +{ + description = "Metanix static flake: meta.nix → addressing, deploy-rs, disko, nixos-anywhere"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; + deploy-rs.url = "github:serokell/deploy-rs"; + disko.url = "github:nix-community/disko"; + nixos-anywhere.url = "github:numtide/nixos-anywhere"; + }; + + outputs = { self, nixpkgs, deploy-rs, disko, nixos-anywhere, ... }: + let + # Default architecture if meta.nix doesn't say otherwise + system = "x86_64-linux"; + + pkgs = import nixpkgs { + inherit system; + }; + + lib = pkgs.lib; + + # Your mkIp / mkHostsFromSpec / mkSubnetsFromSpec, etc. + # This expects: lib/addressing/default.nix + addressing = import ./lib/addressing { + inherit lib; + }; + + # User-provided world model. + meta = import ./meta.nix; + + # Uses your rewritten mkNetworkFromSpec that understands meta.nix shape. + network = addressing.mkNetworkFromSpec meta; + + # Optional: meta.systems = { hostName = { system = "..."; modules = [ ... ]; ... }; ...; } + systemsFromMeta = meta.systems or { }; + in + { + ####################### + # Library-style exports + ####################### + lib = { + metanix = { + inherit meta network addressing; + }; + }; + + ############################# + # Per-host NixOS configs + # + # Driven by meta.systems if present. Shape example: + # meta.systems = { + # deimos = { + # system = "x86_64-linux"; + # modules = [ ./hosts/deimos.nix ]; + # diskoConfig = ./disko/deimos.nix; # optional + # deployHost = "deimos.kasear.net"; # optional + # deployUser = "root"; # optional + # }; + # }; + ############################# + nixosConfigurations = + lib.mapAttrs + (name: sysCfg: + let + systemForHost = sysCfg.system or system; + + pkgsForHost = import nixpkgs { + inherit systemForHost; + }; + + hostLib = pkgsForHost.lib; + + # Optional disko module wiring if sysCfg.diskoConfig exists. + diskoModule = + if sysCfg ? diskoConfig then + { imports = [ disko.nixosModules.disko sysCfg.diskoConfig ]; } + else + { }; + in + hostLib.nixosSystem { + system = systemForHost; + modules = + (sysCfg.modules or [ ]) ++ [ + diskoModule + ({ ... }: { + _module.args = { + inherit meta network addressing; + hostName = name; + }; + }) + ]; + }) + systemsFromMeta; + + ######################################## + # deploy-rs integration + # + # Builds deploy.nodes using meta.systems + addressing. + ######################################## + deploy = { + nodes = + lib.mapAttrs + (name: sysCfg: + let + hasNetworkHost = builtins.hasAttr name network.hosts; + hostInfo = if hasNetworkHost then network.hosts.${name} else null; + + defaultHostname = + if hasNetworkHost then hostInfo.fqdn else "${name}.${meta.domain}"; + in + { + hostname = sysCfg.deployHost or defaultHostname; + + profiles.system = { + user = sysCfg.deployUser or "root"; + path = + deploy-rs.lib.${system}.activate.nixos + self.nixosConfigurations.${name}; + }; + }) + systemsFromMeta; + }; + + ######################################## + # deploy-rs sanity checks + ######################################## + checks.${system}.deploy = + deploy-rs.lib.${system}.deployChecks self.deploy; + + ######################################## + # nixos-anywhere convenience app + ######################################## + apps.${system}.nixos-anywhere = { + type = "app"; + program = + "${nixos-anywhere.packages.${system}.nixos-anywhere}/bin/nixos-anywhere"; + }; + }; +} diff --git a/lib/addressing/default.nix b/lib/addressing/default.nix new file mode 100644 index 0000000..ec45cfa --- /dev/null +++ b/lib/addressing/default.nix @@ -0,0 +1,432 @@ +{ lib ? import { } }: + +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; what’s 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; +}