diff --git a/flake.nix b/flake.nix index 7a8a3ba..c11360a 100644 --- a/flake.nix +++ b/flake.nix @@ -186,6 +186,47 @@ { renderer = "d2"; nixosConfigurations = self.nodes; + + nodes.fritzbox = { + name = "FritzBox"; + deviceType = "router"; + hardware.image = ./fritzbox.png; + # interfaces.wan0.network = "internet"; + interfaces.wan0.physicalConnections = [ + { + node = "ward"; + interface = "wan"; + } + ]; + }; + + nodes.fritzbox-no-img = { + name = "FritzBox No HImg"; + deviceType = "router"; + interfaces.wan0.physicalConnections = [ + { + node = "ward"; + interface = "wan"; + } + ]; + }; + + nodes.fritzbox-device = { + name = "FritzBox No D&HImg"; + deviceType = "device"; + interfaces.wan0.physicalConnections = [ + { + node = "ward"; + interface = "wan"; + } + ]; + }; + + # TODO: + #nodes.fritzbox = config.lib.nodes.mkRouter {}; + #nodes.fritzbox = config.lib.nodes.mkSwitch {}; + #nodes.fritzbox = config.lib.nodes.mkWifiAP {}; + #nodes.printer = config.lib.nodes.mkWifiAP {}; } ]; }; diff --git a/fritzbox.png b/fritzbox.png new file mode 100644 index 0000000..ed0731a Binary files /dev/null and b/fritzbox.png differ diff --git a/pkgs/html-to-svg/main.js b/pkgs/html-to-svg/main.js index f250060..b6788d6 100644 --- a/pkgs/html-to-svg/main.js +++ b/pkgs/html-to-svg/main.js @@ -13,7 +13,8 @@ program .argument("", "output svg") .option("--font ", "Sets the font") .option("--font-bold ", "Sets the bold font") - .option("-w, --width ", "Sets the width of the output", 680) + .option("-w, --width ", "Sets the width of the output. Use auto for automatic scaling.", "auto") + .option("-h, --height ", "Sets the height of the output. Use auto for automatic scaling.", "auto") .action(async (input, output, options) => { if (!options.font) { console.error("--font is required"); @@ -27,7 +28,8 @@ program const markup = html(await fs.readFile(input, { encoding: "utf8" })); const svg = await satori(markup, { - width: options.width, + ...(options.width != "auto" && {width: options.width}), + ...(options.height != "auto" && {height: options.height}), embedFont: true, fonts: [ { diff --git a/topology/icons/devices/cloud-server.svg b/topology/icons/devices/cloud-server.svg new file mode 100644 index 0000000..96e93ba --- /dev/null +++ b/topology/icons/devices/cloud-server.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/topology/icons/devices/router.svg b/topology/icons/devices/router.svg new file mode 100644 index 0000000..99ba7d4 --- /dev/null +++ b/topology/icons/devices/router.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/topology/icons/devices/switch.svg b/topology/icons/devices/switch.svg new file mode 100644 index 0000000..eaca268 --- /dev/null +++ b/topology/icons/devices/switch.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/topology/options/nodes.nix b/topology/options/nodes.nix index e733743..ac275e5 100644 --- a/topology/options/nodes.nix +++ b/topology/options/nodes.nix @@ -6,11 +6,13 @@ f: { inherit (lib) attrValues + elem flatten flip literalExpression mkDefault mkIf + mkMerge mkOption types ; @@ -38,19 +40,33 @@ in defaultText = literalExpression ''""''; }; + hardware = { + description = mkOption { + description = "A description of this node's hardware. Usually the device name or lost of the most important components."; + type = types.lines; + default = ""; + }; + + image = mkOption { + description = "An image representing this node, usually shown larger than an icon."; + type = types.nullOr types.path; + default = null; + }; + }; + icon = mkOption { description = "The icon representing this node. Usually shown next to the name. Must be a path to an image or a valid icon name (.)."; type = types.nullOr (types.either types.path types.str); default = null; }; - # FIXME: TODO emoji / icon - # FIXME: TODO hardware description "Odroid H3" - # FIXME: TODO hardware image - deviceType = mkOption { - description = "TODO"; - type = types.enum ["nixos" "microvm" "nixos-container"]; + 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. + ''; + type = types.either (types.enum ["nixos" "router" "switch" "device"]) types.str; }; deviceIcon = mkOption { @@ -64,14 +80,37 @@ in default = null; 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.''; + }; }; - config = { - # Set the default icon, if an icon exists with a matching name - deviceIcon = mkIf (config.topology.isMainModule && config.icons.devices ? ${nodeSubmod.config.deviceType}) ( - mkDefault ("devices." + nodeSubmod.config.deviceType) - ); - }; + config = let + nodeCfg = nodeSubmod.config; + in + mkIf config.topology.isMainModule (mkMerge [ + # Set the default icon, if an icon exists with a matching name + { + deviceIcon = mkIf (config.icons.devices ? ${nodeCfg.deviceType}) ( + mkDefault ("devices." + nodeCfg.deviceType) + ); + } + + # If the device type is generic device, try to render as an image + # and set the default image to the deviceIcon. + (mkIf (elem nodeCfg.deviceType ["router" "switch" "device"]) { + preferredRenderType = mkDefault "image"; + hardware.image = mkDefault (config.lib.icons.get nodeCfg.deviceIcon); + }) + ]); })); }; diff --git a/topology/topology/renderers/svg/default.nix b/topology/topology/renderers/svg/default.nix index 2b4b2f6..49b2438 100644 --- a/topology/topology/renderers/svg/default.nix +++ b/topology/topology/renderers/svg/default.nix @@ -1,3 +1,13 @@ +# TODO: +# - disks (from disko) + render +# - hardware info (image small top and image big bottom and full (no card), maybe just image and render position) +# - render router and other devices (card with interfaces, card with just image) +# - render nodes with guests, guests in short form +# - nginx proxy pass render, with upstream support +# - more service info +# - impermanence render? +# - stable pseudorandom colors from palette with no-reuse until necessary +# - search todo and do { config, lib, @@ -19,18 +29,25 @@ types ; - htmlToSvgCommand = inFile: outFile: '' + fileBase64 = file: let + out = pkgs.runCommand "base64" {} '' + ${pkgs.coreutils}/bin/base64 -w0 < ${file} > $out + ''; + in "${out}"; + + htmlToSvgCommand = inFile: outFile: args: '' ${lib.getExe pkgs.html-to-svg} \ --font ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Regular.ttf \ --font-bold ${pkgs.jetbrains-mono}/share/fonts/truetype/JetBrainsMono-Bold.ttf \ - --width 680 \ + --width ${toString (args.width or "auto")} \ + --height ${toString (args.height or "auto")} \ ${inFile} ${outFile} ''; - renderHtmlToSvg = html: name: let + renderHtmlToSvg = card: name: let drv = pkgs.runCommand "generate-svg-${name}" {} '' mkdir -p $out - ${htmlToSvgCommand (pkgs.writeText "${name}.html" html) "$out/${name}.svg"} + ${htmlToSvgCommand (pkgs.writeText "${name}.html" card.html) "$out/${name}.svg" card} ''; in "${drv}/${name}.svg"; @@ -46,13 +63,13 @@ content = head (splitString "" withoutPrefix); in '''' else if hasSuffix ".png" file - # FIXME: TODO png, jpg, ... - then '' - " - '' + then '' ''; - mkRootContainer = contents: + mkRootContainer = twAttrs: contents: /* html */ ''
+
${contents}
+
''; - mkRootCard = twAttrs: contents: - mkRootContainer - /* - html - */ - '' -
- ${contents} -
- ''; + mkCardContainer = mkRootContainer "bg-[#101419] rounded-xl"; spacingMt2 = ''
@@ -161,36 +171,73 @@ ''; - mkInfoCardNetwork = node: - mkRootCard "rounded-xl" - /* - html - */ - '' - ${mkTitle node} + mkNetCard = node: { + width = 680; + html = + mkCardContainer + /* + html + */ + '' + ${mkTitle node} - ${concatLines (map mkInterface (attrValues node.interfaces))} - ${optionalString (node.interfaces != {}) spacingMt2} - ''; + ${concatLines (map mkInterface (attrValues node.interfaces))} + ${optionalString (node.interfaces != {}) spacingMt2} - mkInfoCardFull = node: let + ${mkImageMaybe "w-full h-24" node.hardware.image} + ''; + }; + + mkCard = node: let services = filter (x: !x.hidden) (attrValues node.services); - in - mkRootCard "rounded-xl" - /* - html - */ - '' - ${mkTitle node} + in { + width = 680; + html = + mkCardContainer + /* + html + */ + '' + ${mkTitle node} - ${concatLines (map mkInterface (attrValues node.interfaces))} - ${optionalString (node.interfaces != {}) spacingMt2} + ${concatLines (map mkInterface (attrValues node.interfaces))} + ${optionalString (node.interfaces != {}) spacingMt2} - ${concatLines (map mkService services)} - ${optionalString (services != []) spacingMt2} + ${concatLines (map mkService services)} + ${optionalString (services != []) spacingMt2} -
- ''; + ${mkImageMaybe "w-full h-24" node.hardware.image} + ''; + }; + + mkImageWithName = node: { + html = let + deviceIconImage = config.lib.icons.get node.deviceIcon; + in + mkRootContainer "" + /* + html + */ + '' +
+ ${mkImageMaybe "w-12 h-12 mr-4" (config.lib.icons.get node.icon)} +

${node.name}

+
+

${node.deviceType}

+ ${mkImageMaybeIf (node.hardware.image != null -> deviceIconImage != node.hardware.image) "w-16 h-16 ml-4" deviceIconImage} +
+ + ${mkImageMaybe "w-full h-24" node.hardware.image} + ''; + }; + + mkPreferredRender = node: + ( + if node.preferredRenderType == "image" && node.hardware.image != null + then mkImageWithName + else mkCard + ) + node; }; }; in { @@ -206,18 +253,16 @@ in { config = { lib.renderers.svg.node = { - mkInfoCardNetwork = node: renderHtmlToSvg (html.node.mkInfoCardNetwork node) "card-network-${node.name}"; - mkInfoCardFull = node: renderHtmlToSvg (html.node.mkInfoCardFull node) "card-node-${node.name}"; + mkNetCard = node: renderHtmlToSvg (html.node.mkNetCard node) "card-network-${node.id}"; + mkCard = node: renderHtmlToSvg (html.node.mkCard node) "card-node-${node.id}"; + mkPreferredRender = node: renderHtmlToSvg (html.node.mkPreferredRender node) "preferred-render-node-${node.id}"; }; renderers.svg.output = pkgs.runCommand "topology-svgs" {} '' mkdir -p $out/nodes - ${concatLines (flip map (attrValues config.nodes) ( - node: - htmlToSvgCommand ( - pkgs.writeText "node-${node.name}.html" (html.node.mkInfoCardFull node) - ) "$out/nodes/${node.name}.svg" - ))} + ${concatLines (flip map (attrValues config.nodes) (node: '' + cp ${config.lib.renderers.svg.node.mkPreferredRender node} $out/nodes/${node.id}.svg + ''))} ''; }; }