{ config, pkgs, ... }: let inherit (pkgs) lib runCommand graphviz; netColor = net: if net == "core" then "grey" else if net == "mgmt" then "brown" else if builtins.elem net [ "c3d2" "serv" "cluster" ] then "green" else if builtins.match "up.+" net != null || builtins.match "anon.+" net != null then "red" else if builtins.match "priv.+" net != null then "blue" else "black"; dedupLinks = links: builtins.attrValues ( builtins.foldl' (result: { pair, attrs ? {}, startLabel ? "" }: let peer1 = builtins.elemAt pair 0; peer2 = builtins.elemAt pair 1; sorted = if peer1 < peer2 then { from = peer1; to = peer2; } else { from = peer2; to = peer1; }; key = with sorted; "${from} ${to}"; prevAttrs = if result ? ${key} then result.${key}.attrs else {}; newAttrs = attrs // ( if peer1 < peer2 then { taillabel = startLabel; } else { headlabel = startLabel; } ); in result // { "${key}" = { inherit (sorted) from to; attrs = prevAttrs // newAttrs; }; } ) {} links ); toDot = { nodes, links, ... }: '' digraph { graph [splines=ortho, nodesep=64] node [] edge [arrowhead=none, labelfontsize=8] ${lib.concatMapStrings (name: let nodeAttrs = nodes.${name}; in '' "${name}" [${lib.concatMapStringsSep ", " (attr: let value = nodeAttrs.${attr}; in if builtins.isString value then "${attr}=\"${value}\"" else "${attr}=${toString value}" ) (builtins.attrNames nodeAttrs)}] '') (builtins.attrNames nodes)} ${lib.concatMapStrings ({ from, to, attrs }: '' "${from}" -> "${to}" [${lib.concatMapStringsSep ", " (attr: let value = attrs.${attr}; in if builtins.isString value then "${attr}=\"${value}\"" else "${attr}=${toString value}" ) (builtins.attrNames attrs)}] '') (dedupLinks links)} } ''; renderGraph = args@{ name, engine, ... }: runCommand "${name}.png" { src = builtins.toFile "${name}.dot" ( toDot args ); } '' echo $src ${graphviz}/bin/${engine} -Tpng $src > $out ''; in rec { # Layer 2 physical-graph = renderGraph { name = "physical"; engine = "fdp"; nodes = builtins.mapAttrs (_: { role, ... }: { shape = { switch = "box"; server = "house"; container = "oval"; ap = "doubleoctagon"; client = "doublecircle"; }.${role}; }) ( lib.filterAttrs (_: { links, ... }: links != {} ) config.site.hosts ) // builtins.mapAttrs (_: _: { shape = "circle"; }) ( lib.filterAttrs (net: _: builtins.match "up.*" net != null ) config.site.net ); links = builtins.concatMap (hostName: map (link: { pair = [ hostName link ]; startLabel = ( #lib.optionalString (config.site.hosts.${hostName}.links ? ${link}) ( lib.concatStringsSep "," config.site.hosts.${hostName}.links.${link}.ports ); }) ( builtins.filter (link: config.site.hosts ? ${link} || builtins.match "up.*" link != null ) (builtins.attrNames config.site.hosts.${hostName}.links) ) ) (builtins.attrNames config.site.hosts); }; # Layer 3 logical-graph = let containers = lib.filterAttrs (_: { isRouter, role, ... }: role == "container" ) config.site.hosts; in renderGraph { name = "logical"; engine = "sfdp"; nodes = builtins.foldl' (result: hostName: result // { "${hostName}".shape = "box"; } // builtins.mapAttrs (_: _: { shape = "circle"; }) containers.${hostName}.interfaces ) {} (builtins.attrNames containers); links = builtins.concatMap (hostName: map (net: { pair = [ hostName net ]; attrs.color = netColor net; }) (builtins.attrNames containers.${hostName}.interfaces) ) (builtins.attrNames containers); }; network-graphs = runCommand "network-graphs" {} '' DIR=$out/share/doc/zentralwerk mkdir -p $DIR ln -s ${physical-graph} $DIR/physical.png ln -s ${logical-graph} $DIR/logical.png mkdir -p $out/nix-support echo doc image $DIR/physical.png >> $out/nix-support/hydra-build-products echo doc image $DIR/logical.png >> $out/nix-support/hydra-build-products ''; }