diff --git a/generate-topology.nix b/generate-topology.nix index 6162d39..bae90be 100644 --- a/generate-topology.nix +++ b/generate-topology.nix @@ -52,130 +52,31 @@ 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 // {inherit 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 + toD2 = node: let topo = node.config.topology; + in '' + ${topo.id}: |md + # ${topo.id} - diskTable = mkTable (attrValues topo.disks) { - titleRowColor = colors.base0F; - columnOrder = ["name"]; - columns = { - name = { - title = "Name"; - transform = mono; - }; - }; - }; + ## Guests: + ${concatLines (map (x: "- ${x}") topo.guests)} - 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}"; + ## Disks: + ${concatLines (mapAttrsToList (_: v: "- ${v.name}") topo.disks)} - ${nodeId topo.id} [label=< - - - - -
${mono "Attribute"}${mono "Value"}
${mono "id"}${mono topo.id}
${mono "type"}${mono topo.type}
- >]; + ## Interfaces: + ${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") topo.interfaces)} - { - rank = "same"; - ${nodeId "${topo.id}.disks"} [label=< - ${diskTable} - >]; - ${nodeId "${topo.id}.interfaces"} [label=< - ${interfaceTable} - >]; - } + ## Firewall Zones: + ${concatLines (mapAttrsToList (_: v: "- ${v.name}, mac ${toString v.mac}, addrs ${toString v.addresses}, network ${toString v.network}") topo.firewallRules)} + | + ''; - ${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; + d2ForNodes = mapAttrs' (_: node: nameValuePair node.config.topology.id (toD2 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))} - } + pkgs.writeText "topology.d2" '' + ${concatLines (map (x: d2ForNodes.${x}) (attrNames rootNodes))} '' diff --git a/modules/topology.nix b/modules/topology.nix index 84f82d9..936eab2 100644 --- a/modules/topology.nix +++ b/modules/topology.nix @@ -19,24 +19,44 @@ 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 + The attribute name in the given `nodes` which corresponds to this host. + Please overwrite it with a unique identifier if your hostnames are not unique or don't reflect the name you use to refer to that node. ''; + default = config.networking.hostName; + # TODO ensure unique across the board 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"; + type = types.enum ["normal" "microvm" "nixos-container"]; }; + + guests = mkOption { + description = "TODO guests ids (topology.node..id) ensure exists"; + default = []; + type = types.listOf types.str; + }; + + disks = mkOption { + default = {}; + type = types.attrsOf (types.submodule (submod: { + options = { + name = mkOption { + description = "The name of this disk"; + default = submod.config._module.args.name; + readOnly = true; + type = types.str; + }; + }; + })); + }; + interfaces = mkOption { description = "TODO"; + default = {}; type = types.attrsOf (types.submodule (submod: { options = { name = mkOption { @@ -48,30 +68,45 @@ in { mac = mkOption { description = "The MAC address of this interface, if known."; - type = types.nullOr types.str; default = null; + type = types.nullOr types.str; }; addresses = mkOption { description = "The configured address(es), or a descriptive string (like DHCP)."; type = types.listOf types.str; }; + + network = mkOption { + description = '' + The global name of the attached/spanned network. + If this is given, this interface can be shown in the network graph. + ''; + default = null; + type = types.nullOr types.str; + }; }; })); - default = {}; }; - disks = mkOption { + + firewallRules = mkOption { + description = "TODO"; + default = {}; type = types.attrsOf (types.submodule (submod: { options = { name = mkOption { - description = "The name of this disk"; + description = "The name of this firewall rule"; type = types.str; readOnly = true; default = submod.config._module.args.name; }; + + contents = mkOption { + description = "A human readable summary of this rule's effects"; + type = types.lines; + }; }; })); - default = {}; }; }; @@ -89,8 +124,10 @@ in { disks = flip mapAttrs (config.disko.devices.disk or {}) (_: _: {}); + # TODO: zfs pools from disko / fileSystems # TODO: microvm shares # TODO: container shares + # TODO: OCI containers shares interfaces = let isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null);