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:
parent
3e14f82952
commit
07191bac9b
5 changed files with 168 additions and 72 deletions
14
flake.nix
14
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";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue