; }.
+ # 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;
+
+ allNodes = attrNames config.nodes;
+ allInterfacesOf = node: attrNames config.nodes.${node}.interfaces;
+ allNodeInterfacePairs = concatMap (node: map (interface: {inherit node interface;}) (allInterfacesOf node)) allNodes;
+
+ # Helper to add a value to a list if it doesn't already hold it
+ addIfNotExists = list: x:
+ if elem x list
+ then list
+ else list ++ [x];
+
+ # The list of networks that were specifically assigned by the user
+ # to other interfaces which are sharing their network with us.
+ networkDefiningInterfaces = foldl' recursiveUpdate {} (flatten (
+ flip map options.nodes.definitions (mapAttrsToList (
+ nodeId: node:
+ flip mapAttrsToList (node.interfaces or {}) (
+ interfaceId: interface:
+ optional (interface ? network && !isLazyValue interface.network) {
+ ${nodeId}.${interfaceId} = interface.network;
+ }
+ )
+ ))
+ ));
+
+ # A list of all connections between all interfaces. Bidirectional.
+ connectionList = unique (flatten (flip mapAttrsToList config.nodes (
+ nodeId: node:
+ flip mapAttrsToList node.interfaces (
+ interfaceId: interface:
+ flip map interface.physicalConnections (conn: [
+ {
+ src = {
+ node = nodeId;
+ interface = interfaceId;
+ };
+ dst = conn;
+ }
+ {
+ src = conn;
+ dst = {
+ node = nodeId;
+ interface = interfaceId;
+ };
+ }
+ ])
+ )
+ )));
+
+ # A map of all connections constructed from the connection list
+ connections = foldl' (acc: {
+ src,
+ dst,
+ }:
+ recursiveUpdate acc {
+ ${src.node}.${src.interface} = addIfNotExists (acc.${src.node}.${src.interface} or []) dst;
+ }) {}
+ connectionList;
+
+ # Propagates all networks from the source interface to the destination interface
+ propagateNetworks = state: snode: sinterface: dnode: dinterface:
+ #builtins.trace " propagate all nets (${toString (attrNames state.${snode}.${sinterface})}) of ${snode}.${sinterface} to ${dnode}.${dinterface}"
+ (
+ # Fold over each network that the source interface shares
+ # and propagate it if the destination doesn't already have this network
+ foldl' (
+ acc: net:
+ recursiveUpdate acc (
+ optionalAttrs (!(acc ? ${dnode}.${dinterface}.${net}))
+ #builtins.trace " adding ${net} to ${dnode}.${dinterface} via ${toString (acc.${snode}.${sinterface}.${net} ++ ["${snode}.${sinterface}"])}"
+ {
+ # Create the network on the destination interface and append our interface
+ # to the list which indicates from where the network was received
+ ${dnode}.${dinterface}.${net} = acc.${snode}.${sinterface}.${net} ++ ["${snode}.${sinterface}"];
+ }
+ )
+ )
+ state (attrNames state.${snode}.${sinterface})
+ );
+
+ # Propagates all shared networks for the given interface to other interfaces connected to it
+ propagateToConnections = state: src:
+ #builtins.trace "propagate via connections of ${src.node}.${src.interface}"
+ (
+ # Fold over each connection of the current interface
+ foldl' (
+ acc: dst:
+ propagateNetworks acc src.node src.interface dst.node dst.interface
+ )
+ state (connections.${src.node}.${src.interface} or [])
+ );
+
+ # Propagates all shared networks for the given interface to other interfaces on the same
+ # node to which the network is shared
+ propagateLocally = state: src:
+ #builtins.trace "propagate locally on ${src.node}.${src.interface}"
+ (
+ # Fold over each local interface of the current node
+ foldl' (
+ acc: dstInterface:
+ if src.interface != dstInterface && config.nodes.${src.node}.interfaces.${src.interface}.sharesNetworkWith dstInterface
+ then propagateNetworks acc src.node src.interface src.node dstInterface
+ else acc
+ )
+ state (allInterfacesOf src.node)
+ );
+
+ # Assigns each interface a list of networks that were propagated to it
+ # from another interface or connection.
+ propagatedNetworks = let
+ # Initially set sharedNetworks based on whether the interface has a network assigned to it.
+ # This list will then be expaned iteratively.
+ initial = foldl' recursiveUpdate {} (flatten (flip map allNodes (
+ node:
+ flip map (allInterfacesOf node) (interface: {
+ ${node}.${interface} = optionalAttrs (networkDefiningInterfaces ? ${node}.${interface}) {
+ ${networkDefiningInterfaces.${node}.${interface}} = [];
+ };
+ })
+ )));
+
+ # Takes a state and propagates networks via local sharing on the same node
+ propagateEachLocally = state: foldl' propagateLocally state allNodeInterfacePairs;
+ # Takes a state and propagates networks via sharing over connections
+ propagateEachToConnections = state: foldl' propagateToConnections state allNodeInterfacePairs;
+
+ # The update function that propagates state from all interfaces to all neighbors.
+ # The fixpoint of this function is the solution.
+ update = state: propagateEachToConnections (propagateEachLocally state);
+ converged = lib.converge update initial;
+
+ # Extract all interfaces that were assigned multiple interfaces, to issue a warning
+ interfacesWithMultipleNets = flatten (
+ flip mapAttrsToList converged (
+ node: ifs:
+ flip mapAttrsToList ifs (
+ interface: nets:
+ optional (length (attrNames nets) > 1) {
+ inherit node interface nets;
+ }
+ )
+ )
+ );
+ in
+ warnIf (interfacesWithMultipleNets != []) ''
+ topology: Some interfaces have received multiple networks via network propagation!
+ This is an error in your network configuration that must be addressed.
+ Evaluation can still continue by considering only one of them (effectively at random).
+ The affected interfaces are:
+ ${concatLines (flip map interfacesWithMultipleNets (
+ x:
+ " - ${x.node}.${x.interface}:\n"
+ + concatLines (
+ flip mapAttrsToList x.nets (
+ net: assignments: " ${net} assigned via ${concatStringsSep " -> " (reverseList assignments)}"
+ )
+ )
+ ))}''
+ converged;
in
f {
options.nodes = mkOption {
- type = types.attrsOf (types.submodule {
+ type = types.attrsOf (types.submodule (nodeSubmod: {
options = {
interfaces = mkOption {
description = "TODO";
@@ -67,10 +275,38 @@ in
type = types.listOf types.str;
};
- network = mkOption {
- description = "The id of the network to which this interface belongs, if any.";
- default = null;
- type = types.nullOr types.str;
+ network =
+ mkOption {
+ description = "The id of the network to which this interface belongs, if any.";
+ type = lazyOf (types.nullOr types.str);
+ }
+ // optionalAttrs config.topology.isMainModule {
+ default = let
+ sharedNetworks = propagatedNetworks.${nodeSubmod.config.id}.${submod.config.id} or {};
+ sharedNetwork =
+ if sharedNetworks == {}
+ then null
+ else builtins.head (attrNames sharedNetworks);
+ in
+ lazyValue sharedNetwork;
+ };
+
+ sharesNetworkWith = mkOption {
+ description = ''
+ Defines a predicate that determines whether this interface shares its connected network with another provided local interface.
+ The predicates takes the name of another interface and returns true if our network should be shared with the given interface.
+
+ Sharing here means that if a network is set on this interface, it will also be set as the network for any
+ shared interface. Setting the same predicate on multiple interfaces causes them to share a network regardless
+ on which port the network is actually defined.
+
+ An unmanaged switch for example would set this to `const true`, effectively
+ propagating the network set on one port to all other ports. Having two assigned
+ networks within one predicate group will cause a warning to be issued.
+ '';
+ default = const false;
+ defaultText = ''const false'';
+ type = types.functionTo types.bool;
};
physicalConnections = mkOption {
@@ -90,6 +326,21 @@ in
};
});
};
+
+ # Rendering related hints and settings
+ renderer = {
+ hidePhysicalConnections = mkOption {
+ description = ''
+ Whether to hide physical connections of this interface in renderings.
+ Affects both outgoing connections defined here and incoming connections
+ defined on other interfaces.
+
+ Usually only affects rendering of the main topology view, not network-centric views.
+ '';
+ type = types.bool;
+ default = false;
+ };
+ };
};
config = {
@@ -101,10 +352,13 @@ in
}));
};
};
- });
+ }));
};
config = {
+ lib.a.a = connections;
+ lib.a.b = networkDefiningInterfaces;
+ lib.a.c = propagatedNetworks;
assertions = flatten (flip map (attrValues config.nodes) (
node:
flip map (attrValues node.interfaces) (
diff --git a/topology/options/nodes.nix b/topology/options/nodes.nix
index 1d2f5a4..fbe3c0c 100644
--- a/topology/options/nodes.nix
+++ b/topology/options/nodes.nix
@@ -41,8 +41,8 @@ in
};
hardware = {
- description = mkOption {
- description = "A description of this node's hardware. Usually the model name or a description the most important components.";
+ info = mkOption {
+ description = "A single line of information about this node's hardware. Usually the model name or a description the most important components.";
type = types.str;
default = "";
};
@@ -64,7 +64,7 @@ in
description = ''
The device type of the node. This can be set to anything, but some special
values exist that will automatically set some other defaults, most notably
- the deviceIcon and preferredRenderType.
+ the deviceIcon and renderer.preferredType.
'';
type = types.either (types.enum ["nixos" "internet" "router" "switch" "device"]) types.str;
};
@@ -87,15 +87,18 @@ in
type = types.nullOr types.str;
};
- preferredRenderType = mkOption {
- description = ''
- An optional hint to the renderer to specify whether this node should preferrably
- rendered as a full card, or just as an image with name. If there is no hardware
- image, this will usually still render a small card.
- '';
- type = types.enum ["card" "image"];
- default = "card";
- defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
+ # Rendering related hints and settings
+ renderer = {
+ preferredType = mkOption {
+ description = ''
+ An optional hint to the renderer to specify whether this node should preferrably
+ rendered as a full card, or just as an image with name. If there is no hardware
+ image, this will usually still render a small card.
+ '';
+ type = types.enum ["card" "image"];
+ default = "card";
+ defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
+ };
};
};
@@ -103,16 +106,19 @@ in
nodeCfg = nodeSubmod.config;
in
mkIf config.topology.isMainModule (mkMerge [
- # Set the default icon, if an icon exists with a matching name
{
+ # Set the default icon, if an icon exists with a matching name
deviceIcon = mkIf (config.icons.devices ? ${nodeCfg.deviceType}) (
mkDefault ("devices." + nodeCfg.deviceType)
);
+
+ # Set the hardware info to the guest type if nothing else was set
+ hardware.info = mkIf (nodeCfg.guestType != null) (mkDefault nodeCfg.guestType);
}
# If the device type is not a full nixos node, try to render it as an image with name.
(mkIf (elem nodeCfg.deviceType ["internet" "router" "switch" "device"]) {
- preferredRenderType = mkDefault "image";
+ renderer.preferredType = mkDefault "image";
})
]);
}));
diff --git a/topology/topology/renderers/elk/default.nix b/topology/topology/renderers/elk/default.nix
index 65d5cda..81b1c21 100644
--- a/topology/topology/renderers/elk/default.nix
+++ b/topology/topology/renderers/elk/default.nix
@@ -20,7 +20,6 @@
mkOption
optional
optionalAttrs
- optionals
recursiveUpdate
stringLength
types
@@ -64,6 +63,13 @@
// extra;
};
+ mkPort = recursiveUpdate {
+ width = 8;
+ height = 8;
+ style.stroke = "#485263";
+ style.fill = "#b6beca";
+ };
+
mkLabel = text: scale: extraStyle: {
height = scale * 12;
width = scale * 7.2 * (stringLength text);
@@ -83,82 +89,72 @@
scale = 0.8;
};
properties."portLabels.placement" = "OUTSIDE";
+
+ ports.default = mkPort {
+ labels."00-name" = mkLabel "*" 1 {};
+ };
};
}
];
idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}";
- nodeInterfaceToElk = node: interface:
+ nodeInterfaceToElk = node: interface: let
+ interfaceLabels =
+ {
+ "00-name" = mkLabel interface.id 1 {};
+ }
+ // optionalAttrs (interface.mac != null) {
+ "50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
+ }
+ // optionalAttrs (interface.addresses != []) {
+ "60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
+ };
+ in
[
# Interface for node in main view
{
- children."node:${node.id}".ports."interface:${interface.id}" = {
- properties = optionalAttrs (node.preferredRenderType == "card") {
+ children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
+ properties = optionalAttrs (node.renderer.preferredType == "card") {
"port.side" = "WEST";
};
- width = 8;
- height = 8;
- style.stroke = "#485263"; # FIXME: TODO color based on attached network color (autoshade)?
- style.fill = "#b6beca";
- labels =
- {
- "00-name" = mkLabel interface.id 1 {};
- }
- // optionalAttrs (interface.mac != null) {
- "50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
- }
- // optionalAttrs (interface.addresses != []) {
- "60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
- };
+ labels = interfaceLabels;
};
}
+
# Interface for node in network-centric view
{
- children.network.children."node:${node.id}".ports."interface:${interface.id}" = {
- # FIXME: TODO: deduplicate, same as above
- # FIXME: TODO: deduplicate, same as above
- # FIXME: TODO: deduplicate, same as above
- width = 8;
- height = 8;
- style.stroke = "#485263"; # FIXME: TODO color based on attached network color (autoshade)?
- style.fill = "#b6beca";
- labels =
- {
- "00-name" = mkLabel interface.id 1 {};
- }
- // optionalAttrs (interface.mac != null) {
- "50-mac" = mkLabel interface.mac 1 {fill = "#70a5eb";};
- }
- // optionalAttrs (interface.addresses != []) {
- "60-addrs" = mkLabel (toString interface.addresses) 1 {fill = "#f9a872";};
- };
+ children.network.children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
+ labels = interfaceLabels;
};
}
# Edge in network-centric view
(optionalAttrs (interface.network != null) (
- mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}" {
+ mkEdge ("children.network." + idForInterface node interface.id) "children.network.children.net:${interface.network}.ports.default" {
style.stroke = config.networks.${interface.network}.color;
}
))
]
- ++ optionals (!interface.virtual) (flip map interface.physicalConnections (
- conn:
+ ++ flatten (flip map interface.physicalConnections (
+ conn: let
+ otherInterface = config.nodes.${conn.node}.interfaces.${conn.interface};
+ in
optionalAttrs (
- (!any (y: y.node == node.id && y.interface == interface.id) config.nodes.${conn.node}.interfaces.${conn.interface}.physicalConnections)
+ (!any (y: y.node == node.id && y.interface == interface.id) otherInterface.physicalConnections)
|| (node.id < conn.node)
) (
- # Edge in main view
- mkEdge
- (idForInterface node interface.id)
- (idForInterface config.nodes.${conn.node} conn.interface)
- {
- # FIXME: in interface definition ensure that the two ends of physical connections don't have different networks (output warning only)
- style = optionalAttrs (interface.network != null) {
- stroke = config.networks.${interface.network}.color;
- };
- }
+ optional (!interface.renderer.hidePhysicalConnections && !otherInterface.renderer.hidePhysicalConnections) (
+ # Edge in main view
+ mkEdge
+ (idForInterface node interface.id)
+ (idForInterface config.nodes.${conn.node} conn.interface)
+ {
+ style = optionalAttrs (interface.network != null) {
+ stroke = config.networks.${interface.network}.color;
+ };
+ }
+ )
)
));
@@ -175,7 +171,7 @@
{
"portLabels.placement" = "OUTSIDE";
}
- // optionalAttrs (node.preferredRenderType == "card") {
+ // optionalAttrs (node.renderer.preferredType == "card") {
"portConstraints" = "FIXED_SIDE";
};
};
@@ -193,10 +189,8 @@
]
++ optional (node.parent != null) (
{
- children."node:${node.parent}".ports.guests = {
+ children."node:${node.parent}".ports.guests = mkPort {
properties."port.side" = "EAST";
- width = 8;
- height = 8;
style.stroke = "#49d18d";
style.fill = "#78dba9";
labels."00-name" = mkLabel "guests" 1 {};
diff --git a/topology/topology/renderers/svg/default.nix b/topology/topology/renderers/svg/default.nix
index c54a104..0bbc5bc 100644
--- a/topology/topology/renderers/svg/default.nix
+++ b/topology/topology/renderers/svg/default.nix
@@ -1,27 +1,20 @@
# TODO:
-# - stack interfaces horizontally, information is now on the ports anyway.
-# - systemd extractor remove cidr mask
# - address port label render make newline capable (multiple port labels)
-# - mac address show!
-# - split network layout or make rectpacking of childs
# - NAT indication
-# - bottom hw image distorted in card view (move to top anyway)
# - embed font globally, try removing satori embed?
# - network overview card (list all networks with name and cidr, legend style)
-# - colors!
-# - ip labels on edges
-# - network centric view
-# - better layout for interfaces in svg
-# - sevice infos
-# - disks (from disko) + render
+# - stable pseudorandom colors from palette with no-reuse until necessary
+# - network centric view as standalone
+# - split network layout or make rectpacking of childs
# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position)
# - more service info
+# - disks (from disko) + render
# - impermanence render?
# - nixos nftables firewall render?
-# - stable pseudorandom colors from palette with no-reuse until necessary
-# - search todo and do
# - podman / docker harvesting
+# - systemd extractor remove cidr mask
# - nixos-container extractor
+# - search todo and do
{
config,
lib,
@@ -144,24 +137,16 @@
};
node = rec {
- mkInterface = interface: let
- color =
- if interface.virtual
- then "#7a899f"
- else "#70a5eb";
- in
- /*
- html
- */
- ''
-
-
- ${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
- ${interface.id}
-
-
${toString interface.addresses}
-
- '';
+ mkInterface = interface:
+ /*
+ html
+ */
+ ''
+
+ ${mkImage "w-8 h-8 m-1" (config.lib.icons.get interface.icon)}
+ ${interface.id}
+
+ '';
serviceDetail = detail:
/*
@@ -217,19 +202,6 @@
'';
- mkTitle = node:
- /*
- html
- */
- ''
-
- ${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
-
${node.name}
-
- ${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
-
- '';
-
mkCard = node: let
services = filter (x: !x.hidden) (attrValues node.services);
guests = filter (x: x.parent == node.id) (attrValues config.nodes);
@@ -241,18 +213,25 @@
html
*/
''
- ${mkTitle node}
+
+ ${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
+
+ ${node.name}
+ ${optionalString (node.hardware.info != null) ''${node.hardware.info}''}
+
+
+ ${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
+
+ ${optionalString (node.interfaces != {}) ''''}
${concatLines (map mkInterface (attrValues node.interfaces))}
- ${optionalString (node.interfaces != {}) spacingMt2}
+ ${optionalString (node.interfaces != {}) ''
''}
${concatLines (map mkGuest guests)}
${optionalString (guests != []) spacingMt2}
${concatLines (map (mkService {}) services)}
${optionalString (services != []) spacingMt2}
-
- ${mkImageMaybe "w-full h-24" node.hardware.image}
'';
};
@@ -267,21 +246,26 @@
''
${mkImageMaybe "w-8 h-8" (config.lib.icons.get node.icon)}
-
${node.name}
- ${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image)
+
+ ${node.name}
+ ${optionalString (node.hardware.info != null) ''${node.hardware.info}''}
+
+ ${optionalString (deviceIconImage != null && node.hardware.image != null -> deviceIconImage != node.hardware.image)
''
${mkImageMaybe "w-12 h-12" deviceIconImage}
''}
- ${mkImageMaybe "h-24" node.hardware.image}
+
+ ${mkImageMaybe "h-24" node.hardware.image}
+
'';
};
mkPreferredRender = node:
(
- if node.preferredRenderType == "image" && node.hardware.image != null
+ if node.renderer.preferredType == "image" && node.hardware.image != null
then mkImageWithName
else mkCard
)