NOC site for AS203496 served at noc.obr.sh

This commit is contained in:
olaf committed 2026-10-08 15:09:55 +02:00
commit 6758bda3cb
10 files changed
+102

No files matched your search

+1
View File
@@ -0,0 +1 @@
public/
+8
View File
@@ -0,0 +1,8 @@
# noc-site
Static NOC site for AS203496, served at https://noc.obr.sh. `python3 build.py` writes `public/`.
- Brand name, domain, ASN, upstreams, contacts and PGP keys: `site.json` (rebrand = change `brand` and the contact fields there).
- Prose: `content/*.txt`.
- Deploy: copy `public/` to CT114 web-static `/opt/static-sites/sites/noc.obr.sh/current` (see `deploy.sh`).
- noc.obnh.network returns 301 to https://noc.obr.sh (path kept), set up in the CT110 edge traefik and the CT114 nginx.
Executable
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Build the NOC site into ./public. Brand, domain and facts come from site.json; prose from content/*.txt."""
import json, html, shutil, pathlib
root = pathlib.Path(__file__).parent
S = json.load(open(root / "site.json"))
out = root / "public"
shutil.rmtree(out, ignore_errors=True)
shutil.copytree(root / "static", out)
e = html.escape
CSS = """:root{--bg:#fff;--fg:#1c1f24;--mut:#5b6470;--ac:#0b5cad;--bd:#d9dee4}
@media (prefers-color-scheme:dark){:root{--bg:#14171b;--fg:#e6e9ed;--mut:#9aa4b0;--ac:#6cb2ff;--bd:#2d333b}}
body{margin:0;background:var(--bg);color:var(--fg);font:16px/1.6 system-ui,sans-serif}
.w{max-width:46rem;margin:0 auto;padding:0 16px}header{border-bottom:1px solid var(--bd);padding:1rem 0}
header a.b{font-weight:700;color:var(--fg);text-decoration:none}nav{margin-top:.4rem}nav a{margin-right:1rem}
a{color:var(--ac)}h1{margin-top:2rem}table{border-collapse:collapse;width:100%}td,th{border-bottom:1px solid var(--bd);padding:.4rem .5rem;text-align:left;vertical-align:top}
th{color:var(--mut);font-weight:600;width:35%}code{word-break:break-all}footer{color:var(--mut);border-top:1px solid var(--bd);margin:3rem 0 2rem;padding-top:1rem;font-size:.9rem}"""
NAV = [("/", "Home"), ("/peering/", "Peering"), ("/pgp/", "PGP keys"), ("/contact/", "Contact")]
def paras(name):
return "".join(f"<p>{e(l)}</p>" for l in (root / "content" / f"{name}.txt").read_text().splitlines() if l.strip())
def page(path, title, body):
nav = "".join(f'<a href="{u}">{t}</a>' for u, t in NAV)
doc = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{e(title)} - {e(S['brand'])}</title><meta name="description" content="{e(S['asn'])} {e(S['tagline'])}">
<link rel="canonical" href="https://{S['domain']}{path}"><style>{CSS}</style></head><body>
<header><div class="w"><a class="b" href="/">{e(S['brand'])}</a><nav>{nav}</nav></div></header>
<main class="w">{body}</main><footer class="w">{e(S['brand'])} &middot; {e(S['asn'])} &middot; <a href="{S['peeringdb']}">PeeringDB</a></footer></body></html>"""
d = out / path.strip("/"); d.mkdir(parents=True, exist_ok=True)
(d / "index.html").write_text(doc)
ups = "".join(f"<li>{e(u['asn'])} {e(u['name'])} ({e(u['site'])})</li>" for u in S["upstreams"])
facts = f"""<table>
<tr><th>ASN</th><td>{e(S['asn'])}</td></tr>
<tr><th>Prefix</th><td><code>{e(S['prefix'])}</code> (IPv6 only)</td></tr>
<tr><th>Upstreams</th><td><ul>{ups}</ul></td></tr>
<tr><th>AS-SET</th><td><code>{e(S['as_set'])}</code> (RIPE)</td></tr>
<tr><th>RPKI</th><td>ROA {e(S['prefix'])}, origin {e(S['asn'])}, maxLength 48; ASPA providers AS41051, AS6517</td></tr>
<tr><th>PeeringDB</th><td><a href="{S['peeringdb']}">{S['peeringdb']}</a></td></tr>
<tr><th>Policy</th><td>Open, IPv6 preferred</td></tr></table>"""
page("/", S["tagline"], f"<h1>{e(S['brand'])}</h1><p><strong>{e(S['tagline'])}</strong></p>{paras('home')}<h2>Network facts</h2>{facts}")
page("/peering/", "Peering", f"<h1>Peering</h1>{paras('peering')}{facts}")
keys = "".join(f"<tr><th>{e(a)}</th><td><code>{e(f)}</code></td></tr>" for a, f in S["pgp"])
page("/pgp/", "PGP keys", f"<h1>PGP keys</h1>{paras('pgp')}<table>{keys}</table>")
c = S["contacts"]
cbody = f"""<h1>Contact</h1>{paras('contact')}<table>
<tr><th>NOC for {e(S['asn'])}</th><td><a href="mailto:{c['noc']}">{c['noc']}</a></td></tr>
<tr><th>Abuse</th><td><a href="mailto:{c['abuse']}">{c['abuse']}</a></td></tr>
<tr><th>Emergency</th><td>{e(c['phone'])}</td></tr>
<tr><th>Threema</th><td>{e(c['threema'])}<br><img src="/threema-{c['threema']}.jpg" alt="Threema QR code" width="160"></td></tr></table>"""
page("/contact/", "Contact", cbody)
(out / "robots.txt").write_text(f"User-agent: *\nAllow: /\nSitemap: https://{S['domain']}/sitemap.xml\n")
urls = "".join(f"<url><loc>https://{S['domain']}{u}</loc></url>" for u, _ in NAV)
(out / "sitemap.xml").write_text(f'<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">{urls}</urlset>')
print("built", S["domain"])
+1
View File
@@ -0,0 +1 @@
Send operational questions to the NOC address. Report abuse to the abuse address. For emergencies, call the number below. Threema is an alternative channel.
+2
View File
@@ -0,0 +1,2 @@
AS203496 is a small, IPv6-only network for research and training. It exists to learn, test and operate real internet infrastructure the way a production network would.
The network announces one prefix, 2001:67c:1948::/48, from two sites. Each site has its own transit provider, so the network stays reachable if one upstream fails.
+4
View File
@@ -0,0 +1,4 @@
Peering is welcome. AS203496 peers directly where your network shares a facility (a meet-me room) or an internet exchange.
The network prefers IPv6. Direct sessions are IPv6 only. IPv4 sessions are set up only with IXP route servers, and there are no direct IPv4-only sessions. For direct peerings and interconnects, an MD5 password is appreciated.
Customers of AS203496 are not treated as peers at any location. Transit can be provided where that is feasible.
For filtering, use the AS-SET AS203496:AS-OBNH-NETWORK (RIPE). The route6 object and the ROA both cover 2001:67c:1948::/48 with origin AS203496, and an ASPA record lists AS41051 and AS6517 as providers.
+1
View File
@@ -0,0 +1 @@
Public keys for signed and encrypted mail are listed below.
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
# Build and push public/ to web-static CT114 on nibbler (nibbler tailnet 100.82.124.16).
set -e
cd "$(dirname "$0")"
python3 build.py
tar czf - -C public . | ssh root@100.82.124.16 'pct exec 114 -- sh -c "rm -rf /opt/static-sites/sites/noc.obr.sh/current.new && mkdir -p /opt/static-sites/sites/noc.obr.sh/current.new && tar xzf - -C /opt/static-sites/sites/noc.obr.sh/current.new && rm -rf /opt/static-sites/sites/noc.obr.sh/current.old; [ -d /opt/static-sites/sites/noc.obr.sh/current ] && mv /opt/static-sites/sites/noc.obr.sh/current /opt/static-sites/sites/noc.obr.sh/current.old; mv /opt/static-sites/sites/noc.obr.sh/current.new /opt/static-sites/sites/noc.obr.sh/current"'
+27
View File
@@ -0,0 +1,27 @@
{
"brand": "OBNH Network Baumert",
"domain": "noc.obr.sh",
"old_domain": "noc.obnh.network",
"tagline": "IPv6-only training and research network",
"asn": "AS203496",
"prefix": "2001:67c:1948::/48",
"as_set": "AS203496:AS-OBNH-NETWORK",
"upstreams": [
{"asn": "AS41051", "name": "FreeTransit (Openfactory)", "site": "leela"},
{"asn": "AS6517", "name": "Lake Networks", "site": "kiff"}
],
"peeringdb": "https://www.peeringdb.com/net/30644",
"contacts": {
"noc": "noc@obnh.network",
"abuse": "abuse@obnh.network",
"phone": "+41 76 263 0152",
"threema": "XKSBXYFZ"
},
"pgp": [
["admin@obnh.network", "05dc62d7ab054e72c6aea639ba0ee08972efd755"],
["admin@obnh.org", "b6475ed1ff426b3c16b7366c3450c39c710127f1"],
["noc@obnh.network", "f50d798f7b7e1a84fe5d0b8f828c4b32f92444c6"],
["olaf@baumert.cc", "cf8e073a7bba64da51308540638a2a01c171b4f9"],
["olaf@obnh.network", "8ac0a93854d0a59ec28c8b4dd0a70dba97115a8a"]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB