1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 14:50:40 +02:00

feat(topology): add elk json generation

This commit is contained in:
oddlama 2024-03-27 15:36:32 +01:00
parent 3e14f82952
commit 07191bac9b
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
5 changed files with 168 additions and 72 deletions

View file

@ -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";

View file

@ -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;
}
];
};

View file

@ -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 (<category>.<name>). By default an icon will be selected based on the deviceType.";
type = types.nullOr (types.either types.path types.str);

View file

@ -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);
}

View file

@ -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)}
<span tw="font-bold">${interface.id}</span>
</div>
<span>addrs: ${toString interface.addresses}</span>
<span>${toString interface.addresses}</span>
</div>
'';
@ -129,8 +131,8 @@
*/
''
<div tw="flex flex-row mt-1">
<span tw="flex flex-none w-20 font-bold pl-1">${detail.name}</span>
<span tw="flex grow">${detail.text}</span>
<span tw="flex text-sm flex-none w-22 font-bold pl-1">${detail.name}</span>
<span tw="flex text-sm grow">${detail.text}</span>
</div>
'';
@ -144,33 +146,49 @@
html
*/
''
<div tw="flex flex-col mx-4 mt-4 rounded-lg p-2">
<div tw="flex flex-col mx-4 mt-2 rounded-lg p-2">
<div tw="flex flex-row items-center">
${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)}
<div tw="flex flex-col grow">
<h1 tw="text-xl font-bold m-0">${service.name}</h1>
${optionalString (service.info != "") ''<p tw="text-base m-0">${service.info}</p>''}
<h1 tw="text-lg font-bold m-0">${service.name}</h1>
${optionalString (service.info != "") ''<p tw="text-sm m-0">${service.info}</p>''}
</div>
</div>
${serviceDetails service}
</div>
'';
mkGuest = guest:
/*
html
*/
''
<div tw="flex flex-col mx-4 mt-2 rounded-lg p-2">
<div tw="flex flex-row items-center">
${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get guest.deviceIcon)}
<div tw="flex flex-col grow">
<h1 tw="text-lg font-bold m-0">${guest.name}</h1>
<p tw="text-sm m-0">${guest.guestType}</p>
</div>
</div>
</div>
'';
mkTitle = node:
/*
html
*/
''
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get node.icon)}
<h2 tw="text-4xl font-bold">${node.name}</h2>
${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
<h2 tw="text-2xl font-bold">${node.name}</h2>
<div tw="flex grow min-w-8"></div>
${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)}
</div>
'';
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 @@
''
<div tw="flex flex-row mx-6 mt-2 items-center">
${mkImageMaybe "w-12 h-12" (config.lib.icons.get node.icon)}
<h2 tw="text-4xl font-bold">${node.name}</h2>
<h2 tw="text-2xl font-bold">${node.name}</h2>
${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image)
''
<div tw="flex grow min-w-8"></div>