server: box region auto-detects from cloud metadata (#737)
Hand-set STATUS_BOX_REGION drifted on day one (labelled Germany; the box is in Helsinki). Resolution is now env override -> Hetzner metadata -> AWS IMDSv2, zone mapped to a coarse continent+country, omitted when nothing answers. Resolved at use-time so the dry-run's no-live-probes guard actually guards.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 file changed,
+39
−7
deploy/status-publish.sh+39 −7
diff --git a/deploy/status-publish.sh b/deploy/status-publish.sh| index 391485a..b538957 100755 |
| --- a/deploy/status-publish.sh |
| +++ b/deploy/status-publish.sh |
| @@ -27,10 +27,11 @@ |
| # pairs hashed from the machine id ("ada-stargazing") — stable per box, |
| # never peer URLs, IPs, hostnames, or the machine id itself. |
| # |
| -# A box's `region` is COARSE BY DESIGN: an operator-set continent + country |
| -# pair ("EU · Germany", via STATUS_BOX_REGION) — never a city, datacenter, |
| -# IP, or hostname. Unset → the field is omitted entirely (the page shows |
| -# nothing); a region is never guessed from the box. |
| +# A box's `region` is COARSE BY DESIGN: a continent + country pair |
| +# ("EU · Finland") — never a city, datacenter, IP, or hostname. Resolved |
| +# from the box's own cloud metadata (see box_region below); the |
| +# STATUS_BOX_REGION env overrides; unknown → the field is omitted |
| +# entirely. Detected, mapped, or absent — never guessed. |
| # |
| # books.json — AGGREGATED INCOME ONLY. Active-subscription count (by tier — |
| # tier names are the public price book), MRR total in EUR, gross collected |
| @@ -81,9 +82,36 @@ MEMINFO=${STATUS_MEMINFO:-/proc/meminfo} |
| DATA_DIR=${STATUS_DATA_DIR:-/data} |
| # Coarse public region of this box — continent + country ONLY ("EU · Germany"); |
| # see PRIVACY above. Optional: unset omits the field from fleet.json. Per-box |
| -# regions will come from mesh heartbeats when multi-box lands; this env is the |
| -# single-box seam until then. |
| -BOX_REGION=${STATUS_BOX_REGION:-} |
| +# regions will come from mesh heartbeats when multi-box lands. Resolution: |
| +# STATUS_BOX_REGION env (explicit override wins) → the box's own cloud |
| +# metadata service (Hetzner, then AWS IMDSv2), mapped zone→coarse region → |
| +# omitted entirely when nothing answers. Hand-set values drift (the box was |
| +# labelled Germany for a day; it lives in Helsinki) — asking the machine |
| +# where it is can't. The map stays coarse by design: continent + country. |
| +box_region() { |
| + if [ -n "${STATUS_BOX_REGION:-}" ]; then printf '%s' "$STATUS_BOX_REGION"; return; fi |
| + [ -n "$DRY_RUN" ] && return # dry-run never probes live box state |
| + local zone |
| + zone=$(curl -sm2 http://169.254.169.254/hetzner/v1/metadata/availability-zone 2>/dev/null || true) |
| + case "$zone" in |
| + hel*) printf 'EU · Finland'; return ;; |
| + fsn*|nbg*) printf 'EU · Germany'; return ;; |
| + ash*|hil*) printf 'NA · USA'; return ;; |
| + sin*) printf 'AS · Singapore'; return ;; |
| + esac |
| + # AWS: IMDSv2 (token required; both calls fail closed in ~2s off-cloud) |
| + local tok az |
| + tok=$(curl -sm2 -X PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60" 2>/dev/null || true) |
| + [ -n "$tok" ] && az=$(curl -sm2 -H "X-aws-ec2-metadata-token: $tok" http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null || true) |
| + case "$az" in |
| + eu-west-1) printf 'EU · Ireland' ;; |
| + eu-west-2) printf 'EU · UK' ;; |
| + eu-central-1) printf 'EU · Germany' ;; |
| + eu-north-1) printf 'EU · Sweden' ;; |
| + us-east-*|us-west-*) printf 'NA · USA' ;; |
| + *) : ;; # unknown zone: omit, never guess |
| + esac |
| +} |
| STATUS_ENV=${STATUS_ENV_FILE:-/data/status.env} |
| PAGES_DIR=${STATUS_PAGES_DIR:-/opt/lchat/status-pages} |
| STRIPE_API=${STATUS_STRIPE_API:-https://api.stripe.com} |
| @@ -331,6 +359,10 @@ build_fleet() { |
| # never guessed; escaped like the version strings so a quote can't break |
| # out of the SQL literal. |
| local boxes region_kv="" |
| + # Resolved here, not at file top: DRY_RUN/argv must be parsed first so the |
| + # dry-run's no-live-probes guard actually guards (and the dry-run fixture's |
| + # direct BOX_REGION= assignment still wins over a re-resolve). |
| + [ -z "${BOX_REGION:-}" ] && BOX_REGION=$(box_region) |
| if [ -n "$BOX_REGION" ]; then |
| region_kv=", 'region', '$(printf '%s' "$BOX_REGION" | sed "s/'/''/g")'" |
| fi |