Skip to content

Commit a8c1055

Browse files
xzyaoiclaude
andauthored
Infra Improvements (#51)
* clean up & security fix * fix relay peers not properly cleanup * feat(deploy): add Jinja2 templates for node config and systemd unit * feat(deploy): add Ansible inventory and config for test mesh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(deploy): add Ansible playbook with all 5 plays Implements deploy/test/deploy.yml with plays for building a signed binary locally, deploying to all hosts, discovering bootstrap multiaddrs, deploying final configs with peer bootstraps, and verifying mesh formation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Cloudflare DNS + OpenTofu design spec Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update spec with origin rule, SSL mode, and reviewer fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: expand spec to include docs.opentela.ai management Adds R2 bucket, Worker custom domain, and DNS record for docs site to OpenTofu config. Worker code deployment stays with wrangler. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add OpenTofu implementation plan for Cloudflare infrastructure Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cloudflare): add .gitignore for OpenTofu state and secrets * chore(cloudflare): add variable declarations * feat(cloudflare): add provider, bootstraps DNS/origin rule/SSL, and docs infrastructure * chore(cloudflare): add output definitions * docs(cloudflare): add README with setup instructions * fix(cloudflare): add required ttl field to DNS records Cloudflare provider v5 requires ttl explicitly. Value 1 = auto (managed by Cloudflare) for proxied records. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(cloudflare): complete OpenTofu deployment - Remove docs DNS record (auto-created by Worker custom domain) - Add ttl=1 for proxied DNS records (provider v5 requirement) - Add dependency lock file All 6 resources applied: - bootstraps.opentela.ai: 2x A records + origin rule (port 8092) + SSL rule - docs.opentela.ai: R2 bucket + Worker custom domain Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix reviews * feat(protocol): add PublicPort field to Peer struct Peers need to carry their own TCP port so bootstrap address construction uses the correct port per-peer (relays use 18905, heads use 43905). PublicPort is propagated in UpdateNodeTable alongside PublicAddress. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(protocol): add role config field and RoleRelay SWIM constant Introduces 'role' config (worker/head/relay) so nodes can self-identify. Adds RoleRelay to SWIM metadata for relay membership propagation. Default role is 'worker' for backwards compatibility. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(protocol): relay nodes auto-register as bootstrap peers Relays with public-addr automatically advertise as bootstraps. InitializeMyself sets role and PublicPort from config. MarkSelfAsBootstrap triggers for role=relay in addition to public-addr. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(protocol): use per-peer port in bootstrap address construction ConnectedBootstraps() was using the local node's tcpport for all peers, producing wrong multiaddrs for relays on different ports (e.g., 18905). Extracts BuildBootstrapAddr() as a testable pure function. Falls back to local tcpport for un-upgraded peers without PublicPort. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(bootstrap): add bootstraps.opentela.ai as primary bootstrap source Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(deploy): add role config to all deploy configs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document role config field and bootstrap domain endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(protocol): add minimal entries for unknown connected peers The ping handler and ConnectedF callback were ignoring peers not yet in the node table (added in 2c069a2 to avoid creating entries without build attestation). This created a chicken-and-egg problem: fresh nodes could never learn about peers because the fast path (gossip messages) ignored them, and the slow path (CRDT DAG walk) could take minutes for deep history. Now, when we receive a gossip message from a peer that is actually connected at the libp2p level, or when a new libp2p connection is established, we create a minimal entry in the node table. The full record (with build attestation) will overwrite this when the CRDT PutHook fires. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(protocol): use peer.Decode for connectedness check in ConnectedBootstraps peer.ID(stringValue) does not produce a valid peer.ID from a string representation — it just casts the raw bytes. Use peer.Decode() which properly parses the base58/CID-encoded peer ID string. This was causing ConnectedBootstraps() to always see NotConnected for remote peers, making the endpoint only return the local node's own address. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(protocol): stop writing liveness updates to CRDT store The 30-second verification ticker was writing Connected/LastSeen for every peer to the CRDT store, creating ~12,000 DAG entries per day. Fresh nodes joining the network had to walk this entire history via bitswap before they could discover any peers, taking 20+ minutes. Liveness updates (Connected, LastSeen, stale peer marking) now go to the in-memory node table only via UpdateNodeTableHook. Structural changes (join, leave, service registration, bootstrap) still go through CRDT store.Put for replication. Also removes the periodic ReannounceLocalServices() from the 2-minute maintenance ticker — it was writing to CRDT every 2 minutes even when nothing changed. Reannouncement still happens on actual reconnects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(deploy): use domain-only bootstrap in all configs All deploy configs now use only https://bootstraps.opentela.ai/v1/dnt/bootstraps as their bootstrap source. No hardcoded relay multiaddrs or peer IDs. Nodes discover each other dynamically: - Head nodes register themselves via public-addr - Relay nodes register via role=relay + public-addr - Workers and relays fetch the aggregated list from the domain endpoint This eliminates the need to update configs when relay IPs or peer IDs change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(routing): connect to workers via relay circuit when not directly reachable When the head node forwards a request to a worker it's not directly connected to (e.g., worker behind HPC firewall), it now tries relay circuit addresses through any connected peer. This enables the flow: client → head node → relay (circuit) → worker EnsureConnected() first tries the peerstore's known addresses, then constructs /p2p/<relay>/p2p-circuit/p2p/<target> multiaddrs through each connected peer until one succeeds. The proxy handler calls EnsureConnected before forwarding, returning 502 Bad Gateway with a clear error if the worker is truly unreachable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(p2p): only force public reachability for nodes with public-addr ForceReachabilityPublic() tells libp2p the node is publicly reachable, which prevents it from making relay reservations. Workers behind HPC firewalls need libp2p to detect they're private so it automatically reserves slots on relay nodes, enabling circuit relay connections. Now only nodes with public-addr set (head nodes, relays) force public reachability. Workers without public-addr let AutoNAT detect their reachability, enabling automatic relay reservation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(p2p): enable AutoRelay for workers to make relay reservations Workers without public-addr are forced to ForceReachabilityPrivate and use EnableAutoRelayWithPeerSource to automatically find relay servers among connected peers and maintain active reservations. This is required for relay circuit routing: the relay returns NO_RESERVATION (204) when a head node tries to connect to a worker through it, unless the worker has previously reserved a slot. The peer source callback offers all connected peers as relay candidates. Peers running EnableRelayService() (all nodes) will accept reservations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(p2p): explicitly make relay reservations for workers AutoRelay wasn't making reservations due to timing issues (peer source callback fires before host has connections). Instead, workers now explicitly call client.Reserve() on every connected peer after bootstrap and on reconnects. This ensures the relay has an active reservation so head nodes can connect to workers via circuit relay. MakeRelayReservations() runs 10s after bootstrap (to let connections establish) and 5s after each reconnect. Only runs for workers (nodes without public-addr). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(routing): relay-hop routing for unreachable workers Replace relay circuit stream approach (which times out due to p2phttp over circuit limitations) with HTTP relay-hop routing: When the head node can't directly reach a worker, it forwards the request through a connected relay node's /v1/p2p/<worker>/ handler: client → head (HTTP) → relay (libp2p) → worker (libp2p) The relay receives the request via its P2P listener and forwards it to the worker using its own direct libp2p connection. No relay circuit or reservation needed for this path — just standard p2phttp between directly connected peers at each hop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(protocol): use os.UserHomeDir instead of homedir.Dir for key storage homedir.Dir() from github.com/mitchellh/go-homedir reads /etc/passwd and ignores the HOME env var. This caused relay nodes (which override HOME=/tmp/opentela-relay in start-relay.sh) to still read/write keys from the real home directory on Lustre, producing inconsistent peer IDs across sessions. os.UserHomeDir() (Go stdlib) respects the HOME environment variable, so the relay's key file is now correctly stored under the overridden home directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(routing): don't evict relay-connected workers from provider list Two fixes for workers behind relays: 1. GetAllProviders: Include all peers with matching services regardless of connectivity status. Workers behind relays appear as disconnected from the head's perspective but are reachable via relay-hop routing. 2. Stale peer cleanup: Skip peers with registered services when marking as disconnected or removing from the table. They're actively providing workloads and reachable through relays even if we can't ping directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(crdt): use default GossipSub params instead of oversized values The hardcoded GossipSub parameters (D=128, Dlo=16, Dhi=256) prevented mesh formation in networks with fewer than 16 peers. With only 3-5 nodes (2 heads + 1 relay + workers), the Dlo=16 threshold was never met, causing GossipSub to never form a proper mesh for the CRDT topic. This meant CRDT data from workers never propagated through the relay to cloud head nodes. Reverts to Go-libp2p default params (D=6, Dlo=4, Dhi=12) which work correctly for any network size. The scalability.crdt_tuned override is preserved for large deployments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(crdt): remove ClosePeer loop that disrupted GossipSub mesh The close-all-peers-and-reconnect pattern during CRDT initialization was disrupting GossipSub mesh topology. After closing all connections, the ping topic (ocf-crdt-net) would reform because it actively publishes every 20s, but the CRDT topic (ocf-crdt) mesh never reformed because it only publishes on data changes. This caused CRDT data from workers to never propagate through the relay to cloud head nodes. Removing the ClosePeer loop preserves the GossipSub mesh that was already established during host creation. The IPFS-lite bootstrap still runs to ensure bitswap connectivity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(crdt): bootstrap before joining GossipSub topics for proper mesh formation GossipSub exchanges topic subscriptions with already-connected peers. When topics were joined before bootstrap (peers not yet connected), the relay never saw cloud nodes as CRDT topic subscribers, creating a unidirectional mesh where CRDT data never propagated from relay to cloud. Fix: bootstrap and wait 3s for connections to establish BEFORE joining GossipSub topics. This ensures peers are connected when topic subscriptions are exchanged, forming a proper bidirectional mesh. Also adds diagnostic logging to PubSubBroadcaster (topic peer count on broadcast, received message source on receive). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(routing): prefer relay-role peers for relay-hop routing FindRelayFor now prefers peers with role=relay in the node table, since relay nodes bridge network segments and are most likely connected to unreachable workers. Previously it picked any connected peer, which could route through a head node that also can't reach the worker. Also includes GossipSub mesh fix (bootstrap before topic join) and diagnostic logging in PubSubBroadcaster. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(routing): workers advertise their relay for precise routing Workers now store the relay peer ID in their CRDT entry (RelayPeer field) after successfully making a relay reservation. Head nodes use this to route requests through the correct relay: head → libp2p://<worker's relay>/v1/p2p/<worker>/<path> This solves the multi-relay problem: with Euler, Clariden, etc. relays, the head node needs to know which specific relay can reach each worker. The worker tells it by advertising RelayPeer in the CRDT. FindRelayFor checks the worker's RelayPeer first, falling back to any connected relay-role peer. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: document relay-hop routing in routing tutorial Adds sections covering: - Direct vs relay-hop routing flow diagrams - How the head node detects unreachable workers and routes through relays - The relay_peer CRDT field and automatic relay registration - Zero-config worker bootstrap via domain endpoint Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(protocol): add GetSelf, SetMyselfForTest, and RegisterRemotePeer Expose the node's own Peer record via GetSelf(), provide a test helper SetMyselfForTest() for other packages, and add RegisterRemotePeer() to write a remote peer's entry into the CRDT store using the peer's own ID as the key (with Connected=false and a fresh LastSeen timestamp). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(protocol): include recently-registered relays in bootstrap list ConnectedBootstraps() now includes relay peers seen within the last 10 minutes (maxBootstrapAge), even when not currently P2P-connected. This ensures newly-registered relay nodes propagated via CRDT are usable as bootstrap addresses before a direct connection is established. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(server): add challenge-response registration endpoints Add challengePeer (GET /v1/dnt/challenge) and registerPeer (POST /v1/dnt/register) handlers with nonce-based challenge-response, RSA public key verification, build/identity attestation validation, and CRDT peer registration. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(server): add localhost-only /v1/self and /v1/sign endpoints Adds getSelf (GET /v1/self) returning this node's Peer struct and signData (POST /v1/sign) signing hex-encoded data with the node's libp2p private key, both restricted to loopback clients. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(server): wire registration and self/sign routes Register /v1/dnt/challenge and /v1/dnt/register under the crdtGroup gated by role=="head", and add /v1/self and /v1/sign to the v1 group. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(deploy): add challenge-response registration to relay startup Adds a register_relay function and background registration loop to start-relay.sh. After the relay becomes healthy, it fetches a challenge nonce from the bootstrap service, signs it with the relay's libp2p key, and POSTs the full self-info plus challenge_response to register. Uses exponential backoff (5s → 120s cap) on failure and re-registers every 5 minutes on success. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(deploy): remove hardcoded relay multiaddrs, add registration to Clariden All three deploy configs (ocf-1, ocf-2, euler worker) already had no hardcoded relay multiaddrs — only the bootstrap HTTP URL. Updated deploy/clariden/start-relay.sh to match deploy/euler/start-relay.sh: added IP detection, public-addr config patching, health-wait loop, register_relay() challenge-response function, and registration loop with exponential backoff. Adapted for Clariden's binary name (otela) and ports (18092/18905). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(server): harden registration against spoofed headers, unverified metadata, and invalid ports - Replace c.ClientIP() with c.Request.RemoteAddr in isLoopback() to prevent X-Forwarded-For spoofing on /v1/self and /v1/sign - Add nil check for P2P host in signData() to return 503 when node is not ready - Sanitize peer metadata before CRDT write: clear Owner/ProviderID when no identity attestation, restrict Role to exactly ["relay"], clear Service/Load/Hardware - Validate public_port as a numeric TCP port (1-65535) instead of just non-empty - Add TestGetSelf_SpoofedXForwardedFor to verify spoofed XFF is rejected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(server): add WebSocket transport via Cloudflare for firewall-restricted nodes - Add dedicated `wsport` config for a separate WebSocket listener port (43906), avoiding conflict with raw TCP multistream on the main libp2p port - Add `ws_domain` config (e.g., "p2p.opentela.ai") so ConnectedBootstraps() advertises WSS multiaddrs for all peers with a PublicAddress - Add Cloudflare DNS + origin rules for p2p.opentela.ai proxying 443 → 43906 - Nodes behind restrictive firewalls (e.g., JSC/JUWELS) can now connect via wss://p2p.opentela.ai:443, eliminating the need for cloudflared tunnels Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): harden concurrency, nil guards, nonce handling, and broadcaster logging - Add nil checks for GetP2PNode in MakeRelayReservations, IsDirectlyConnected, FindRelayFor to prevent panics when host is not ready - Defer nonce consumption in registration until after signature verification so attackers cannot burn legitimate nonces - Add myselfMu RWMutex to protect the global `myself` Peer from concurrent read/write across goroutines (ticker, reannounce, relay reservations) - Remove per-call debug logging from CRDT PubSubBroadcaster (ListPeers on every Broadcast was unnecessary overhead at scale) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * minor fix --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 47c7b99 commit a8c1055

59 files changed

Lines changed: 3856 additions & 4577 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

deploy/clariden/job.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/bash
2+
#SBATCH --job-name=opentela-sglang
3+
#SBATCH --account=infra02
4+
#SBATCH --nodes=1
5+
#SBATCH --ntasks-per-node=1
6+
#SBATCH --gpus-per-task=1
7+
#SBATCH --time=01:00:00
8+
#SBATCH --partition=debug
9+
#SBATCH --output=logs/opentela_%j.out
10+
#SBATCH --error=logs/opentela_%j.err
11+
12+
set -euo pipefail
13+
14+
# --- Configuration ---
15+
MODEL="Qwen/Qwen3-0.6B"
16+
SGLANG_PORT=30000
17+
OTELA_BINARY="$HOME/opentela/otela"
18+
OTELA_CONFIG="$HOME/.config/opentela/cfg.yaml"
19+
20+
# --- Start sglang server inside container ---
21+
echo "Starting sglang server for model $MODEL on port $SGLANG_PORT..."
22+
srun --environment=$HOME/.edf/sglang.toml \
23+
python3 -m sglang.launch_server \
24+
--model-path "$MODEL" \
25+
--port "$SGLANG_PORT" \
26+
--host 127.0.0.1 \
27+
--trust-remote-code \
28+
--skip-server-warmup &
29+
30+
SGLANG_PID=$!
31+
32+
# --- Wait for sglang to be ready ---
33+
echo "Waiting for sglang to be ready..."
34+
for i in $(seq 1 180); do
35+
if curl -s "http://localhost:${SGLANG_PORT}/v1/models" > /dev/null 2>&1; then
36+
echo "sglang is ready after ${i}s"
37+
break
38+
fi
39+
if ! kill -0 "$SGLANG_PID" 2>/dev/null; then
40+
echo "ERROR: sglang process died"
41+
exit 1
42+
fi
43+
sleep 1
44+
done
45+
46+
# Verify sglang is serving
47+
if ! curl -s "http://localhost:${SGLANG_PORT}/v1/models" > /dev/null 2>&1; then
48+
echo "ERROR: sglang failed to start within 180s"
49+
kill "$SGLANG_PID" 2>/dev/null || true
50+
exit 1
51+
fi
52+
53+
echo "sglang models:"
54+
curl -s "http://localhost:${SGLANG_PORT}/v1/models" | python3 -m json.tool 2>/dev/null || true
55+
56+
# --- Start OpenTela worker (native binary, no container) ---
57+
echo "Starting OpenTela worker..."
58+
"$OTELA_BINARY" start --config "$OTELA_CONFIG" &
59+
OTELA_PID=$!
60+
61+
echo "OpenTela worker started (PID=$OTELA_PID)"
62+
echo "sglang server running (PID=$SGLANG_PID)"
63+
echo "Job is running on $(hostname)"
64+
65+
# --- Wait for both processes ---
66+
wait -n "$SGLANG_PID" "$OTELA_PID" 2>/dev/null
67+
EXIT_CODE=$?
68+
69+
echo "A process exited with code $EXIT_CODE, shutting down..."
70+
kill "$SGLANG_PID" "$OTELA_PID" 2>/dev/null || true
71+
wait
72+
exit $EXIT_CODE

deploy/clariden/relay.cfg.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: clariden-relay
2+
seed: "199"
3+
port: "18092"
4+
tcpport: "18905"
5+
udpport: "18820"
6+
mode: full
7+
loglevel: debug
8+
cleanslate: true
9+
role: relay
10+
11+
security:
12+
require_signed_binary: false
13+
14+
solana:
15+
skip_verification: true
16+
17+
bootstrap:
18+
sources:
19+
- "https://bootstraps.opentela.ai/v1/dnt/bootstraps"

deploy/clariden/setup.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
# Setup script: build OpenTela for ARM64 and transfer to Clariden
3+
# Run this locally before submitting the SLURM job.
4+
#
5+
# Usage: bash deploy/clariden/setup.sh
6+
7+
set -euo pipefail
8+
9+
CLARIDEN_HOST="${CLARIDEN_HOST:-clariden}"
10+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11+
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
12+
13+
echo "=== Building OpenTela binary (linux/arm64) ==="
14+
cd "$PROJECT_DIR/src"
15+
GOOS=linux GOARCH=arm64 make build
16+
echo "Binary built: src/build/entry"
17+
18+
echo ""
19+
echo "=== Transferring to Clariden ==="
20+
ssh "${CLARIDEN_HOST}" 'mkdir -p ~/opentela ~/logs ~/.config/opentela ~/.edf'
21+
scp "$PROJECT_DIR/src/build/entry" "${CLARIDEN_HOST}:~/opentela/otela"
22+
scp "$SCRIPT_DIR/worker.cfg.yaml" "${CLARIDEN_HOST}:~/.config/opentela/cfg.yaml"
23+
scp "$SCRIPT_DIR/job.sh" "${CLARIDEN_HOST}:~/opentela/job.sh"
24+
scp "$SCRIPT_DIR/sglang.toml" "${CLARIDEN_HOST}:~/.edf/sglang.toml"
25+
ssh "${CLARIDEN_HOST}" 'chmod +x ~/opentela/otela'
26+
27+
echo ""
28+
echo "=== Done ==="
29+
echo "Files transferred to Clariden:"
30+
echo " ~/opentela/otela - OpenTela binary (arm64)"
31+
echo " ~/opentela/job.sh - SLURM job script"
32+
echo " ~/.config/opentela/cfg.yaml - Worker config"
33+
echo " ~/.edf/sglang.toml - sglang container EDF"
34+
echo ""
35+
echo "Next steps on Clariden:"
36+
echo " ssh ${CLARIDEN_HOST}"
37+
echo " cd ~/opentela && sbatch job.sh"
38+
echo " squeue -u \$USER # monitor the job"

deploy/clariden/sglang.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
image = "lmsysorg/sglang:latest"
2+
3+
mounts = [
4+
"/users/xyao:/users/xyao",
5+
"/iopsstor/scratch/cscs/xyao:/iopsstor/scratch/cscs/xyao",
6+
"/capstor:/capstor",
7+
]
8+
9+
workdir = "/users/xyao"
10+
11+
[env]
12+
HF_HOME="/capstor/store/cscs/swissai/a09/xyao/models"
13+
HF_TOKEN=""
14+
15+
[annotations]
16+
com.hooks.aws_ofi_nccl.enabled = "true"
17+
com.hooks.aws_ofi_nccl.variant = "cuda12"

deploy/clariden/start-relay.sh

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/bin/bash
2+
# Start the OpenTela relay node on Clariden login node (clariden-ln003)
3+
set -euo pipefail
4+
5+
REAL_HOME="$HOME"
6+
RELAY_HOME="/tmp/opentela-relay"
7+
CONFIG="$REAL_HOME/opentela/relay.cfg.yaml"
8+
LOGFILE="$REAL_HOME/opentela/relay.log"
9+
BINARY="$REAL_HOME/opentela/otela"
10+
RELAY_PORT=18092
11+
TCP_PORT=18905
12+
BOOTSTRAP_URL="https://bootstraps.opentela.ai"
13+
14+
pkill -f "opentela/otela.*relay" 2>/dev/null || true
15+
sleep 1
16+
17+
# Detect the primary IP of this login node.
18+
NODE_IP=$(hostname -I | awk '{print $1}')
19+
echo "Detected login node IP: $NODE_IP ($(hostname))"
20+
21+
# Patch public-addr in config to match this node's actual IP
22+
sed -i "s/^public-addr:.*/public-addr: \"${NODE_IP}\"/" "$CONFIG"
23+
24+
# Use /tmp for BadgerDB to avoid filesystem issues
25+
rm -rf "$RELAY_HOME"
26+
mkdir -p "$RELAY_HOME"
27+
28+
# Start relay with HOME overridden to /tmp
29+
nohup env HOME="$RELAY_HOME" "$BINARY" start --config "$CONFIG" > "$LOGFILE" 2>&1 &
30+
RELAY_PID=$!
31+
echo "Relay started with PID=$RELAY_PID (data dir: $RELAY_HOME)"
32+
33+
# Wait for relay to be healthy
34+
echo "Waiting for relay to be ready..."
35+
for i in $(seq 1 30); do
36+
curl -sf "http://localhost:${RELAY_PORT}/v1/health" > /dev/null 2>&1 && break
37+
if ! kill -0 "$RELAY_PID" 2>/dev/null; then
38+
echo "ERROR: relay process died"
39+
tail -30 "$LOGFILE"
40+
exit 1
41+
fi
42+
sleep 1
43+
done
44+
45+
tail -30 "$LOGFILE"
46+
47+
# --- Registration with bootstrap service ---
48+
49+
register_relay() {
50+
# Get self info (includes peer ID, build attestation, etc.)
51+
SELF=$(curl -sf "http://localhost:${RELAY_PORT}/v1/self")
52+
if [ -z "$SELF" ]; then
53+
echo "WARN: could not get self info"
54+
return 1
55+
fi
56+
PEER_ID=$(echo "$SELF" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
57+
58+
# Step 1: Get challenge nonce from head node
59+
CHALLENGE=$(curl -sf "${BOOTSTRAP_URL}/v1/dnt/challenge?peer_id=${PEER_ID}")
60+
if [ -z "$CHALLENGE" ]; then
61+
echo "WARN: challenge request failed"
62+
return 1
63+
fi
64+
NONCE=$(echo "$CHALLENGE" | python3 -c "import sys,json; print(json.load(sys.stdin)['nonce'])")
65+
66+
# Step 2: Sign nonce with libp2p key (via local endpoint)
67+
SIGNED=$(curl -sf -X POST "http://localhost:${RELAY_PORT}/v1/sign" \
68+
-H "Content-Type: application/json" \
69+
-d "{\"data\": \"${NONCE}\"}")
70+
if [ -z "$SIGNED" ]; then
71+
echo "WARN: signing failed"
72+
return 1
73+
fi
74+
SIGNATURE=$(echo "$SIGNED" | python3 -c "import sys,json; print(json.load(sys.stdin)['signature'])")
75+
PUBLIC_KEY=$(echo "$SIGNED" | python3 -c "import sys,json; print(json.load(sys.stdin)['public_key'])")
76+
77+
# Step 3: Register with bootstrap service
78+
PAYLOAD=$(echo "$SELF" | python3 -c "
79+
import sys, json
80+
peer = json.load(sys.stdin)
81+
peer['challenge_response'] = {
82+
'nonce': '${NONCE}',
83+
'signature': '${SIGNATURE}',
84+
'peer_id': '${PEER_ID}',
85+
'public_key': '${PUBLIC_KEY}'
86+
}
87+
json.dump(peer, sys.stdout)
88+
")
89+
RESULT=$(curl -sf -X POST "${BOOTSTRAP_URL}/v1/dnt/register" \
90+
-H "Content-Type: application/json" \
91+
-d "$PAYLOAD")
92+
if [ -z "$RESULT" ]; then
93+
echo "WARN: registration POST failed"
94+
return 1
95+
fi
96+
echo "Registered: $RESULT"
97+
return 0
98+
}
99+
100+
# Registration loop with exponential backoff
101+
BACKOFF=5
102+
while kill -0 "$RELAY_PID" 2>/dev/null; do
103+
if register_relay; then
104+
BACKOFF=5
105+
sleep 300 # re-register every 5 minutes
106+
else
107+
echo "Registration failed, retrying in ${BACKOFF}s"
108+
sleep $BACKOFF
109+
BACKOFF=$((BACKOFF * 2))
110+
[ $BACKOFF -gt 120 ] && BACKOFF=120
111+
fi
112+
done &
113+
REGISTER_PID=$!
114+
115+
echo "Registration loop running (PID=$REGISTER_PID)"
116+
echo "Relay is ready on ${NODE_IP}:${TCP_PORT}"

deploy/clariden/worker.cfg.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: clariden-worker
2+
seed: "200"
3+
port: "8092"
4+
tcpport: "43905"
5+
udpport: "59820"
6+
mode: full
7+
loglevel: debug
8+
cleanslate: true
9+
10+
service:
11+
name: llm
12+
port: "30000"
13+
14+
security:
15+
require_signed_binary: false
16+
17+
solana:
18+
skip_verification: true
19+
20+
bootstrap:
21+
sources:
22+
- "https://bootstraps.opentela.ai/v1/dnt/bootstraps"

deploy/cloudflare/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# OpenTofu / Terraform
2+
.terraform/
3+
*.tfstate
4+
*.tfstate.backup
5+
6+
# Secrets
7+
terraform.tfvars

deploy/cloudflare/.terraform.lock.hcl

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/cloudflare/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Cloudflare Infrastructure (OpenTofu)
2+
3+
Manages Cloudflare DNS records, origin rules, R2 bucket, and Worker routing for:
4+
- `bootstraps.opentela.ai` — round-robin HTTPS proxy to bootstrap nodes (port 8092)
5+
- `docs.opentela.ai` — Worker custom domain + R2 cache for the docs site
6+
7+
Worker code deployment is handled by `wrangler deploy` from `docs/`, not by OpenTofu.
8+
9+
## Prerequisites
10+
11+
- [OpenTofu](https://opentofu.org/docs/intro/install/) installed
12+
- Cloudflare API token with permissions: DNS:Edit, Zone Rulesets:Edit, R2:Edit, Workers:Edit
13+
- Cloudflare account ID and zone ID for `opentela.ai`
14+
15+
## Setup
16+
17+
1. Create `terraform.tfvars` (this file is git-ignored):
18+
19+
```hcl
20+
cloudflare_api_token = "your-api-token"
21+
cloudflare_account_id = "your-account-id"
22+
cloudflare_zone_id = "your-zone-id"
23+
```
24+
25+
2. Initialize:
26+
27+
```bash
28+
tofu init
29+
```
30+
31+
3. If adopting existing Cloudflare resources, import them first:
32+
33+
```bash
34+
# Find record IDs via Cloudflare dashboard or API
35+
tofu import cloudflare_r2_bucket.docs_cache <account_id>/opentela-docs-opennext-cache
36+
tofu import cloudflare_workers_custom_domain.docs <account_id>/docs.opentela.ai
37+
```
38+
39+
4. Review and apply:
40+
41+
```bash
42+
tofu plan
43+
tofu apply
44+
```
45+
46+
5. Commit the lock file:
47+
48+
```bash
49+
git add .terraform.lock.hcl
50+
git commit -m "chore(cloudflare): add dependency lock file"
51+
```
52+
53+
## Adding/Removing Bootstrap Nodes
54+
55+
Edit `terraform.tfvars`:
56+
57+
```hcl
58+
node_ips = ["140.238.223.116", "152.67.64.117", "NEW.IP.HERE"]
59+
```
60+
61+
Then: `tofu plan && tofu apply`

0 commit comments

Comments
 (0)