Ship edge-activate.sh: workstation-side activation + pool verification
One-shot watcher for the workstation: probes ping.occupymedia.cc through a reachable edge IP, flips the four hostnames after the port-forward verifies, checks the public path for 6 minutes and auto-reverts to TUNNEL if the new pool assignment is broken too. Retires itself if the tunnel heals first. README updated to match.
This commit is contained in:
parent
9365b561b2
commit
161f06f1e9
2 changed files with 143 additions and 7 deletions
|
|
@ -55,13 +55,20 @@ Router: forward TCP 443 (80 optional) → the CT's LAN IP.
|
||||||
|
|
||||||
## Activation
|
## Activation
|
||||||
|
|
||||||
The workstation watcher (`edge-activate.sh`, not shipped here) probes
|
The workstation watcher (`edge-activate.sh`) probes `ping.occupymedia.cc`
|
||||||
`ping.occupymedia.cc` through a reachable edge IP. Two consecutive 200s after
|
through a reachable edge IP. Two consecutive 200s after the port-forward goes
|
||||||
the port-forward goes live → it sets `EDGE_ENABLED=true`, flips the four
|
live → it sets `EDGE_ENABLED=true`, flips the four hostnames, verifies the
|
||||||
hostnames, verifies the public path for 6 minutes, and **reverts to TUNNEL
|
public path for 6 minutes, and **reverts to TUNNEL automatically** if the new
|
||||||
automatically** if the new pool assignment is broken too. The CT watcher then
|
pool assignment is broken too. The CT watcher then owns ongoing failover (2
|
||||||
owns ongoing failover (2 tunnel failures → ORIGIN/GREY; 3 origin failures →
|
tunnel failures → ORIGIN/GREY; 3 origin failures → TUNNEL).
|
||||||
TUNNEL).
|
|
||||||
|
Run it from a machine that (a) has the repo `.env` with the Cloudflare token,
|
||||||
|
(b) can SSH to the CT, and (c) is OUTSIDE the affected region if you need to
|
||||||
|
distinguish "edge pool broken" from "origin unreachable":
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TARGET_MODE=ORIGIN ./edge-activate.sh # or GREY for direct DNS
|
||||||
|
```
|
||||||
|
|
||||||
Target mode: `TARGET_MODE=ORIGIN` (default) or `TARGET_MODE=GREY` in
|
Target mode: `TARGET_MODE=ORIGIN` (default) or `TARGET_MODE=GREY` in
|
||||||
`watcher.env` / the activation environment.
|
`watcher.env` / the activation environment.
|
||||||
|
|
|
||||||
129
infra/occupymedia/edge/edge-activate.sh
Executable file
129
infra/occupymedia/edge/edge-activate.sh
Executable file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# edge-activate.sh — one-shot activation: flips the four hostnames to ORIGIN mode
|
||||||
|
# once the router port-forward verifiably works THROUGH CLOUDFLARE (the real
|
||||||
|
# production path: working edge pool -> CF -> origin:443 -> nginx -> apps).
|
||||||
|
#
|
||||||
|
# Probe: https://ping.occupymedia.cc (proxied A -> public IP). nginx returns 200
|
||||||
|
# "edge-ok" only when the whole chain works. While the forward is down CF
|
||||||
|
# answers 521 (origin unreachable), which counts as not-live.
|
||||||
|
#
|
||||||
|
# Also keeps ping's A record synced to the current public IP (the IP is dynamic).
|
||||||
|
# Retires itself if the tunnel path heals first (flip not needed).
|
||||||
|
set -uo pipefail
|
||||||
|
source /home/moxexley/projects/SuperPoster/.env 2>/dev/null || { echo "no .env"; exit 1; }
|
||||||
|
|
||||||
|
ZONE="4bd0ac2e13a735d2e0068ce3feea2996"
|
||||||
|
HOSTS=(app api tube chat)
|
||||||
|
TARGET_MODE=${TARGET_MODE:-ORIGIN} # ORIGIN (CF-proxied) or GREY (direct DNS)
|
||||||
|
LOG=/tmp/opencode/edge-activate.log
|
||||||
|
SSH="ssh -i $HOME/.ssh/devserver -o ConnectTimeout=10 root@proxnode2"
|
||||||
|
|
||||||
|
log() { echo "$(date -u '+%H:%M:%S') $*" >> "$LOG"; }
|
||||||
|
|
||||||
|
get_ip() { $SSH "curl -s -m 8 ifconfig.me" 2>/dev/null; }
|
||||||
|
|
||||||
|
probe_edge() { curl -s -4 -o /dev/null -w '%{http_code}' -m 20 \
|
||||||
|
--resolve "ping.occupymedia.cc:443:104.21.59.202" https://ping.occupymedia.cc 2>/dev/null; }
|
||||||
|
|
||||||
|
revert_to_tunnel() {
|
||||||
|
T="Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
||||||
|
ZURL="https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records"
|
||||||
|
for h in "${HOSTS[@]}"; do
|
||||||
|
aid=$(curl -s -m 15 -H "$T" "$ZURL?type=A&name=$h.occupymedia.cc" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')")
|
||||||
|
[[ -n $aid ]] && curl -s -m 15 -X DELETE -H "$T" "$ZURL/$aid" > /dev/null
|
||||||
|
cid=$(curl -s -m 15 -H "$T" "$ZURL?type=CNAME&name=$h.occupymedia.cc" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')")
|
||||||
|
if [[ -z $cid ]]; then
|
||||||
|
curl -s -m 15 -X POST -H "$T" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"CNAME\",\"name\":\"$h.occupymedia.cc\",\"content\":\"e5695d8b-94eb-4e2b-9600-ed3cbf20e541.cfargotunnel.com\",\"proxied\":true}" \
|
||||||
|
"$ZURL" > /dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
$SSH "pct exec 150 -- bash -c 'cd /opt/anarchoposter/infra/occupymedia/edge && sed -i \"s/^EDGE_ENABLED=.*/EDGE_ENABLED=false/\" watcher.env && echo \"mode=TUNNEL failures=0 tunnel_ok=0 origin_ok=1 ip=$1\" > edge.state'" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
probe_default() { curl -s -o /dev/null -w '%{http_code}' -m 8 https://app.occupymedia.cc 2>/dev/null; }
|
||||||
|
|
||||||
|
ping_record_id() { curl -s -m 15 -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records?type=A&name=ping.occupymedia.cc" \
|
||||||
|
| python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')" 2>/dev/null; }
|
||||||
|
|
||||||
|
sync_ping_ip() { # $1 = current public IP
|
||||||
|
local rid content
|
||||||
|
rid=$(ping_record_id)
|
||||||
|
[[ -n $rid ]] || { log "ping record missing — recreating"; \
|
||||||
|
curl -s -m 15 -X POST -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"type\":\"A\",\"name\":\"ping.occupymedia.cc\",\"content\":\"$1\",\"proxied\":true,\"ttl\":60}" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records" > /dev/null; return; }
|
||||||
|
content=$(curl -s -m 15 -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
|
||||||
|
"https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$rid" \
|
||||||
|
| python3 -c "import json,sys; print(json.load(sys.stdin)['result']['content'])" 2>/dev/null)
|
||||||
|
if [[ $content != "$1" ]]; then
|
||||||
|
curl -s -m 15 -X PATCH -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" \
|
||||||
|
-d "{\"content\":\"$1\"}" "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records/$rid" > /dev/null
|
||||||
|
log "ping A synced $content -> $1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
fails=0
|
||||||
|
while true; do
|
||||||
|
sleep 60
|
||||||
|
ip=$(get_ip)
|
||||||
|
[[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || { log "ip lookup failed"; continue; }
|
||||||
|
sync_ping_ip "$ip"
|
||||||
|
|
||||||
|
default_code=$(probe_default)
|
||||||
|
if [[ $default_code == 200 || $default_code == 307 ]]; then
|
||||||
|
log "tunnel path healed (default=$default_code) — activation not needed, retiring."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
code=$(probe_edge)
|
||||||
|
if [[ $code == 200 ]]; then
|
||||||
|
fails=$((fails+1))
|
||||||
|
log "edge probe via CF -> $code (live, $fails/2)"
|
||||||
|
else
|
||||||
|
fails=0
|
||||||
|
log "edge probe via CF -> $code (forward not live yet)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $fails -ge 2 ]]; then
|
||||||
|
log "origin path confirmed live via Cloudflare ($code x2) — ACTIVATING as $TARGET_MODE"
|
||||||
|
$SSH "pct exec 150 -- sed -i 's/^EDGE_ENABLED=.*/EDGE_ENABLED=true/; s/^TARGET_MODE=.*/TARGET_MODE=$TARGET_MODE/' /opt/anarchoposter/infra/occupymedia/edge/watcher.env" >/dev/null 2>&1
|
||||||
|
if [[ $TARGET_MODE == GREY ]]; then
|
||||||
|
$SSH "pct exec 150 -- bash -c 'cd /opt/anarchoposter/infra/occupymedia/edge && echo OPEN > firewall.mode && ./edge-firewall.sh'" >/dev/null 2>&1
|
||||||
|
log "firewall relaxed to OPEN (grey mode: visitors connect directly)"
|
||||||
|
fi
|
||||||
|
proxied=true
|
||||||
|
[[ $TARGET_MODE == GREY ]] && proxied=false
|
||||||
|
T="Authorization: Bearer $CLOUDFLARE_API_TOKEN"
|
||||||
|
ZURL="https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records"
|
||||||
|
for h in "${HOSTS[@]}"; do
|
||||||
|
cid=$(curl -s -m 15 -H "$T" "$ZURL?type=CNAME&name=$h.occupymedia.cc" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')")
|
||||||
|
[[ -n $cid ]] && curl -s -m 15 -X DELETE -H "$T" "$ZURL/$cid" > /dev/null
|
||||||
|
aid=$(curl -s -m 15 -H "$T" "$ZURL?type=A&name=$h.occupymedia.cc" | python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(r[0]['id'] if r else '')")
|
||||||
|
if [[ -n $aid ]]; then
|
||||||
|
curl -s -m 15 -X PATCH -H "$T" -H "Content-Type: application/json" -d "{\"content\":\"$ip\",\"proxied\":$proxied}" "$ZURL/$aid" > /dev/null
|
||||||
|
else
|
||||||
|
curl -s -m 15 -X POST -H "$T" -H "Content-Type: application/json" -d "{\"type\":\"A\",\"name\":\"$h.occupymedia.cc\",\"content\":\"$ip\",\"proxied\":$proxied}" "$ZURL" > /dev/null
|
||||||
|
fi
|
||||||
|
log "$h -> A $ip proxied=$proxied"
|
||||||
|
done
|
||||||
|
$SSH "pct exec 150 -- bash -c 'cd /opt/anarchoposter/infra/occupymedia/edge && echo \"mode=$TARGET_MODE failures=0 tunnel_ok=0 origin_ok=1 ip=$ip\" > edge.state'" >/dev/null 2>&1
|
||||||
|
log "DNS flipped to $TARGET_MODE mode. Verifying public path for 6 min..."
|
||||||
|
verified=0
|
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
||||||
|
sleep 30
|
||||||
|
dc=$(probe_default)
|
||||||
|
if [[ $dc == 200 || $dc == 307 ]]; then verified=1; break; fi
|
||||||
|
log "post-flip check $i/12: default=$dc"
|
||||||
|
done
|
||||||
|
if [[ $verified == 1 ]]; then
|
||||||
|
log "PUBLIC PATH VERIFIED — $TARGET_MODE mode live, site reachable. CT watcher owns failover."
|
||||||
|
else
|
||||||
|
log "default path STILL broken post-flip — $TARGET_MODE pool assignment is dead too. REVERTING to TUNNEL."
|
||||||
|
revert_to_tunnel "$ip"
|
||||||
|
log "Reverted to TUNNEL + watcher gated off. If guaranteed reachability is required: rerun with TARGET_MODE=GREY (direct DNS, origin IP exposed, firewall OPEN)."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
Loading…
Add table
Add a link
Reference in a new issue