diff --git a/ci/update-docs-assignments.py b/ci/update-docs-assignments.py new file mode 100755 index 0000000..92dd7b7 --- /dev/null +++ b/ci/update-docs-assignments.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Update the consolidated network-assignment tables in docs/networking.md. + +Reads nixos.allAssignments from the flake and renders assignment-key subgroups for each site +(colony / home / remote / other) between per-site `` markers. The +hand-written Notes column is preserved, keyed by (box, assignment). +""" + +import json +import re +import subprocess +import sys +from pathlib import Path + +DOCS = Path("docs") +TARGET = DOCS / "networking.md" +HEADER = "| Box | IPv4 | IPv6 | Domain | Notes |" +SEP = "|---|---|---|---|---|" + +# Table order; also the set of valid `` tokens. +SITE_ORDER = ["colony", "home", "remote", "other"] + +# Per-site internal domains of the remote boxes (britway=lon1, britnet=bhx1, kelder=hentai). +REMOTE_DOMAINS = ("lon1.int.nul.ie", "bhx1.int.nul.ie", "hentai.engineer") + + +def site_of(assignments: dict) -> str: + """Classify a box into a site table by its assignment domain. + + Prefer the `internal` assignment's domain, else the first assignment that has one. + """ + dom = (assignments.get("internal") or {}).get("domain") + if not dom: + for a in assignments.values(): + if a.get("domain"): + dom = a["domain"] + break + if not dom: + return "other" + if dom.endswith("ams1.int.nul.ie"): + return "colony" + if dom == "h.nul.ie" or dom.endswith(".h.nul.ie"): + return "home" + if any(dom.endswith(s) for s in REMOTE_DOMAINS): + return "remote" + return "other" + + +def fmt_ip(ip: dict) -> str: + addr = ip.get("address") + if addr is None: + return "—" + parts = [f"{addr}/{ip.get('mask')}"] + if ip.get("gateway") is not None: + parts.append(f"gw {ip['gateway']}") + return f"`{' '.join(parts)}`" + + +def find_page(box: str) -> str | None: + """Doc page for a box (relative to docs/), if one exists.""" + for pattern in (f"{box}.md", f"{box}/README.md"): + for p in sorted(DOCS.rglob(pattern)): + return p.relative_to(DOCS).as_posix() + return None + + +def box_cell(box: str) -> str: + page = find_page(box) + return f"[`{box}`]({page})" if page else f"`{box}`" + + +def box_name_from_cell(cell: str) -> str: + m = re.match(r"\[`?([^`\]]+)`?\]", cell) + return m.group(1) if m else cell.strip("`") + + +def parse_notes(lines: list[str]) -> dict[tuple[str, str], str]: + """Existing Notes indexed by (box, assignment), from either table layout.""" + notes: dict[tuple[str, str], str] = {} + assignment = None + for line in lines: + stripped = line.strip() + heading = re.match(r"^####\s+`([^`]+)`$", stripped) + if heading: + assignment = heading.group(1) + continue + if not stripped.startswith("|"): + continue + cells = [c.strip() for c in stripped.split("|")] + # "| a | b | ... |" splits to ['', 'a', 'b', ..., ''] + if len(cells) >= 7 and assignment is not None: + # Current layout: one table under each assignment heading. + box, row_assignment, note = cells[1], assignment, cells[5] + elif len(cells) >= 8: + # Previous layout: one site table with an Assignment column. + box, row_assignment, note = cells[1], cells[2], cells[6] + else: + continue + # Skip header and separator rows. + if box == "Box" or set(box) <= {"-"}: + continue + notes[(box_name_from_cell(box), row_assignment)] = note + return notes + + +def render_site(boxes: list[str], all_assignments: dict, notes: dict) -> list[str]: + groups: dict[str, list[tuple[str, dict]]] = {} + for box in sorted(boxes): + for key, a in all_assignments[box].items(): + groups.setdefault(key, []).append((box, a)) + + lines: list[str] = [] + group_order = sorted(groups, key=lambda key: (key != "internal", key)) + for key in group_order: + if lines: + lines.append("") + lines.extend([f"#### `{key}`", "", HEADER, SEP]) + for box, a in groups[key]: + lines.append( + "| " + + " | ".join( + [ + box_cell(box), + fmt_ip(a.get("ipv4", {})), + fmt_ip(a.get("ipv6", {})), + a.get("domain") or "—", + notes.get((box, key), ""), + ] + ) + + " |" + ) + return lines + + +def update_target(all_assignments: dict) -> bool: + by_site: dict[str, list[str]] = {s: [] for s in SITE_ORDER} + for box, assignments in all_assignments.items(): + if not assignments: + continue + by_site[site_of(assignments)].append(box) + + text = TARGET.read_text() + lines = text.splitlines() + marker_re = re.compile(r"^$") + + # Walk the file line by line; at each `` marker, replace + # everything up to the matching `` with a freshly rendered table + # (carrying the hand-written Notes over), leaving all other lines untouched. + out: list[str] = [] + i = 0 + while i < len(lines): + m = marker_re.match(lines[i].strip()) + if not m: + out.append(lines[i]) + i += 1 + continue + site = m.group(1) + end = i + 1 + while end < len(lines) and lines[end].strip() != "": + end += 1 + if end >= len(lines): + out.append(lines[i]) + i += 1 + continue + notes = parse_notes(lines[i + 1 : end]) + out.append(lines[i]) + out.append("") + out.extend(render_site(by_site.get(site, []), all_assignments, notes)) + out.append("") + i = end + 1 + + new_text = "\n".join(out) + "\n" + if new_text != text: + TARGET.write_text(new_text) + return True + return False + + +def main() -> int: + result = subprocess.run( + ["nix", "eval", ".#nixfiles.config.nixos.allAssignments", "--json"], + capture_output=True, + text=True, + check=True, + ) + all_assignments = json.loads(result.stdout) + + if update_target(all_assignments): + print(f"updated {TARGET}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/networking.md b/docs/networking.md index 81b651d..fa1bc49 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -52,18 +52,183 @@ edit prose there, never the other generated cells. +#### `internal` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`chatterbox`](sites/colony/shill/containers/chatterbox.md) | `10.100.2.5/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::5/64` | ams1.int.nul.ie | | +| [`colony`](sites/colony/colony.md) | `94.142.241.224/32` | `2a0e:97c0:4d2:10::2/64` | ams1.int.nul.ie | | +| [`colony-psql`](sites/colony/shill/containers/colony-psql.md) | `10.100.2.4/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::4/64` | ams1.int.nul.ie | | +| `enshrouded-oci` | `10.100.3.5/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::5/64` | ams1.int.nul.ie | Enshrouded OCI container on [`whale2`](sites/colony/whale2.md#game-servers); disabled | +| [`estuary`](sites/colony/estuary.md) | `94.142.240.44/24 gw 94.142.240.254` | `2a02:898:0:20::329:1/64 gw 2a02:898:0:20::1` | ams1.int.nul.ie | | +| [`gam`](sites/colony/shill/containers/gam.md) | `10.100.2.11/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::b/64` | ams1.int.nul.ie | | +| [`git`](sites/colony/git.md) | `94.142.241.117/32` | `2a0e:97c0:4d2:11::4/64` | ams1.int.nul.ie | | +| `graeme-oci` | `10.100.3.8/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::8/64` | ams1.int.nul.ie | Minecraft OCI container on [`whale2`](sites/colony/whale2.md#game-servers) | +| [`jackflix`](sites/colony/shill/containers/jackflix.md) | `10.100.2.6/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::6/64` | ams1.int.nul.ie | | +| `kevcraft-oci` | `10.100.3.6/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::6/64` | ams1.int.nul.ie | Minecraft OCI container on [`whale2`](sites/colony/whale2.md#game-servers) | +| `kinkcraft-oci` | `10.100.3.7/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::7/64` | ams1.int.nul.ie | Minecraft OCI container on [`whale2`](sites/colony/whale2.md#game-servers) | +| [`middleman`](sites/colony/shill/containers/middleman.md) | `10.100.2.2/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::2/64` | ams1.int.nul.ie | | +| [`object`](sites/colony/shill/containers/object.md) | `10.100.2.7/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::7/64` | ams1.int.nul.ie | | +| [`qclk`](sites/colony/shill/containers/qclk.md) | `10.100.2.10/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::a/64` | ams1.int.nul.ie | | +| [`shill`](sites/colony/shill/README.md) | `94.142.241.225/32` | `2a0e:97c0:4d2:11::2/64` | ams1.int.nul.ie | | +| `simpcraft-oci` | `10.100.3.3/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::3/64` | ams1.int.nul.ie | Minecraft OCI container on [`whale2`](sites/colony/whale2.md#game-servers) | +| `simpcraft-staging-oci` | `10.100.3.4/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::4/64` | ams1.int.nul.ie | Minecraft staging OCI container on [`whale2`](sites/colony/whale2.md#game-servers); disabled | +| [`toot`](sites/colony/shill/containers/toot.md) | `10.100.2.8/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::8/64` | ams1.int.nul.ie | | +| `valheim-oci` | `10.100.3.2/24 gw 10.100.3.1` | `2a0e:97c0:4d2:13::2/64` | ams1.int.nul.ie | Valheim OCI container on [`whale2`](sites/colony/whale2.md#game-servers) | +| [`vaultwarden`](sites/colony/shill/containers/vaultwarden.md) | `10.100.2.3/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::3/64` | ams1.int.nul.ie | | +| [`waffletail`](sites/colony/shill/containers/waffletail.md) | `10.100.2.9/24 gw 10.100.2.1` | `2a0e:97c0:4d2:12::9/64` | ams1.int.nul.ie | | +| [`whale2`](sites/colony/whale2.md) | `94.142.241.226/32` | `2a0e:97c0:4d2:11::3/64` | ams1.int.nul.ie | | + +#### `as211024` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`estuary`](sites/colony/estuary.md) | `10.100.50.1/24` | `2a0e:97c0:4df::1/64` | — | | + +#### `base` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`estuary`](sites/colony/estuary.md) | `10.100.0.1/24` | `2a0e:97c0:4d2:10::1/64` | ams1.int.nul.ie | | + +#### `ctrs` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`shill`](sites/colony/shill/README.md) | `10.100.2.1/24` | `2a0e:97c0:4d2:12::1/64` | ams1.int.nul.ie | | + +#### `oci` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`whale2`](sites/colony/whale2.md) | `10.100.3.1/24` | `2a0e:97c0:4d2:13::1/64` | ams1.int.nul.ie | | + +#### `qclk` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`qclk`](sites/colony/shill/containers/qclk.md) | `10.100.4.1/24` | — | — | | + +#### `routing` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`colony`](sites/colony/colony.md) | `10.100.0.2/24 gw 10.100.0.1` | — | ams1.int.nul.ie | | +| [`git`](sites/colony/git.md) | `10.100.1.4/24 gw 10.100.1.1` | — | ams1.int.nul.ie | | +| [`shill`](sites/colony/shill/README.md) | `10.100.1.2/24 gw 10.100.1.1` | — | ams1.int.nul.ie | | +| [`whale2`](sites/colony/whale2.md) | `10.100.1.3/24 gw 10.100.1.1` | — | ams1.int.nul.ie | | + +#### `tailscale` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`waffletail`](sites/colony/shill/containers/waffletail.md) | `100.64.0.5/32` | `fd7a:115c:a1e0::5/128` | — | | + +#### `vms` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`colony`](sites/colony/colony.md) | `10.100.1.1/24` | `2a0e:97c0:4d2:11::1/64` | ams1.int.nul.ie | | ### home +#### `as211024` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`river`](sites/home/river.md) | `10.100.50.2/24` | `2a0e:97c0:4df:0:1::1/64 gw 2a0e:97c0:4df:0:2::1` | — | | +| [`stream`](sites/home/stream.md) | `10.100.50.3/24` | `2a0e:97c0:4df:0:1::2/64 gw 2a0e:97c0:4df:0:2::1` | — | | + +#### `core` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`palace`](sites/home/palace.md) | `192.168.64.20/24` | — | h.nul.ie | | +| [`river`](sites/home/river.md) | `192.168.64.1/24` | — | h.nul.ie | | +| [`stream`](sites/home/stream.md) | `192.168.64.2/24` | — | h.nul.ie | | +| [`unifi`](sites/home/sfh/containers/unifi.md) | `192.168.64.21/24` | — | h.nul.ie | | + +#### `hi` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`castle`](sites/home/castle.md) | `192.168.68.40/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::3:1/64` | h.nul.ie | | +| [`cellar`](sites/home/cellar.md) | `192.168.68.80/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::4:1/64` | h.nul.ie | | +| [`hass`](sites/home/sfh/containers/hass.md) | `192.168.68.103/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::5:3/64` | h.nul.ie | | +| [`palace`](sites/home/palace.md) | `192.168.68.22/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::2:1/64` | h.nul.ie | | +| [`river`](sites/home/river.md) | `192.168.68.1/22` | `2a0e:97c0:4d0:1::1/64` | h.nul.ie | | +| `router-hi` | `192.168.71.254/22 gw 192.168.68.1` | `2a0e:97c0:4d0:1::ffff/64` | h.nul.ie | Floating VIP shared by [`river`](sites/home/river.md) and [`stream`](sites/home/stream.md) | +| [`sfh`](sites/home/sfh/README.md) | `192.168.68.81/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::4:2/64` | h.nul.ie | | +| [`stream`](sites/home/stream.md) | `192.168.68.2/22` | `2a0e:97c0:4d0:1::2/64` | h.nul.ie | | +| [`unifi`](sites/home/sfh/containers/unifi.md) | `192.168.68.100/22 gw 192.168.71.254` | `2a0e:97c0:4d0:1::5:1/64` | h.nul.ie | | + +#### `lo` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`hass`](sites/home/sfh/containers/hass.md) | `192.168.72.103/21` | `2a0e:97c0:4d0:2::5:3/64` | h.nul.ie | | +| [`river`](sites/home/river.md) | `192.168.72.1/21` | `2a0e:97c0:4d0:2::1/64` | h.nul.ie | | +| `router-lo` | `192.168.79.254/21 gw 192.168.72.1` | `2a0e:97c0:4d0:2::ffff/64` | h.nul.ie | Floating VIP shared by [`river`](sites/home/river.md) and [`stream`](sites/home/stream.md) | +| [`stream`](sites/home/stream.md) | `192.168.72.2/21` | `2a0e:97c0:4d0:2::2/64` | h.nul.ie | | + +#### `untrusted` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`river`](sites/home/river.md) | `192.168.80.1/24` | `2a0e:97c0:4d0:3::1/64` | h.nul.ie | | +| `router-ut` | `192.168.80.254/24 gw 192.168.80.1` | `2a0e:97c0:4d0:3::ffff/64` | h.nul.ie | Floating VIP shared by [`river`](sites/home/river.md) and [`stream`](sites/home/stream.md) | +| [`stream`](sites/home/stream.md) | `192.168.80.2/24` | `2a0e:97c0:4d0:3::2/64` | h.nul.ie | | ### remote +#### `internal` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`kelder-acquisition`](remote/kelder/containers/kelder-acquisition.md) | `172.16.64.2/24 gw 172.16.64.1` | — | hentai.engineer | | +| [`kelder-spoder`](remote/kelder/containers/kelder-spoder.md) | `172.16.64.3/24 gw 172.16.64.1` | — | hentai.engineer | | + +#### `allhost` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`britnet`](remote/britnet.md) | `77.74.199.67/24 gw 77.74.199.1` | `2a12:ab46:5344:99::a/64 gw 2a12:ab46:5344::1` | bhx1.int.nul.ie | | + +#### `as211024` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`britway`](remote/britway.md) | `10.100.50.5/24` | `2a0e:97c0:4df:0:2::1/64` | — | | + +#### `ctrs` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`kelder`](remote/kelder/README.md) | `172.16.64.1/24` | — | hentai.engineer | | + +#### `estuary` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`kelder`](remote/kelder/README.md) | `94.142.242.254/32` | — | — | | + +#### `vpn` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`britnet`](remote/britnet.md) | `10.200.0.1/24` | `fdfb:5ebf:6e84::1/64` | — | | + +#### `vultr` + +| Box | IPv4 | IPv6 | Domain | Notes | +|---|---|---|---|---| +| [`britway`](remote/britway.md) | `45.76.141.188/23 gw 45.76.140.1` | `2001:19f0:7402:128b::1/64` | lon1.int.nul.ie | | ## Domains diff --git a/pkgs/default.nix b/pkgs/default.nix index a96a2cc..a6fd6b2 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -11,4 +11,8 @@ in chocolate-doom2xx = callPackage ./chocolate-doom2xx { }; windowtolayer = callPackage ./windowtolayer.nix { }; swaylock-plugin = callPackage ./swaylock-plugin.nix { }; + + update-docs-assignments = pkgs.writeShellScriptBin "update-docs-assignments" '' + exec ${pkgs.python3}/bin/python3 ${../ci/update-docs-assignments.py} "$@" + ''; }