diff --git a/flake.nix b/flake.nix index 98d4faf..67699fb 100644 --- a/flake.nix +++ b/flake.nix @@ -152,18 +152,6 @@ nixosConfigurationsMinimal ; - # XXX: WIP: only testing - d2diag = let - inherit - (nixpkgs.lib) - attrValues - concatLines - ; - in - self.pkgs.x86_64-linux.writeText "test.d2" ( - concatLines (map (x: x.config.d2diag.text) (attrValues self.nixosConfigurations)) - ); - # All nixosSystem instanciations are collected here, so that we can refer # to any system via nodes. nodes = self.nixosConfigurations // self.guestConfigs; @@ -195,6 +183,12 @@ ]; }; + # XXX: WIP: only testing + topology = import ./generate-topology.nix { + inherit pkgs; + nixosConfigurations = self.nodes; + }; + # For each major system, we provide a customized installer image that # has ssh and some other convenience stuff preconfigured. # Not strictly necessary for new setups. diff --git a/generate-topology.nix b/generate-topology.nix new file mode 100644 index 0000000..fff2563 --- /dev/null +++ b/generate-topology.nix @@ -0,0 +1,185 @@ +{ + pkgs, + renderer ? "graphviz", + nixosConfigurations, +}: let + inherit + (pkgs.lib) + any + attrNames + attrValues + concatLines + concatMapStrings + concatStringsSep + const + elem + escapeXML + flip + filterAttrs + id + imap0 + mapAttrs + mapAttrs' + nameValuePair + mapAttrsToList + optional + optionalAttrs + optionalString + optionals + ; + + global = { + # global entities; + }; + + asjson = builtins.toFile "topology.dot" ( + builtins.toJSON (map (x: x.config.topology) (attrValues nixosConfigurations)) + ); + + colors.base00 = "#101419"; + colors.base01 = "#171B20"; + colors.base02 = "#21262e"; + colors.base03 = "#242931"; + colors.base03b = "#353c48"; + colors.base04 = "#485263"; + colors.base05 = "#b6beca"; + colors.base06 = "#dee1e6"; + colors.base07 = "#e3e6eb"; + colors.base08 = "#e05f65"; + colors.base09 = "#f9a872"; + colors.base0A = "#f1cf8a"; + colors.base0B = "#78dba9"; + colors.base0C = "#74bee9"; + colors.base0D = "#70a5eb"; + colors.base0E = "#c68aee"; + colors.base0F = "#9378de"; + + nodesById = mapAttrs' (_: node: nameValuePair node.config.topology.id node) nixosConfigurations; + + xmlAttrs = attrs: concatStringsSep " " (mapAttrsToList (n: v: "${n}=\"${v}\"") attrs); + font = attrs: text: "${text}"; + fontMono = {face = "JetBrains Mono";}; + mono = font fontMono; + monoColor = color: font (fontMono // {color = color;}); + + mkCell = cellAttrs: text: "${text}"; + mapToTableRows = xs: { + columnOrder, + columns, + titleRow ? true, + titleRowColor ? colors.base0C, + titleRowAttrs ? {bgcolor = titleRowColor;}, + alternateRowAttrs ? {bgcolor = colors.base03b;}, + }: + concatLines ( + optional titleRow "${concatStringsSep "" (flip map columnOrder (c: mkCell titleRowAttrs "${mono columns.${c}.title}"))}" + ++ flip imap0 xs ( + i: x: "${concatStringsSep "" (flip map columnOrder (c: + mkCell + (optionalAttrs (pkgs.lib.mod i 2 == 1) alternateRowAttrs // (columns.${c}.cellAttrs or {})) + (columns.${c}.transform x.${c})))}" + ) + ); + + mkTable = xs: settings: '' + + ${mapToTableRows xs settings} +
+ ''; + + nodeId = str: "\"${escapeXML str}\""; + isGuestOfAny = node: any (x: elem node x.config.topology.guests) (attrValues nodesById); + rootNodes = filterAttrs (n: _: !(isGuestOfAny n)) nodesById; + + toDot = node: let + topo = node.config.topology; + + diskTable = mkTable (attrValues topo.disks) { + titleRowColor = colors.base0F; + columnOrder = ["name"]; + columns = { + name = { + title = "Name"; + transform = mono; + }; + }; + }; + + interfaceTable = mkTable (attrValues topo.interfaces) { + titleRowColor = colors.base0D; + columnOrder = ["name" "mac" "addresses"]; + columns = { + name = { + title = "Name"; + transform = x: + if x == null + then "" + else mono x; + }; + mac = { + title = "MAC"; + transform = x: + if x == null + then "" + else monoColor colors.base09 x; + }; + addresses = { + title = "Addr"; + transform = xs: mono (concatStringsSep " " xs); + }; + }; + }; + in + '' + subgraph ${nodeId "cluster_${topo.id}"} { + color = "${colors.base04}"; + + ${nodeId topo.id} [label=< + + + + +
${mono "Attribute"}${mono "Value"}
${mono "id"}${mono topo.id}
${mono "type"}${mono topo.type}
+ >]; + + { + rank = "same"; + ${nodeId "${topo.id}.disks"} [label=< + ${diskTable} + >]; + ${nodeId "${topo.id}.interfaces"} [label=< + ${interfaceTable} + >]; + } + + ${nodeId topo.id} -> ${nodeId "${topo.id}.disks"} [label="disks", color="${colors.base05}", fontcolor="${colors.base06}"]; + ${nodeId topo.id} -> ${nodeId "${topo.id}.interfaces"} [label="interfaces", color="${colors.base05}", fontcolor="${colors.base06}"]; + '' + + optionalString (topo.guests != []) '' + subgraph ${nodeId "cluster_guests_${topo.id}"} { + color = "${colors.base04}"; + { + rank = "same"; + ${concatLines (map (guest: "${nodeId guest};") topo.guests)} + } + + ${concatLines (map (guest: dotForNodes.${guest}) topo.guests)} + }; + + ${concatLines (map (guest: "${nodeId topo.id} -> ${nodeId guest} [color=\"${colors.base05}\"];") topo.guests)} + } + '' + + optionalString (!isGuestOfAny topo.id) '' + root -> ${nodeId topo.id} [color="${colors.base05}"]; + ''; + + dotForNodes = mapAttrs' (_: node: nameValuePair node.config.topology.id (toDot node)) nodesById; +in + pkgs.writeText "topology.dot" '' + digraph G { + graph [rankdir=TB, splines=spline, bgcolor="${colors.base00}"]; + node [shape=plaintext, fontcolor="${colors.base06}", color="${colors.base06}"]; + + ${concatLines (map (x: dotForNodes.${x}) (attrNames rootNodes))} + } + '' diff --git a/modules/d2diag.nix b/modules/d2diag.nix deleted file mode 100644 index 50e0a92..0000000 --- a/modules/d2diag.nix +++ /dev/null @@ -1,85 +0,0 @@ -{ - config, - lib, - nodes, - ... -}: let - inherit - (lib) - attrNames - concatMap - getAttrFromPath - mkMerge - mkOption - optionals - types - ; - - nodeName = config.node.name; -in { - options.d2diag.text = mkOption { - # TODO readonly, _text - description = "TODO"; - type = types.lines; - }; - - options.d2diag.services = mkOption { - description = "TODO"; - type = types.attrsOf (types.submodule { - options = { - }; - }); - }; - - config = { - d2diag.text = - '' - ${nodeName}: ${nodeName} { - disks: Disks { - shape: sql_table - ${lib.concatLines (map (x: "${x}: 8TB") (lib.attrNames config.disko.devices.disk))} - } - net: Interfaces { - shape: sql_table - ${lib.concatLines (lib.mapAttrsToList (n: v: ''${n}: ${v.mac}'') (config.repo.secrets.local.networking.interfaces or {lan.mac = "?";}))} - } - '' - + (lib.optionalString (config.guests != {}) '' - guests: { - ${ - lib.concatLines ( - lib.flip lib.mapAttrsToList config.guests ( - guestName: guestDef: - ( - if guestDef.backend == "microvm" - then config.microvm.vms.${guestName}.config - else config.containers.${guestName}.nixosConfiguration - ) - .config - .d2diag - .text - ) - ) - } - } - - ${ - lib.concatLines ( - lib.flip lib.mapAttrsToList config.guests ( - guestName: guestDef: "net.lan -> guests.${( - if guestDef.backend == "microvm" - then config.microvm.vms.${guestName}.config - else config.containers.${guestName}.nixosConfiguration - ) - .config - .node - .name}.net.lan" - ) - ) - } - '') - + '' - } - ''; - }; -} diff --git a/modules/default.nix b/modules/default.nix index 87cda06..7e98409 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -41,7 +41,7 @@ ./wireguard-proxy.nix ./wireguard.nix - ./d2diag.nix + ./topology.nix ]; nixpkgs.overlays = [ diff --git a/modules/guests/microvm.nix b/modules/guests/microvm.nix index fe04555..c3bc825 100644 --- a/modules/guests/microvm.nix +++ b/modules/guests/microvm.nix @@ -72,6 +72,8 @@ in { }; networking.renameInterfacesByMac.${guestCfg.networking.mainLinkName} = guestCfg.microvm.mac; - systemd.network.networks."10-${guestCfg.networking.mainLinkName}".matchConfig.MACAddress = guestCfg.microvm.mac; + systemd.network.networks."10-${guestCfg.networking.mainLinkName}".matchConfig = mkForce { + MACAddress = guestCfg.microvm.mac; + }; }; } diff --git a/modules/topology.nix b/modules/topology.nix new file mode 100644 index 0000000..a1d4e0a --- /dev/null +++ b/modules/topology.nix @@ -0,0 +1,126 @@ +{ + config, + lib, + ... +}: let + inherit + (lib) + attrNames + concatMap + flip + filterAttrs + getAttrFromPath + mapAttrs + mapAttrs' + mapAttrsToList + mkMerge + mkOption + nameValuePair + optionals + types + ; +in { + options.topology = { + id = mkOption { + description = '' + The attribute name in nixosConfigurations corresponding to this host. + Please overwrite with a unique identifier if your hostnames are not + unique or don't reflect the name you use to refer to that node. + ''; + type = types.str; + }; + guests = mkOption { + description = "TODO guests ids (topology.id)"; + type = types.listOf types.str; + default = []; + }; + type = mkOption { + description = "TODO"; + type = types.enum ["normal" "microvm" "nixos-container"]; + default = "normal"; + }; + interfaces = mkOption { + description = "TODO"; + type = types.attrsOf (types.submodule (submod: { + options = { + name = mkOption { + description = "The name of this interface"; + type = types.str; + readOnly = true; + default = submod.config._module.args.name; + }; + + mac = mkOption { + description = "The MAC address of this interface, if known."; + type = types.nullOr types.str; + default = null; + }; + + addresses = mkOption { + description = "The configured address(es), or a descriptive string (like DHCP)."; + type = types.listOf types.str; + }; + }; + })); + default = {}; + }; + disks = mkOption { + type = types.attrsOf (types.submodule (submod: { + options = { + name = mkOption { + description = "The name of this disk"; + type = types.str; + readOnly = true; + default = submod.config._module.args.name; + }; + }; + })); + default = {}; + }; + }; + + config.topology = mkMerge [ + { + ################### TODO user config! ################# + id = config.node.name; + ################### END user config ################# + + guests = + flip mapAttrsToList (config.microvm.vms or {}) + (_: vmCfg: vmCfg.config.config.topology.id); + # TODO: container + + disks = + flip mapAttrs (config.disko.devices.disk or {}) + (_: _: {}); + # TODO: microvm shares + # TODO: container shares + + interfaces = let + isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null); + macsByName = mapAttrs' (flip nameValuePair) (config.networking.renameInterfacesByMac or {}); + netNameFor = netName: netDef: + if netDef ? matchConfig.Name + then netDef.matchConfig.Name + else if netDef ? matchConfig.MACAddress && macsByName ? ${netDef.matchConfig.MACAddress} + then macsByName.${netDef.matchConfig.MACAddress} + else lib.trace "Could not derive network name for systemd network ${netName} on host ${config.node.name}, using unit name as fallback." netName; + netMACFor = netDef: + if netDef ? matchConfig.MACAddress + then netDef.matchConfig.MACAddress + else null; + networks = filterAttrs (_: isNetwork) (config.systemd.network.networks or {}); + in + flip mapAttrs' networks (netName: netDef: + nameValuePair (netNameFor netName netDef) { + mac = netMACFor netDef; + addresses = + if netDef.address != [] + then netDef.address + else ["DHCP"]; + }); + + # TODO: for each nftable zone show open ports + } + ]; +}