mirror of
https://github.com/oddlama/nix-config.git
synced 2025-10-11 07:10:39 +02:00
feat(topology): add automatic lazy network propagation
This commit is contained in:
parent
65f1fc4bd7
commit
dc4d82c828
9 changed files with 412 additions and 137 deletions
15
flake.nix
15
flake.nix
|
@ -207,6 +207,7 @@
|
||||||
nodes.fritzbox = {
|
nodes.fritzbox = {
|
||||||
name = "FritzBox";
|
name = "FritzBox";
|
||||||
deviceType = "router";
|
deviceType = "router";
|
||||||
|
hardware.info = "FRITZ!Box 7520";
|
||||||
hardware.image = ./fritzbox.png;
|
hardware.image = ./fritzbox.png;
|
||||||
# interfaces.wan0.network = "internet";
|
# interfaces.wan0.network = "internet";
|
||||||
interfaces.wan0 = {};
|
interfaces.wan0 = {};
|
||||||
|
@ -229,14 +230,20 @@
|
||||||
cidrv4 = "192.168.178.0/24";
|
cidrv4 = "192.168.178.0/24";
|
||||||
color = "#f1cf8a";
|
color = "#f1cf8a";
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes.ward.interfaces.lan.network = "home-lan";
|
nodes.ward.interfaces.lan.network = "home-lan";
|
||||||
nodes.ward.interfaces.wan.network = "home-fritzbox";
|
|
||||||
nodes.fritzbox.interfaces.eth0.network = "home-fritzbox";
|
nodes.fritzbox.interfaces.eth0.network = "home-fritzbox";
|
||||||
|
|
||||||
nodes.switch-attic = {
|
nodes.switch-attic = {
|
||||||
name = "Switch Attic";
|
name = "Switch Attic";
|
||||||
deviceType = "switch";
|
deviceType = "switch";
|
||||||
|
hardware.info = "D-Link DGS-1016D";
|
||||||
hardware.image = ./dlink-dgs1016d.png;
|
hardware.image = ./dlink-dgs1016d.png;
|
||||||
|
|
||||||
|
interfaces.eth0.sharesNetworkWith = _: true;
|
||||||
|
interfaces.eth1.sharesNetworkWith = _: true;
|
||||||
|
interfaces.eth2.sharesNetworkWith = _: true;
|
||||||
|
|
||||||
interfaces.eth0.physicalConnections = [
|
interfaces.eth0.physicalConnections = [
|
||||||
{
|
{
|
||||||
node = "ward";
|
node = "ward";
|
||||||
|
@ -255,7 +262,13 @@
|
||||||
nodes.switch-bedroom-1 = {
|
nodes.switch-bedroom-1 = {
|
||||||
name = "Switch Bedroom 1";
|
name = "Switch Bedroom 1";
|
||||||
deviceType = "switch";
|
deviceType = "switch";
|
||||||
|
hardware.info = "D-Link DGS-105";
|
||||||
hardware.image = ./dlink-dgs105.png;
|
hardware.image = ./dlink-dgs105.png;
|
||||||
|
|
||||||
|
interfaces.eth0.sharesNetworkWith = _: true;
|
||||||
|
interfaces.eth1.sharesNetworkWith = _: true;
|
||||||
|
interfaces.eth2.sharesNetworkWith = _: true;
|
||||||
|
|
||||||
interfaces.eth0.physicalConnections = [
|
interfaces.eth0.physicalConnections = [
|
||||||
{
|
{
|
||||||
node = "switch-attic";
|
node = "switch-attic";
|
||||||
|
|
|
@ -22,6 +22,27 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
topology.self.hardware.image = ../../odroid-h3.png;
|
topology.self.hardware.image = ../../odroid-h3.png;
|
||||||
|
topology.self.hardware.info = "ODROID H3, 64GB RAM";
|
||||||
|
# TODO FIXME topology bogus
|
||||||
|
topology.self.interfaces.lan-self.physicalConnections = [
|
||||||
|
{
|
||||||
|
node = config.node.name;
|
||||||
|
interface = "lan";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
topology.self.interfaces.lan.physicalConnections =
|
||||||
|
lib.flip map [
|
||||||
|
"adguardhome"
|
||||||
|
"forgejo"
|
||||||
|
"kanidm"
|
||||||
|
"radicale"
|
||||||
|
"vaultwarden"
|
||||||
|
] (
|
||||||
|
x: {
|
||||||
|
node = "ward-${x}";
|
||||||
|
interface = "lan";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
boot.mode = "efi";
|
boot.mode = "efi";
|
||||||
boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" "r8169"];
|
boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" "r8169"];
|
||||||
|
|
11
topology/README.md
Normal file
11
topology/README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
## Options
|
||||||
|
|
||||||
|
## Renderers
|
||||||
|
|
||||||
|
### svg
|
||||||
|
|
||||||
|
### elk
|
||||||
|
|
||||||
|
## NixOS Extractors
|
||||||
|
|
||||||
|
## Network propagation
|
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" viewBox="0 0 512 512"><path d="M168 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M256 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M212 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/><path d="M460 0H52C23.28 0 0 23.28 0 52v408c0 28.72 23.28 52 52 52h408c28.72 0 52-23.28 52-52V52c0-28.72-23.28-52-52-52m-16 284h-88.024c-2.212 0-3.976 1.64-3.976 3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h44z"/><path d="M124 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M388 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M344 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M300 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="800" height="800" viewBox="0 0 512 512"><path fill="#e3e6eb" d="M168 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M256 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M212 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/><path fill="#e3e6eb" d="M460 0H52C23.28 0 0 23.28 0 52v408c0 28.72 23.28 52 52 52h408c28.72 0 52-23.28 52-52V52c0-28.72-23.28-52-52-52m-16 284h-88.024c-2.212 0-3.976 1.64-3.976 3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h20v92.184c0 6.624 5.368 12 12 12 6.624 0 12-5.376 12-12V68h44z"/><path fill="#e3e6eb" d="M124 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M388 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M344 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12M300 184.036c-6.632 0-12 5.376-12 12v23.752c0 6.628 5.368 12 12 12 6.624 0 12-5.372 12-12v-23.752c0-6.624-5.376-12-12-12"/></svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -13,7 +13,6 @@
|
||||||
mkIf
|
mkIf
|
||||||
mkMerge
|
mkMerge
|
||||||
filter
|
filter
|
||||||
optionals
|
|
||||||
;
|
;
|
||||||
|
|
||||||
headOrNull = xs:
|
headOrNull = xs:
|
||||||
|
@ -49,7 +48,6 @@ in {
|
||||||
wgName: wgCfg: let
|
wgName: wgCfg: let
|
||||||
inherit
|
inherit
|
||||||
(lib.wireguard inputs wgName)
|
(lib.wireguard inputs wgName)
|
||||||
participatingClientNodes
|
|
||||||
participatingServerNodes
|
participatingServerNodes
|
||||||
wgCfgOf
|
wgCfgOf
|
||||||
;
|
;
|
||||||
|
@ -57,11 +55,6 @@ in {
|
||||||
isServer = wgCfg.server.host != null;
|
isServer = wgCfg.server.host != null;
|
||||||
filterSelf = filter (x: x != config.node.name);
|
filterSelf = filter (x: x != config.node.name);
|
||||||
|
|
||||||
# All nodes that use our node as the via into the wireguard network
|
|
||||||
ourClientNodes =
|
|
||||||
optionals isServer
|
|
||||||
(filter (n: (wgCfgOf n).client.via == config.node.name) participatingClientNodes);
|
|
||||||
|
|
||||||
# The list of peers that are "physically" connected in the wireguard network,
|
# The list of peers that are "physically" connected in the wireguard network,
|
||||||
# meaning they communicate directly with each other.
|
# meaning they communicate directly with each other.
|
||||||
connectedPeers =
|
connectedPeers =
|
||||||
|
@ -69,13 +62,12 @@ in {
|
||||||
then
|
then
|
||||||
# Other servers in the same network
|
# Other servers in the same network
|
||||||
filterSelf participatingServerNodes
|
filterSelf participatingServerNodes
|
||||||
# Our clients
|
|
||||||
++ ourClientNodes
|
|
||||||
else [wgCfg.client.via];
|
else [wgCfg.client.via];
|
||||||
in {
|
in {
|
||||||
${wgCfg.linkName} = {
|
${wgCfg.linkName} = {
|
||||||
network = networkId wgName;
|
network = networkId wgName;
|
||||||
virtual = true;
|
virtual = true;
|
||||||
|
renderer.hidePhysicalConnections = true;
|
||||||
physicalConnections = flip map connectedPeers (peer: {
|
physicalConnections = flip map connectedPeers (peer: {
|
||||||
node = inputs.self.nodes.${peer}.config.topology.id;
|
node = inputs.self.nodes.${peer}.config.topology.id;
|
||||||
interface = (wgCfgOf peer).linkName;
|
interface = (wgCfgOf peer).linkName;
|
||||||
|
|
|
@ -1,23 +1,231 @@
|
||||||
f: {
|
f: {
|
||||||
lib,
|
lib,
|
||||||
config,
|
config,
|
||||||
|
options,
|
||||||
...
|
...
|
||||||
}: let
|
}: let
|
||||||
inherit
|
inherit
|
||||||
(lib)
|
(lib)
|
||||||
|
all
|
||||||
|
assertMsg
|
||||||
|
attrNames
|
||||||
attrValues
|
attrValues
|
||||||
|
concatLines
|
||||||
|
concatMap
|
||||||
|
concatStringsSep
|
||||||
|
const
|
||||||
|
elem
|
||||||
flatten
|
flatten
|
||||||
flip
|
flip
|
||||||
|
foldl'
|
||||||
|
isAttrs
|
||||||
|
length
|
||||||
|
mapAttrsToList
|
||||||
mkDefault
|
mkDefault
|
||||||
mkIf
|
mkIf
|
||||||
mkOption
|
mkOption
|
||||||
|
mkOptionType
|
||||||
optional
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
recursiveUpdate
|
||||||
|
reverseList
|
||||||
|
showOption
|
||||||
types
|
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;
|
||||||
|
|
||||||
|
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
|
in
|
||||||
f {
|
f {
|
||||||
options.nodes = mkOption {
|
options.nodes = mkOption {
|
||||||
type = types.attrsOf (types.submodule {
|
type = types.attrsOf (types.submodule (nodeSubmod: {
|
||||||
options = {
|
options = {
|
||||||
interfaces = mkOption {
|
interfaces = mkOption {
|
||||||
description = "TODO";
|
description = "TODO";
|
||||||
|
@ -67,10 +275,38 @@ in
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
};
|
};
|
||||||
|
|
||||||
network = mkOption {
|
network =
|
||||||
description = "The id of the network to which this interface belongs, if any.";
|
mkOption {
|
||||||
default = null;
|
description = "The id of the network to which this interface belongs, if any.";
|
||||||
type = types.nullOr types.str;
|
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 {
|
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 = {
|
config = {
|
||||||
|
@ -101,10 +352,13 @@ in
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
lib.a.a = connections;
|
||||||
|
lib.a.b = networkDefiningInterfaces;
|
||||||
|
lib.a.c = propagatedNetworks;
|
||||||
assertions = flatten (flip map (attrValues config.nodes) (
|
assertions = flatten (flip map (attrValues config.nodes) (
|
||||||
node:
|
node:
|
||||||
flip map (attrValues node.interfaces) (
|
flip map (attrValues node.interfaces) (
|
||||||
|
|
|
@ -41,8 +41,8 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
hardware = {
|
hardware = {
|
||||||
description = mkOption {
|
info = mkOption {
|
||||||
description = "A description of this node's hardware. Usually the model name or a description the most important components.";
|
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;
|
type = types.str;
|
||||||
default = "";
|
default = "";
|
||||||
};
|
};
|
||||||
|
@ -64,7 +64,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
The device type of the node. This can be set to anything, but some special
|
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
|
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;
|
type = types.either (types.enum ["nixos" "internet" "router" "switch" "device"]) types.str;
|
||||||
};
|
};
|
||||||
|
@ -87,15 +87,18 @@ in
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
};
|
};
|
||||||
|
|
||||||
preferredRenderType = mkOption {
|
# Rendering related hints and settings
|
||||||
description = ''
|
renderer = {
|
||||||
An optional hint to the renderer to specify whether this node should preferrably
|
preferredType = mkOption {
|
||||||
rendered as a full card, or just as an image with name. If there is no hardware
|
description = ''
|
||||||
image, this will usually still render a small card.
|
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
|
||||||
type = types.enum ["card" "image"];
|
image, this will usually still render a small card.
|
||||||
default = "card";
|
'';
|
||||||
defaultText = ''"card" # defaults to card but is also derived from the deviceType if possible.'';
|
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;
|
nodeCfg = nodeSubmod.config;
|
||||||
in
|
in
|
||||||
mkIf config.topology.isMainModule (mkMerge [
|
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}) (
|
deviceIcon = mkIf (config.icons.devices ? ${nodeCfg.deviceType}) (
|
||||||
mkDefault ("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.
|
# 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"]) {
|
(mkIf (elem nodeCfg.deviceType ["internet" "router" "switch" "device"]) {
|
||||||
preferredRenderType = mkDefault "image";
|
renderer.preferredType = mkDefault "image";
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -20,7 +20,6 @@
|
||||||
mkOption
|
mkOption
|
||||||
optional
|
optional
|
||||||
optionalAttrs
|
optionalAttrs
|
||||||
optionals
|
|
||||||
recursiveUpdate
|
recursiveUpdate
|
||||||
stringLength
|
stringLength
|
||||||
types
|
types
|
||||||
|
@ -64,6 +63,13 @@
|
||||||
// extra;
|
// extra;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mkPort = recursiveUpdate {
|
||||||
|
width = 8;
|
||||||
|
height = 8;
|
||||||
|
style.stroke = "#485263";
|
||||||
|
style.fill = "#b6beca";
|
||||||
|
};
|
||||||
|
|
||||||
mkLabel = text: scale: extraStyle: {
|
mkLabel = text: scale: extraStyle: {
|
||||||
height = scale * 12;
|
height = scale * 12;
|
||||||
width = scale * 7.2 * (stringLength text);
|
width = scale * 7.2 * (stringLength text);
|
||||||
|
@ -83,82 +89,72 @@
|
||||||
scale = 0.8;
|
scale = 0.8;
|
||||||
};
|
};
|
||||||
properties."portLabels.placement" = "OUTSIDE";
|
properties."portLabels.placement" = "OUTSIDE";
|
||||||
|
|
||||||
|
ports.default = mkPort {
|
||||||
|
labels."00-name" = mkLabel "*" 1 {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
idForInterface = node: interfaceId: "children.node:${node.id}.ports.interface:${interfaceId}";
|
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
|
# Interface for node in main view
|
||||||
{
|
{
|
||||||
children."node:${node.id}".ports."interface:${interface.id}" = {
|
children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
|
||||||
properties = optionalAttrs (node.preferredRenderType == "card") {
|
properties = optionalAttrs (node.renderer.preferredType == "card") {
|
||||||
"port.side" = "WEST";
|
"port.side" = "WEST";
|
||||||
};
|
};
|
||||||
width = 8;
|
labels = interfaceLabels;
|
||||||
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";};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# Interface for node in network-centric view
|
# Interface for node in network-centric view
|
||||||
{
|
{
|
||||||
children.network.children."node:${node.id}".ports."interface:${interface.id}" = {
|
children.network.children."node:${node.id}".ports."interface:${interface.id}" = mkPort {
|
||||||
# FIXME: TODO: deduplicate, same as above
|
labels = interfaceLabels;
|
||||||
# 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";};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
# Edge in network-centric view
|
# Edge in network-centric view
|
||||||
(optionalAttrs (interface.network != null) (
|
(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;
|
style.stroke = config.networks.${interface.network}.color;
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
]
|
]
|
||||||
++ optionals (!interface.virtual) (flip map interface.physicalConnections (
|
++ flatten (flip map interface.physicalConnections (
|
||||||
conn:
|
conn: let
|
||||||
|
otherInterface = config.nodes.${conn.node}.interfaces.${conn.interface};
|
||||||
|
in
|
||||||
optionalAttrs (
|
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)
|
|| (node.id < conn.node)
|
||||||
) (
|
) (
|
||||||
# Edge in main view
|
optional (!interface.renderer.hidePhysicalConnections && !otherInterface.renderer.hidePhysicalConnections) (
|
||||||
mkEdge
|
# Edge in main view
|
||||||
(idForInterface node interface.id)
|
mkEdge
|
||||||
(idForInterface config.nodes.${conn.node} conn.interface)
|
(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) {
|
style = optionalAttrs (interface.network != null) {
|
||||||
stroke = config.networks.${interface.network}.color;
|
stroke = config.networks.${interface.network}.color;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -175,7 +171,7 @@
|
||||||
{
|
{
|
||||||
"portLabels.placement" = "OUTSIDE";
|
"portLabels.placement" = "OUTSIDE";
|
||||||
}
|
}
|
||||||
// optionalAttrs (node.preferredRenderType == "card") {
|
// optionalAttrs (node.renderer.preferredType == "card") {
|
||||||
"portConstraints" = "FIXED_SIDE";
|
"portConstraints" = "FIXED_SIDE";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -193,10 +189,8 @@
|
||||||
]
|
]
|
||||||
++ optional (node.parent != null) (
|
++ optional (node.parent != null) (
|
||||||
{
|
{
|
||||||
children."node:${node.parent}".ports.guests = {
|
children."node:${node.parent}".ports.guests = mkPort {
|
||||||
properties."port.side" = "EAST";
|
properties."port.side" = "EAST";
|
||||||
width = 8;
|
|
||||||
height = 8;
|
|
||||||
style.stroke = "#49d18d";
|
style.stroke = "#49d18d";
|
||||||
style.fill = "#78dba9";
|
style.fill = "#78dba9";
|
||||||
labels."00-name" = mkLabel "guests" 1 {};
|
labels."00-name" = mkLabel "guests" 1 {};
|
||||||
|
|
|
@ -1,27 +1,20 @@
|
||||||
# TODO:
|
# 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)
|
# - address port label render make newline capable (multiple port labels)
|
||||||
# - mac address show!
|
|
||||||
# - split network layout or make rectpacking of childs
|
|
||||||
# - NAT indication
|
# - NAT indication
|
||||||
# - bottom hw image distorted in card view (move to top anyway)
|
|
||||||
# - embed font globally, try removing satori embed?
|
# - embed font globally, try removing satori embed?
|
||||||
# - network overview card (list all networks with name and cidr, legend style)
|
# - network overview card (list all networks with name and cidr, legend style)
|
||||||
# - colors!
|
# - stable pseudorandom colors from palette with no-reuse until necessary
|
||||||
# - ip labels on edges
|
# - network centric view as standalone
|
||||||
# - network centric view
|
# - split network layout or make rectpacking of childs
|
||||||
# - better layout for interfaces in svg
|
|
||||||
# - sevice infos
|
|
||||||
# - disks (from disko) + render
|
|
||||||
# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position)
|
# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position)
|
||||||
# - more service info
|
# - more service info
|
||||||
|
# - disks (from disko) + render
|
||||||
# - impermanence render?
|
# - impermanence render?
|
||||||
# - nixos nftables firewall render?
|
# - nixos nftables firewall render?
|
||||||
# - stable pseudorandom colors from palette with no-reuse until necessary
|
|
||||||
# - search todo and do
|
|
||||||
# - podman / docker harvesting
|
# - podman / docker harvesting
|
||||||
|
# - systemd extractor remove cidr mask
|
||||||
# - nixos-container extractor
|
# - nixos-container extractor
|
||||||
|
# - search todo and do
|
||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
|
@ -144,24 +137,16 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
node = rec {
|
node = rec {
|
||||||
mkInterface = interface: let
|
mkInterface = interface:
|
||||||
color =
|
/*
|
||||||
if interface.virtual
|
html
|
||||||
then "#7a899f"
|
*/
|
||||||
else "#70a5eb";
|
''
|
||||||
in
|
<div tw="flex flex-col flex-none items-center border-[#21262e] border-2 rounded-lg px-2 py-1 m-1">
|
||||||
/*
|
${mkImage "w-8 h-8 m-1" (config.lib.icons.get interface.icon)}
|
||||||
html
|
<span tw="font-bold text-xs">${interface.id}</span>
|
||||||
*/
|
</div>
|
||||||
''
|
'';
|
||||||
<div tw="flex flex-row items-center my-2">
|
|
||||||
<div tw="flex flex-row flex-none items-center bg-[${color}] text-[#101419] rounded-lg px-2 py-1 w-46 h-8 mx-4">
|
|
||||||
${mkImage "w-6 h-6 mr-2" (config.lib.icons.get interface.icon)}
|
|
||||||
<span tw="font-bold">${interface.id}</span>
|
|
||||||
</div>
|
|
||||||
<span>${toString interface.addresses}</span>
|
|
||||||
</div>
|
|
||||||
'';
|
|
||||||
|
|
||||||
serviceDetail = detail:
|
serviceDetail = detail:
|
||||||
/*
|
/*
|
||||||
|
@ -217,19 +202,6 @@
|
||||||
</div>
|
</div>
|
||||||
'';
|
'';
|
||||||
|
|
||||||
mkTitle = node:
|
|
||||||
/*
|
|
||||||
html
|
|
||||||
*/
|
|
||||||
''
|
|
||||||
<div tw="flex flex-row mx-6 mt-2 items-center">
|
|
||||||
${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-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
|
|
||||||
</div>
|
|
||||||
'';
|
|
||||||
|
|
||||||
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);
|
guests = filter (x: x.parent == node.id) (attrValues config.nodes);
|
||||||
|
@ -241,18 +213,25 @@
|
||||||
html
|
html
|
||||||
*/
|
*/
|
||||||
''
|
''
|
||||||
${mkTitle node}
|
<div tw="flex flex-row mx-6 mt-2 items-center">
|
||||||
|
${mkImageMaybe "w-8 h-8 mr-4" (config.lib.icons.get node.icon)}
|
||||||
|
<div tw="flex flex-col min-h-18 justify-center">
|
||||||
|
<span tw="text-2xl font-bold">${node.name}</span>
|
||||||
|
${optionalString (node.hardware.info != null) ''<span tw="text-xs">${node.hardware.info}</span>''}
|
||||||
|
</div>
|
||||||
|
<div tw="flex grow min-w-8"></div>
|
||||||
|
${mkImageMaybe "w-12 h-12 ml-4" (config.lib.icons.get node.deviceIcon)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${optionalString (node.interfaces != {}) ''<div tw="flex flex-row flex-wrap items-center my-2 mx-3">''}
|
||||||
${concatLines (map mkInterface (attrValues node.interfaces))}
|
${concatLines (map mkInterface (attrValues node.interfaces))}
|
||||||
${optionalString (node.interfaces != {}) spacingMt2}
|
${optionalString (node.interfaces != {}) ''</div>''}
|
||||||
|
|
||||||
${concatLines (map mkGuest guests)}
|
${concatLines (map mkGuest guests)}
|
||||||
${optionalString (guests != []) spacingMt2}
|
${optionalString (guests != []) spacingMt2}
|
||||||
|
|
||||||
${concatLines (map (mkService {}) services)}
|
${concatLines (map (mkService {}) services)}
|
||||||
${optionalString (services != []) spacingMt2}
|
${optionalString (services != []) spacingMt2}
|
||||||
|
|
||||||
${mkImageMaybe "w-full h-24" node.hardware.image}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -267,21 +246,26 @@
|
||||||
''
|
''
|
||||||
<div tw="flex flex-row mx-6 mt-2 items-center">
|
<div tw="flex flex-row mx-6 mt-2 items-center">
|
||||||
${mkImageMaybe "w-8 h-8" (config.lib.icons.get node.icon)}
|
${mkImageMaybe "w-8 h-8" (config.lib.icons.get node.icon)}
|
||||||
<h2 tw="text-2xl font-bold">${node.name}</h2>
|
<div tw="flex flex-col min-h-18 justify-center">
|
||||||
${optionalString (node.hardware.image != null -> deviceIconImage != node.hardware.image)
|
<span tw="text-2xl font-bold">${node.name}</span>
|
||||||
|
${optionalString (node.hardware.info != null) ''<span tw="text-xs">${node.hardware.info}</span>''}
|
||||||
|
</div>
|
||||||
|
${optionalString (deviceIconImage != null && node.hardware.image != null -> deviceIconImage != node.hardware.image)
|
||||||
''
|
''
|
||||||
<div tw="flex grow min-w-4"></div>
|
<div tw="flex grow min-w-4"></div>
|
||||||
${mkImageMaybe "w-12 h-12" deviceIconImage}
|
${mkImageMaybe "w-12 h-12" deviceIconImage}
|
||||||
''}
|
''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${mkImageMaybe "h-24" node.hardware.image}
|
<div tw="flex flex-row w-full justify-center">
|
||||||
|
${mkImageMaybe "h-24" node.hardware.image}
|
||||||
|
</div>
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
mkPreferredRender = node:
|
mkPreferredRender = node:
|
||||||
(
|
(
|
||||||
if node.preferredRenderType == "image" && node.hardware.image != null
|
if node.renderer.preferredType == "image" && node.hardware.image != null
|
||||||
then mkImageWithName
|
then mkImageWithName
|
||||||
else mkCard
|
else mkCard
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue