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

chore: play with topology generation in graphviz

This commit is contained in:
oddlama 2024-01-08 03:03:59 +01:00
parent fbab6415ca
commit 714dec1c33
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
6 changed files with 321 additions and 99 deletions

View file

@ -152,18 +152,6 @@
nixosConfigurationsMinimal
;
# XXX: WIP: only testing
d2diag = let
inherit
(nixpkgs.lib)
attrValues
concatLines
;
in
self.pkgs.x86_64-linux.writeText "test.d2" (
concatLines (map (x: x.config.d2diag.text) (attrValues self.nixosConfigurations))
);
# All nixosSystem instanciations are collected here, so that we can refer
# to any system via nodes.<name>
nodes = self.nixosConfigurations // self.guestConfigs;
@ -195,6 +183,12 @@
];
};
# XXX: WIP: only testing
topology = import ./generate-topology.nix {
inherit pkgs;
nixosConfigurations = self.nodes;
};
# For each major system, we provide a customized installer image that
# has ssh and some other convenience stuff preconfigured.
# Not strictly necessary for new setups.

185
generate-topology.nix Normal file
View file

@ -0,0 +1,185 @@
{
pkgs,
renderer ? "graphviz",
nixosConfigurations,
}: let
inherit
(pkgs.lib)
any
attrNames
attrValues
concatLines
concatMapStrings
concatStringsSep
const
elem
escapeXML
flip
filterAttrs
id
imap0
mapAttrs
mapAttrs'
nameValuePair
mapAttrsToList
optional
optionalAttrs
optionalString
optionals
;
global = {
# global entities;
};
asjson = builtins.toFile "topology.dot" (
builtins.toJSON (map (x: x.config.topology) (attrValues nixosConfigurations))
);
colors.base00 = "#101419";
colors.base01 = "#171B20";
colors.base02 = "#21262e";
colors.base03 = "#242931";
colors.base03b = "#353c48";
colors.base04 = "#485263";
colors.base05 = "#b6beca";
colors.base06 = "#dee1e6";
colors.base07 = "#e3e6eb";
colors.base08 = "#e05f65";
colors.base09 = "#f9a872";
colors.base0A = "#f1cf8a";
colors.base0B = "#78dba9";
colors.base0C = "#74bee9";
colors.base0D = "#70a5eb";
colors.base0E = "#c68aee";
colors.base0F = "#9378de";
nodesById = mapAttrs' (_: node: nameValuePair node.config.topology.id node) nixosConfigurations;
xmlAttrs = attrs: concatStringsSep " " (mapAttrsToList (n: v: "${n}=\"${v}\"") attrs);
font = attrs: text: "<font ${xmlAttrs attrs}>${text}</font>";
fontMono = {face = "JetBrains Mono";};
mono = font fontMono;
monoColor = color: font (fontMono // {color = color;});
mkCell = cellAttrs: text: "<td ${xmlAttrs cellAttrs}>${text}</td>";
mapToTableRows = xs: {
columnOrder,
columns,
titleRow ? true,
titleRowColor ? colors.base0C,
titleRowAttrs ? {bgcolor = titleRowColor;},
alternateRowAttrs ? {bgcolor = colors.base03b;},
}:
concatLines (
optional titleRow "<tr>${concatStringsSep "" (flip map columnOrder (c: mkCell titleRowAttrs "<b>${mono columns.${c}.title}</b>"))}</tr>"
++ flip imap0 xs (
i: x: "<tr>${concatStringsSep "" (flip map columnOrder (c:
mkCell
(optionalAttrs (pkgs.lib.mod i 2 == 1) alternateRowAttrs // (columns.${c}.cellAttrs or {}))
(columns.${c}.transform x.${c})))}</tr>"
)
);
mkTable = xs: settings: ''
<table border="0" cellborder="0" cellspacing="0" cellpadding="4" bgcolor="${colors.base03}" color="${colors.base04}">
${mapToTableRows xs settings}
</table>
'';
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
topo = node.config.topology;
diskTable = mkTable (attrValues topo.disks) {
titleRowColor = colors.base0F;
columnOrder = ["name"];
columns = {
name = {
title = "Name";
transform = mono;
};
};
};
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}";
${nodeId topo.id} [label=<
<table border="0" cellborder="0" cellspacing="0" cellpadding="4" bgcolor="${colors.base03}" color="${colors.base04}">
<tr><td bgcolor="${colors.base08}"><b>${mono "Attribute"}</b></td><td bgcolor="${colors.base08}"><b>${mono "Value"}</b></td></tr>
<tr><td>${mono "id"}</td><td>${mono topo.id}</td></tr>
<tr><td>${mono "type"}</td><td>${mono topo.type}</td></tr>
</table>
>];
{
rank = "same";
${nodeId "${topo.id}.disks"} [label=<
${diskTable}
>];
${nodeId "${topo.id}.interfaces"} [label=<
${interfaceTable}
>];
}
${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;
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))}
}
''

View file

@ -1,85 +0,0 @@
{
config,
lib,
nodes,
...
}: let
inherit
(lib)
attrNames
concatMap
getAttrFromPath
mkMerge
mkOption
optionals
types
;
nodeName = config.node.name;
in {
options.d2diag.text = mkOption {
# TODO readonly, _text
description = "TODO";
type = types.lines;
};
options.d2diag.services = mkOption {
description = "TODO";
type = types.attrsOf (types.submodule {
options = {
};
});
};
config = {
d2diag.text =
''
${nodeName}: ${nodeName} {
disks: Disks {
shape: sql_table
${lib.concatLines (map (x: "${x}: 8TB") (lib.attrNames config.disko.devices.disk))}
}
net: Interfaces {
shape: sql_table
${lib.concatLines (lib.mapAttrsToList (n: v: ''${n}: ${v.mac}'') (config.repo.secrets.local.networking.interfaces or {lan.mac = "?";}))}
}
''
+ (lib.optionalString (config.guests != {}) ''
guests: {
${
lib.concatLines (
lib.flip lib.mapAttrsToList config.guests (
guestName: guestDef:
(
if guestDef.backend == "microvm"
then config.microvm.vms.${guestName}.config
else config.containers.${guestName}.nixosConfiguration
)
.config
.d2diag
.text
)
)
}
}
${
lib.concatLines (
lib.flip lib.mapAttrsToList config.guests (
guestName: guestDef: "net.lan -> guests.${(
if guestDef.backend == "microvm"
then config.microvm.vms.${guestName}.config
else config.containers.${guestName}.nixosConfiguration
)
.config
.node
.name}.net.lan"
)
)
}
'')
+ ''
}
'';
};
}

View file

@ -41,7 +41,7 @@
./wireguard-proxy.nix
./wireguard.nix
./d2diag.nix
./topology.nix
];
nixpkgs.overlays = [

View file

@ -72,6 +72,8 @@ in {
};
networking.renameInterfacesByMac.${guestCfg.networking.mainLinkName} = guestCfg.microvm.mac;
systemd.network.networks."10-${guestCfg.networking.mainLinkName}".matchConfig.MACAddress = guestCfg.microvm.mac;
systemd.network.networks."10-${guestCfg.networking.mainLinkName}".matchConfig = mkForce {
MACAddress = guestCfg.microvm.mac;
};
};
}

126
modules/topology.nix Normal file
View file

@ -0,0 +1,126 @@
{
config,
lib,
...
}: let
inherit
(lib)
attrNames
concatMap
flip
filterAttrs
getAttrFromPath
mapAttrs
mapAttrs'
mapAttrsToList
mkMerge
mkOption
nameValuePair
optionals
types
;
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
unique or don't reflect the name you use to refer to that node.
'';
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";
};
interfaces = mkOption {
description = "TODO";
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this interface";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
mac = mkOption {
description = "The MAC address of this interface, if known.";
type = types.nullOr types.str;
default = null;
};
addresses = mkOption {
description = "The configured address(es), or a descriptive string (like DHCP).";
type = types.listOf types.str;
};
};
}));
default = {};
};
disks = mkOption {
type = types.attrsOf (types.submodule (submod: {
options = {
name = mkOption {
description = "The name of this disk";
type = types.str;
readOnly = true;
default = submod.config._module.args.name;
};
};
}));
default = {};
};
};
config.topology = mkMerge [
{
################### TODO user config! #################
id = config.node.name;
################### END user config #################
guests =
flip mapAttrsToList (config.microvm.vms or {})
(_: vmCfg: vmCfg.config.config.topology.id);
# TODO: container
disks =
flip mapAttrs (config.disko.devices.disk or {})
(_: _: {});
# TODO: microvm shares
# TODO: container shares
interfaces = let
isNetwork = netDef: (netDef.matchConfig != {}) && (netDef.address != [] || netDef.DHCP != null);
macsByName = mapAttrs' (flip nameValuePair) (config.networking.renameInterfacesByMac or {});
netNameFor = netName: netDef:
if netDef ? matchConfig.Name
then netDef.matchConfig.Name
else if netDef ? matchConfig.MACAddress && macsByName ? ${netDef.matchConfig.MACAddress}
then macsByName.${netDef.matchConfig.MACAddress}
else lib.trace "Could not derive network name for systemd network ${netName} on host ${config.node.name}, using unit name as fallback." netName;
netMACFor = netDef:
if netDef ? matchConfig.MACAddress
then netDef.matchConfig.MACAddress
else null;
networks = filterAttrs (_: isNetwork) (config.systemd.network.networks or {});
in
flip mapAttrs' networks (netName: netDef:
nameValuePair (netNameFor netName netDef) {
mac = netMACFor netDef;
addresses =
if netDef.address != []
then netDef.address
else ["DHCP"];
});
# TODO: for each nftable zone show open ports
}
];
}