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 automatic network coloring

This commit is contained in:
oddlama 2024-03-31 23:43:44 +02:00
parent dc4d82c828
commit c3dcc619af
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
6 changed files with 217 additions and 65 deletions

View file

@ -223,12 +223,12 @@
networks.home-lan = {
name = "Home LAN";
cidrv4 = "192.168.1.0/24";
color = "#78dba9";
#color = "#78dba9";
};
networks.home-fritzbox = {
name = "Home Fritzbox";
cidrv4 = "192.168.178.0/24";
color = "#f1cf8a";
#color = "#f1cf8a";
};
nodes.ward.interfaces.lan.network = "home-lan";

View file

@ -6,8 +6,6 @@ f: {
}: let
inherit
(lib)
all
assertMsg
attrNames
attrValues
concatLines
@ -18,51 +16,26 @@ f: {
flatten
flip
foldl'
isAttrs
length
mapAttrsToList
mkDefault
mkIf
mkOption
mkOptionType
optional
optionalAttrs
recursiveUpdate
reverseList
showOption
types
unique
warnIf
;
# Checks whether the value is a lazy value without causing
# it's value to be evaluated
isLazyValue = x: isAttrs x && x ? _lazyValue;
# Constructs a lazy value holding the given value.
lazyValue = value: {_lazyValue = value;};
# Represents a lazy value of the given type, which
# holds the actual value as an attrset like { _lazyValue = <actual value>; }.
# This allows the option to be defined and filtered from a defintion
# list without evaluating the value.
lazyValueOf = type:
mkOptionType rec {
name = "lazyValueOf ${type.name}";
inherit (type) description descriptionClass emptyValue getSubOptions getSubModules;
check = isLazyValue;
merge = loc: defs:
assert assertMsg
(all (x: type.check x._lazyValue) defs)
"The option `${showOption loc}` is defined with a lazy value holding an invalid type";
types.mergeOneOption loc defs;
substSubModules = m: types.uniq (type.substSubModules m);
functor = (types.defaultFunctor name) // {wrapped = type;};
nestedTypes.elemType = type;
};
# Represents a value or lazy value of the given type that will
# automatically be coerced to the given type when merged.
lazyOf = type: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type;
inherit
(import ../topology/lazy.nix lib)
isLazyValue
lazyOf
lazyValue
;
allNodes = attrNames config.nodes;
allInterfacesOf = node: attrNames config.nodes.${node}.interfaces;
@ -323,6 +296,12 @@ in
description = "The other node's interface id.";
type = types.str;
};
renderer.reverse = mkOption {
description = "Whether to reverse the edge. Can be useful to affect node positioning if the layouter is directional.";
type = types.bool;
default = false;
};
};
});
};

View file

@ -1,16 +1,101 @@
f: {
lib,
config,
options,
...
}: let
inherit
(lib)
attrNames
attrValues
elemAt
flatten
flip
foldl'
imap0
length
mapAttrsToList
mkOption
mod
optional
optionalAttrs
recursiveUpdate
subtractLists
types
unique
warnIf
;
inherit
(import ../topology/lazy.nix lib)
isLazyValue
lazyOf
lazyValue
;
style = primaryColor: secondaryColor: pattern: {
inherit primaryColor secondaryColor pattern;
};
predefinedStyles = [
(style "#f1cf8a" null "solid")
(style "#70a5eb" null "solid")
(style "#9dd68d" null "solid")
(style "#5fe1ff" null "solid")
(style "#e05f65" null "solid")
(style "#f9a872" null "solid")
(style "#78dba9" null "solid")
(style "#9378de" null "solid")
(style "#c68aee" null "solid")
(style "#f5a6b8" null "solid")
(style "#70a5eb" null "dashed")
(style "#9dd68d" null "dashed")
(style "#f1cf8a" null "dashed")
(style "#5fe1ff" null "dashed")
(style "#e05f65" null "dashed")
(style "#f9a872" null "dashed")
(style "#78dba9" null "dashed")
(style "#9378de" null "dashed")
(style "#c68aee" null "dashed")
(style "#f5a6b8" null "dashed")
(style "#e05f65" "#e3e6eb" "dashed")
(style "#70a5eb" "#e3e6eb" "dashed")
(style "#9378de" "#e3e6eb" "dashed")
(style "#9dd68d" "#707379" "dashed")
(style "#f1cf8a" "#707379" "dashed")
(style "#5fe1ff" "#707379" "dashed")
(style "#f9a872" "#707379" "dashed")
(style "#78dba9" "#707379" "dashed")
(style "#c68aee" "#707379" "dashed")
(style "#f5a6b8" "#707379" "dashed")
];
# A map containing all networks that have an explicitly assigned style, and the style.
explicitStyles = foldl' recursiveUpdate {} (flatten (
flip map options.networks.definitions (mapAttrsToList (
netId: net:
optional (net ? style && !isLazyValue net.style) {
${netId} = net.style;
}
))
));
# All unused predefined styles
remainingStyles = subtractLists (unique (attrValues explicitStyles)) predefinedStyles;
# All networks without styles
remainingNets = subtractLists (attrNames explicitStyles) (attrNames config.networks);
# Fold over all networks that have no style and assign the next free one. Warn and repeat from beginning if necessary.
computedStyles =
explicitStyles
// warnIf
(length remainingNets > length remainingStyles)
"topology: There are more networks without styles than predefined styles. Some styles will have to be reused!"
(
foldl' recursiveUpdate {} (imap0 (i: net: {
${net} = elemAt remainingStyles (mod i (length remainingStyles));
})
remainingNets)
);
in
f {
options.networks = mkOption {
@ -37,15 +122,20 @@ in
default = null;
};
color = mkOption {
description = "The color of this network";
default = "random";
apply = x:
if x == "random"
then "#ff00ff" # FIXME: TODO: lookuptable linear probing hashing into palette
else x;
type = types.either (types.strMatching "^#[0-9a-f]{6}$") (types.enum ["random"]);
};
style =
mkOption {
description = ''
A style for this network, usually used to draw connections.
Must be an attrset consisting of three attributes:
- primaryColor (#rrggbb): The primary color, usually the color of edges.
- secondaryColor (#rrggbb): The secondary color, usually the background of a dashed line and only shown when pattern != solid. Set to null for transparent.
- pattern (solid, dashed, dotted): The pattern to use.
'';
type = lazyOf types.attrs;
}
// optionalAttrs config.topology.isMainModule {
default = lazyValue computedStyles.${networkSubmod.config.id};
};
cidrv4 = mkOption {
description = "The CIDRv4 address space of this network or null if it doesn't use ipv4";

View file

@ -0,0 +1,42 @@
lib: let
inherit
(lib)
all
assertMsg
isAttrs
mkOptionType
showOption
types
;
# Checks whether the value is a lazy value without causing
# it's value to be evaluated
isLazyValue = x: isAttrs x && x ? _lazyValue;
# Constructs a lazy value holding the given value.
lazyValue = value: {_lazyValue = value;};
# Represents a lazy value of the given type, which
# holds the actual value as an attrset like { _lazyValue = <actual value>; }.
# This allows the option to be defined and filtered from a defintion
# list without evaluating the value.
lazyValueOf = type:
mkOptionType rec {
name = "lazyValueOf ${type.name}";
inherit (type) description descriptionClass emptyValue getSubOptions getSubModules;
check = isLazyValue;
merge = loc: defs:
assert assertMsg
(all (x: type.check x._lazyValue) defs)
"The option `${showOption loc}` is defined with a lazy value holding an invalid type";
types.mergeOneOption loc defs;
substSubModules = m: types.uniq (type.substSubModules m);
functor = (types.defaultFunctor name) // {wrapped = type;};
nestedTypes.elemType = type;
};
# Represents a value or lazy value of the given type that will
# automatically be coerced to the given type when merged.
lazyOf = type: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type;
in {
inherit isLazyValue lazyValue lazyValueOf lazyOf;
}

View file

@ -54,14 +54,17 @@
else value
);
mkEdge = from: to: extra: {
edges."${from}__${to}" =
{
sources = [from];
targets = [to];
}
// extra;
};
mkEdge = from: to: reverse: extra:
if reverse
then mkEdge to from false extra
else {
edges."${from}__${to}" =
{
sources = [from];
targets = [to];
}
// extra;
};
mkPort = recursiveUpdate {
width = 8;
@ -81,6 +84,32 @@
// extraStyle;
};
pathStyleFromNetworkStyle = style:
{
solid = {
stroke = style.primaryColor;
};
dashed =
{
stroke = style.primaryColor;
stroke-dasharray = "10,8";
stroke-linecap = "round";
}
// optionalAttrs (style.secondaryColor != null) {
background = style.secondaryColor;
};
dotted =
{
stroke = style.primaryColor;
stroke-dasharray = "2,6";
stroke-linecap = "round";
}
// optionalAttrs (style.secondaryColor != null) {
background = style.secondaryColor;
};
}
.${style.pattern};
netToElk = net: [
{
children.network.children."net:${net.id}" = {
@ -100,15 +129,18 @@
idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}";
nodeInterfaceToElk = node: interface: let
netStyle = optionalAttrs (interface.network != null) {
fill = config.networks.${interface.network}.style.primaryColor;
};
interfaceLabels =
{
"00-name" = mkLabel interface.id 1 {};
}
// optionalAttrs (interface.mac != null) {
"50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
"50-mac" = mkLabel interface.mac 1 netStyle;
}
// optionalAttrs (interface.addresses != []) {
"60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
"60-addrs" = mkLabel (toString interface.addresses) 1 netStyle;
};
in
[
@ -131,8 +163,8 @@
# Edge in network-centric view
(optionalAttrs (interface.network != null) (
mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" {
style.stroke = config.networks.${interface.network}.color;
mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" false {
style = pathStyleFromNetworkStyle config.networks.${interface.network}.style;
}
))
]
@ -149,10 +181,11 @@
mkEdge
(idForInterface node interface.id)
(idForInterface config.nodes.${conn.node} conn.interface)
conn.renderer.reverse
{
style = optionalAttrs (interface.network != null) {
stroke = config.networks.${interface.network}.color;
};
style = optionalAttrs (interface.network != null) (
pathStyleFromNetworkStyle config.networks.${interface.network}.style
);
}
)
)
@ -196,7 +229,7 @@
labels."00-name" = mkLabel "guests" 1 {};
};
}
// mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" {
// mkEdge "children.node:${node.parent}.ports.guests" "children.node:${node.id}" false {
style.stroke-dasharray = "10,8";
style.stroke-linecap = "round";
}

View file

@ -113,10 +113,18 @@
mkCard = net: {
width = 480;
html = let
netColor =
if net.color != null
then net.color
else "#b6beca";
netStylePreview = let
secondaryColor =
if net.style.secondaryColor == null
then "#00000000"
else net.style.secondaryColor;
in
{
solid = ''<div tw="flex flex-none bg-[${net.style.primaryColor}] w-8 h-4 mr-4 rounded-md"></div>'';
dashed = ''<div tw="flex flex-none w-8 h-4 mr-4 rounded-md" style="backgroundImage: linear-gradient(90deg, ${net.style.primaryColor} 0%, ${net.style.primaryColor} 50%, ${secondaryColor} 50.01%, ${secondaryColor} 100%);"></div>'';
dotted = ''<div tw="flex flex-none w-8 h-4 mr-4 rounded-md" style="backgroundImage: radial-gradient(circle, ${net.style.primaryColor} 30%, ${secondaryColor} 30.01%);"></div>'';
}
.${net.style.pattern};
in
mkCardContainer
/*
@ -124,7 +132,7 @@
*/
''
<div tw="flex flex-row mx-6 mt-2 items-center">
<div tw="flex flex-none bg-[${netColor}] w-8 h-8 mr-4 rounded-lg"></div>
${netStylePreview}
<h2 tw="text-2xl font-bold">${net.name}</h2>
<div tw="flex grow min-w-8"></div>
${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get net.icon)}