From 07191bac9b636d8ed8dcc0cd723b75888367179b Mon Sep 17 00:00:00 2001 From: oddlama Date: Wed, 27 Mar 2024 15:36:32 +0100 Subject: [PATCH] feat(topology): add elk json generation --- flake.nix | 14 +- hosts/ward/default.nix | 4 + topology/options/nodes.nix | 6 + topology/topology/renderers/elk/default.nix | 168 +++++++++++++------- topology/topology/renderers/svg/default.nix | 48 ++++-- 5 files changed, 168 insertions(+), 72 deletions(-) diff --git a/flake.nix b/flake.nix index c11360a..1653482 100644 --- a/flake.nix +++ b/flake.nix @@ -184,7 +184,7 @@ inherit pkgs; modules = [ { - renderer = "d2"; + renderer = "elk"; nixosConfigurations = self.nodes; nodes.fritzbox = { @@ -211,6 +211,18 @@ ]; }; + nodes.fritzbox-device-nd = { + name = "FritzBox No DImg"; + deviceType = "device"; + hardware.image = ./fritzbox.png; + interfaces.wan0.physicalConnections = [ + { + node = "ward"; + interface = "wan"; + } + ]; + }; + nodes.fritzbox-device = { name = "FritzBox No D&HImg"; deviceType = "device"; diff --git a/hosts/ward/default.nix b/hosts/ward/default.nix index 9537563..31aee08 100644 --- a/hosts/ward/default.nix +++ b/hosts/ward/default.nix @@ -64,6 +64,10 @@ networking.nftables.firewall = { zones.untrusted.interfaces = [config.guests.${guestName}.networking.mainLinkName]; }; + + # TODO: FIXME: remove!!!! + topology.self.guestType = "microvm"; + topology.self.parent = config.node.name; } ]; }; diff --git a/topology/options/nodes.nix b/topology/options/nodes.nix index 96bed20..fae6bf8 100644 --- a/topology/options/nodes.nix +++ b/topology/options/nodes.nix @@ -69,6 +69,12 @@ in type = types.either (types.enum ["nixos" "router" "switch" "device"]) types.str; }; + guestType = mkOption { + description = "If the device is a guest of another device, this will tell the type of guest it is."; + default = null; + type = types.nullOr (types.either (types.enum ["microvm" "nixos-container"]) types.str); + }; + deviceIcon = mkOption { description = "The icon representing this node's type. Must be a path to an image or a valid icon name (.). By default an icon will be selected based on the deviceType."; type = types.nullOr (types.either types.path types.str); diff --git a/topology/topology/renderers/elk/default.nix b/topology/topology/renderers/elk/default.nix index 9a0479c..b9665b8 100644 --- a/topology/topology/renderers/elk/default.nix +++ b/topology/topology/renderers/elk/default.nix @@ -8,68 +8,109 @@ (lib) any attrValues - concatLines + concatStringsSep + elem + flatten flip + foldl' + isAttrs + last + mapAttrs + mapAttrsToList mkOption - optionalString + optional + optionalAttrs + recursiveUpdate types ; - toBase64 = text: let - inherit (lib) sublist mod stringToCharacters concatMapStrings; - inherit (lib.strings) charToInt; - inherit (builtins) substring foldl' genList elemAt length concatStringsSep stringLength; - lookup = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789__"; - sliceN = size: list: n: sublist (n * size) size list; - pows = [(64 * 64 * 64) (64 * 64) 64 1]; - intSextets = i: map (j: mod (i / j) 64) pows; - compose = f: g: x: f (g x); - intToChar = elemAt lookup; - convertTripletInt = sliceInt: concatMapStrings intToChar (intSextets sliceInt); - sliceToInt = foldl' (acc: val: acc * 256 + val) 0; - convertTriplet = compose convertTripletInt sliceToInt; - join = concatStringsSep ""; - convertLastSlice = slice: let - len = length slice; - in - if len == 1 - then (substring 0 2 (convertTripletInt ((sliceToInt slice) * 256 * 256))) + "__" - else if len == 2 - then (substring 0 3 (convertTripletInt ((sliceToInt slice) * 256))) + "_" - else ""; - len = stringLength text; - nFullSlices = len / 3; - bytes = map charToInt (stringToCharacters text); - tripletAt = sliceN 3 bytes; - head = genList (compose convertTriplet tripletAt) nFullSlices; - tail = convertLastSlice (tripletAt nFullSlices); + mapAttrsRecursiveInner = f: set: let + recurse = path: set: + f path ( + flip mapAttrs set ( + name: value: + if isAttrs value + then recurse (path ++ [name]) value + else value + ) + ); in - join (head ++ [tail]); + recurse [] set; - netToElk = net: '' - node net_${toBase64 net.id} { - label "${net.name}" - } - ''; + attrsToListify = ["children" "labels" "ports" "edges"]; + + # Converts an attrset to a list of values with a new field id reflecting the attribute name with all parent ids appended. + listify = path: mapAttrsToList (id: v: {id = concatStringsSep "." (path ++ [id]);} // v); + + # In nix we like to use refer to named attributes using attrsets over using lists, because it + # has merging capabilities and allows easy referencing. But elk needs some attributes like + # children as a list of objects, which we need to transform. + elkifySchema = schema: + flip mapAttrsRecursiveInner schema ( + path: value: + if path != [] && elem (last path) attrsToListify + then listify path value + else value + ); + + netToElk = net: { + children."net:${net.id}" = { + width = 50; + height = 50; + }; + }; nodeInterfaceToElk = node: interface: - concatLines (flip map interface.physicalConnections (x: - optionalString ( + [ + { + children."node:${node.id}".ports."interface:${interface.id}" = { + properties = { + "port.side" = "WEST"; + }; + width = 8; + height = 8; + }; + } + ] + ++ flip map interface.physicalConnections (x: + optionalAttrs ( (!any (y: y.node == node.id && y.interface == interface.id) config.nodes.${x.node}.interfaces.${x.interface}.physicalConnections) || (node.id < x.node) - ) - '' - edge node_${toBase64 node.id} -> node_${toBase64 x.node} - '')); + ) { + edges."node:${node.id}.ports.interface:${interface.id}-to-node:${x.node}.ports.interface:${x.interface}" = { + sources = ["children.node:${node.id}.ports.interface:${interface.id}"]; + targets = ["children.node:${x.node}.ports.interface:${x.interface}"]; + }; + }); - nodeToElk = node: '' - node node_${toBase64 node.id} { - layout [size: 680, 0] - label "${node.name}" + nodeToElk = node: + [ + { + children."node:${node.id}" = { + svg = { + file = config.lib.renderers.svg.node.mkPreferredRender node; + scale = 0.8; + }; + properties = { + "portConstraints" = "FIXED_SIDE"; + }; + }; + } + ] + ++ optional (node.parent != null) { + children."node:${node.parent}".ports.guests = { + properties = { + "port.side" = "EAST"; + }; + width = 8; + height = 8; + }; + edges."node:${node.parent}.ports.guests-to-node:${node.id}" = { + sources = ["children.node:${node.parent}.ports.guests"]; + targets = ["children.node:${node.id}"]; + }; } - - ${concatLines (map (nodeInterfaceToElk node) (attrValues node.interfaces))} - ''; + ++ map (nodeInterfaceToElk node) (attrValues node.interfaces); in { options.renderers.elk = { output = mkOption { @@ -79,13 +120,24 @@ in { }; }; - config.renderers.elk.output = pkgs.writeText "graph.elk" '' - interactiveLayout: true - separateConnectedComponents: false - crossingMinimization.semiInteractive: true - elk.direction: RIGHT - - ${concatLines (map netToElk (attrValues config.networks))} - ${concatLines (map nodeToElk (attrValues config.nodes))} - ''; + config.renderers.elk.output = let + graph = elkifySchema (foldl' recursiveUpdate {} ( + [ + { + id = "root"; + layoutOptions = { + "org.eclipse.elk.algorithm" = "layered"; + "org.eclipse.elk.edgeRouting" = "ORTHOGONAL"; + "org.eclipse.elk.layered.crossingMinimization.strategy" = true; + "org.eclipse.elk.layered.nodePlacement.strategy" = "NETWORK_SIMPLEX"; + "org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers" = 40; + "org.eclipse.elk.direction" = "RIGHT"; + }; + } + ] + ++ flatten (map netToElk (attrValues config.networks)) + ++ flatten (map nodeToElk (attrValues config.nodes)) + )); + in + pkgs.writeText "graph.elk.json" (builtins.toJSON graph); } diff --git a/topology/topology/renderers/svg/default.nix b/topology/topology/renderers/svg/default.nix index b87c907..2596763 100644 --- a/topology/topology/renderers/svg/default.nix +++ b/topology/topology/renderers/svg/default.nix @@ -8,6 +8,8 @@ # - impermanence render? # - stable pseudorandom colors from palette with no-reuse until necessary # - search todo and do +# - podman / docker harvesting +# - adjust device icon based on guest type { config, lib, @@ -119,7 +121,7 @@ ${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)} ${interface.id} - addrs: ${toString interface.addresses} + ${toString interface.addresses} ''; @@ -129,8 +131,8 @@ */ ''
- ${detail.name} - ${detail.text} + ${detail.name} + ${detail.text}
''; @@ -144,33 +146,49 @@ html */ '' -
+
- ${mkImage "w-16 h-16 mr-4 rounded-lg" (config.lib.icons.get service.icon)} + ${mkImage "w-12 h-12 mr-4 rounded-lg" (config.lib.icons.get service.icon)}
-

${service.name}

- ${optionalString (service.info != "") ''

${service.info}

''} +

${service.name}

+ ${optionalString (service.info != "") ''

${service.info}

''}
${serviceDetails service}
''; + mkGuest = guest: + /* + html + */ + '' +
+
+ ${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get guest.deviceIcon)} +
+

${guest.name}

+

${guest.guestType}

+
+
+
+ ''; + mkTitle = node: /* html */ ''
- ${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get node.icon)} -

${node.name}

+ ${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)} +

${node.name}

- ${mkImageMaybe "w-16 h-16 ml-4" (config.lib.icons.get node.deviceIcon)} + ${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
''; mkNetCard = node: { - width = 680; + width = 480; html = mkCardContainer /* @@ -188,8 +206,9 @@ mkCard = node: let services = filter (x: !x.hidden) (attrValues node.services); + guests = filter (x: x.parent == node.id) (attrValues config.nodes); in { - width = 680; + width = 480; html = mkCardContainer /* @@ -201,6 +220,9 @@ ${concatLines (map mkInterface (attrValues node.interfaces))} ${optionalString (node.interfaces != {}) spacingMt2} + ${concatLines (map mkGuest guests)} + ${optionalString (guests != []) spacingMt2} + ${concatLines (map mkService services)} ${optionalString (services != []) spacingMt2} @@ -219,7 +241,7 @@ ''
${mkImageMaybe "w-12 h-12" (config.lib.icons.get node.icon)} -

${node.name}

+

${node.name}

${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image) ''