mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-11 07:10:39 +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;
|
inherit pkgs;
|
||||||
modules = [
|
modules = [
|
||||||
{
|
{
|
||||||
renderer = "d2";
|
renderer = "elk";
|
||||||
nixosConfigurations = self.nodes;
|
nixosConfigurations = self.nodes;
|
||||||
|
|
||||||
nodes.fritzbox = {
|
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 = {
|
nodes.fritzbox-device = {
|
||||||
name = "FritzBox No D&HImg";
|
name = "FritzBox No D&HImg";
|
||||||
deviceType = "device";
|
deviceType = "device";
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
networking.nftables.firewall = {
|
networking.nftables.firewall = {
|
||||||
zones.untrusted.interfaces = [config.guests.${guestName}.networking.mainLinkName];
|
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;
|
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 {
|
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.";
|
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);
|
type = types.nullOr (types.either types.path types.str);
|
||||||
|
|
|
@ -8,68 +8,109 @@
|
||||||
(lib)
|
(lib)
|
||||||
any
|
any
|
||||||
attrValues
|
attrValues
|
||||||
concatLines
|
concatStringsSep
|
||||||
|
elem
|
||||||
|
flatten
|
||||||
flip
|
flip
|
||||||
|
foldl'
|
||||||
|
isAttrs
|
||||||
|
last
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
mkOption
|
mkOption
|
||||||
optionalString
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
recursiveUpdate
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
|
|
||||||
toBase64 = text: let
|
mapAttrsRecursiveInner = f: set: let
|
||||||
inherit (lib) sublist mod stringToCharacters concatMapStrings;
|
recurse = path: set:
|
||||||
inherit (lib.strings) charToInt;
|
f path (
|
||||||
inherit (builtins) substring foldl' genList elemAt length concatStringsSep stringLength;
|
flip mapAttrs set (
|
||||||
lookup = stringToCharacters "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789__";
|
name: value:
|
||||||
sliceN = size: list: n: sublist (n * size) size list;
|
if isAttrs value
|
||||||
pows = [(64 * 64 * 64) (64 * 64) 64 1];
|
then recurse (path ++ [name]) value
|
||||||
intSextets = i: map (j: mod (i / j) 64) pows;
|
else value
|
||||||
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);
|
|
||||||
in
|
in
|
||||||
join (head ++ [tail]);
|
recurse [] set;
|
||||||
|
|
||||||
netToElk = net: ''
|
attrsToListify = ["children" "labels" "ports" "edges"];
|
||||||
node net_${toBase64 net.id} {
|
|
||||||
label "${net.name}"
|
# 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:
|
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)
|
(!any (y: y.node == node.id && y.interface == interface.id) config.nodes.${x.node}.interfaces.${x.interface}.physicalConnections)
|
||||||
|| (node.id < x.node)
|
|| (node.id < x.node)
|
||||||
)
|
) {
|
||||||
''
|
edges."node:${node.id}.ports.interface:${interface.id}-to-node:${x.node}.ports.interface:${x.interface}" = {
|
||||||
edge node_${toBase64 node.id} -> node_${toBase64 x.node}
|
sources = ["children.node:${node.id}.ports.interface:${interface.id}"];
|
||||||
''));
|
targets = ["children.node:${x.node}.ports.interface:${x.interface}"];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
nodeToElk = node: ''
|
nodeToElk = node:
|
||||||
node node_${toBase64 node.id} {
|
[
|
||||||
layout [size: 680, 0]
|
{
|
||||||
label "${node.name}"
|
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}"];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
++ map (nodeInterfaceToElk node) (attrValues node.interfaces);
|
||||||
${concatLines (map (nodeInterfaceToElk node) (attrValues node.interfaces))}
|
|
||||||
'';
|
|
||||||
in {
|
in {
|
||||||
options.renderers.elk = {
|
options.renderers.elk = {
|
||||||
output = mkOption {
|
output = mkOption {
|
||||||
|
@ -79,13 +120,24 @@ in {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config.renderers.elk.output = pkgs.writeText "graph.elk" ''
|
config.renderers.elk.output = let
|
||||||
interactiveLayout: true
|
graph = elkifySchema (foldl' recursiveUpdate {} (
|
||||||
separateConnectedComponents: false
|
[
|
||||||
crossingMinimization.semiInteractive: true
|
{
|
||||||
elk.direction: RIGHT
|
id = "root";
|
||||||
|
layoutOptions = {
|
||||||
${concatLines (map netToElk (attrValues config.networks))}
|
"org.eclipse.elk.algorithm" = "layered";
|
||||||
${concatLines (map nodeToElk (attrValues config.nodes))}
|
"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?
|
# - impermanence render?
|
||||||
# - stable pseudorandom colors from palette with no-reuse until necessary
|
# - stable pseudorandom colors from palette with no-reuse until necessary
|
||||||
# - search todo and do
|
# - search todo and do
|
||||||
|
# - podman / docker harvesting
|
||||||
|
# - adjust device icon based on guest type
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
@ -119,7 +121,7 @@
|
||||||
${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
|
${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
|
||||||
<span tw="font-bold">${interface.id}</span>
|
<span tw="font-bold">${interface.id}</span>
|
||||||
</div>
|
</div>
|
||||||
<span>addrs: ${toString interface.addresses}</span>
|
<span>${toString interface.addresses}</span>
|
||||||
</div>
|
</div>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -129,8 +131,8 @@
|
||||||
*/
|
*/
|
||||||
''
|
''
|
||||||
<div tw="flex flex-row mt-1">
|
<div tw="flex flex-row mt-1">
|
||||||
<span tw="flex flex-none w-20 font-bold pl-1">${detail.name}</span>
|
<span tw="flex text-sm flex-none w-22 font-bold pl-1">${detail.name}</span>
|
||||||
<span tw="flex grow">${detail.text}</span>
|
<span tw="flex text-sm grow">${detail.text}</span>
|
||||||
</div>
|
</div>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -144,33 +146,49 @@
|
||||||
html
|
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">
|
<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">
|
<div tw="flex flex-col grow">
|
||||||
<h1 tw="text-xl font-bold m-0">${service.name}</h1>
|
<h1 tw="text-lg font-bold m-0">${service.name}</h1>
|
||||||
${optionalString (service.info != "") ''<p tw="text-base m-0">${service.info}</p>''}
|
${optionalString (service.info != "") ''<p tw="text-sm m-0">${service.info}</p>''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${serviceDetails service}
|
${serviceDetails service}
|
||||||
</div>
|
</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:
|
mkTitle = node:
|
||||||
/*
|
/*
|
||||||
html
|
html
|
||||||
*/
|
*/
|
||||||
''
|
''
|
||||||
<div tw="flex flex-row mx-6 mt-2 items-center">
|
<div tw="flex flex-row mx-6 mt-2 items-center">
|
||||||
${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get node.icon)}
|
${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
|
||||||
<h2 tw="text-4xl font-bold">${node.name}</h2>
|
<h2 tw="text-2xl font-bold">${node.name}</h2>
|
||||||
<div tw="flex grow min-w-8"></div>
|
<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>
|
</div>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
mkNetCard = node: {
|
mkNetCard = node: {
|
||||||
width = 680;
|
width = 480;
|
||||||
html =
|
html =
|
||||||
mkCardContainer
|
mkCardContainer
|
||||||
/*
|
/*
|
||||||
|
@ -188,8 +206,9 @@
|
||||||
|
|
||||||
mkCard = node: let
|
mkCard = node: let
|
||||||
services = filter (x: !x.hidden) (attrValues node.services);
|
services = filter (x: !x.hidden) (attrValues node.services);
|
||||||
|
guests = filter (x: x.parent == node.id) (attrValues config.nodes);
|
||||||
in {
|
in {
|
||||||
width = 680;
|
width = 480;
|
||||||
html =
|
html =
|
||||||
mkCardContainer
|
mkCardContainer
|
||||||
/*
|
/*
|
||||||
|
@ -201,6 +220,9 @@
|
||||||
${concatLines (map mkInterface (attrValues node.interfaces))}
|
${concatLines (map mkInterface (attrValues node.interfaces))}
|
||||||
${optionalString (node.interfaces != {}) spacingMt2}
|
${optionalString (node.interfaces != {}) spacingMt2}
|
||||||
|
|
||||||
|
${concatLines (map mkGuest guests)}
|
||||||
|
${optionalString (guests != []) spacingMt2}
|
||||||
|
|
||||||
${concatLines (map mkService services)}
|
${concatLines (map mkService services)}
|
||||||
${optionalString (services != []) spacingMt2}
|
${optionalString (services != []) spacingMt2}
|
||||||
|
|
||||||
|
@ -219,7 +241,7 @@
|
||||||
''
|
''
|
||||||
<div tw="flex flex-row mx-6 mt-2 items-center">
|
<div tw="flex flex-row mx-6 mt-2 items-center">
|
||||||
${mkImageMaybe "w-12 h-12" (config.lib.icons.get node.icon)}
|
${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)
|
${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image)
|
||||||
''
|
''
|
||||||
<div tw="flex grow min-w-8"></div>
|
<div tw="flex grow min-w-8"></div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue