infra/cloud-devnet: hcloud (doctl variant) create, builder-VM provision from a git-archive source tarball, systemd units for igneumd --devnet-suffix with a sparse --addpeer mesh and a CPU trickle miner per node, stdlib wRPC client, experiments (latency, partition, hop, collect, observer hookup), README with the command sequence and the Hetzner API prices of 3 Oct 2026. infra/gpu-bench: RunPod image recipes (CUDA 12.8, ROCm), bundle, run.sh (vectors gate, 10-min raw, sweep, inline shortcut ratio, nvcc/NVRTC/OpenCL recompile timings, results row, intake upload), bench-log template. infra/seed-nodes: create-seed (persistent IPv4, firewall), provision on the VM, health check, addPeer from the Mac over grpcurl, seeds.txt; igneum-seed-1 created at 188.245.5.161 (Hetzner cx23, fsn1). docs/plans/cloud-devnet.md and docs/plans/seed-nodes.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
91 lines
4.2 KiB
Bash
91 lines
4.2 KiB
Bash
# Shared helpers for infra/cloud-devnet. Source this; it sources config.sh.
|
|
# Needs bash 3.2 or newer (the Mac's /bin/bash is fine), ssh, scp, python3.
|
|
|
|
set -euo pipefail
|
|
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REPO="$(cd "$HERE/../.." && pwd)"
|
|
export REPO
|
|
# shellcheck source=../config.sh
|
|
. "$HERE/config.sh"
|
|
|
|
NODES_FILE="${NODES_FILE:-$HERE/nodes.tsv}" # name<TAB>index<TAB>region<TAB>ip, one line per node, written by create.sh
|
|
BUILD_DIR="$HERE/build"
|
|
BIN_DIR="$BUILD_DIR/bin"
|
|
RESULTS_DIR="$HERE/results"
|
|
|
|
# Hetzner API token: the only line of ~/.config/igneum/hetzner-token (never printed, never in the repo).
|
|
if [ -z "${HCLOUD_TOKEN:-}" ] && [ -s "${HETZNER_TOKEN_FILE:-$HOME/.config/igneum/hetzner-token}" ]; then
|
|
HCLOUD_TOKEN="$(head -1 "${HETZNER_TOKEN_FILE:-$HOME/.config/igneum/hetzner-token}" | tr -d '[:space:]')"; export HCLOUD_TOKEN
|
|
fi
|
|
|
|
log() { printf '%s %s\n' "$(date -u +%H:%M:%S)" "$*"; }
|
|
die() { log "ERROR: $*" >&2; exit 1; }
|
|
need() { command -v "$1" >/dev/null 2>&1 || die "$1 is not installed ($2)"; }
|
|
|
|
# Hetzner location name for node index i (1-based) and the DigitalOcean one.
|
|
region_of() {
|
|
local i="$1" list
|
|
if [ "$PROVIDER" = digitalocean ]; then list="$DO_REGIONS"; else list="$REGIONS"; fi
|
|
local n; n=$(printf '%s' "$list" | tr ',' '\n' | grep -c .)
|
|
printf '%s' "$list" | tr ',' '\n' | sed -n "$(( (i - 1) % n + 1 ))p"
|
|
}
|
|
|
|
node_name() { printf '%s-%02d' "$PREFIX" "$1"; }
|
|
|
|
# Server type for a Hetzner location: SERVER_TYPE if set, else the TYPE_BY_LOCATION map, else cx23.
|
|
type_for_location() {
|
|
if [ -n "$SERVER_TYPE" ]; then printf '%s' "$SERVER_TYPE"; return; fi
|
|
local t; t=$(printf '%s' "$TYPE_BY_LOCATION" | tr ',' '\n' | awk -F= -v l="$1" '$1 == l { print $2 }')
|
|
printf '%s' "${t:-cx23}"
|
|
}
|
|
|
|
# nodes.tsv accessors
|
|
require_nodes() { [ -s "$NODES_FILE" ] || die "no $NODES_FILE: run create.sh first"; }
|
|
node_count() { grep -c . "$NODES_FILE"; }
|
|
node_ip() { awk -F'\t' -v n="$1" '$1 == n || $2 == n { print $4; exit }' "$NODES_FILE"; }
|
|
node_region() { awk -F'\t' -v n="$1" '$1 == n || $2 == n { print $3; exit }' "$NODES_FILE"; }
|
|
node_names() { cut -f1 "$NODES_FILE"; }
|
|
nodes_in_region() { awk -F'\t' -v r="$1" '$3 == r { print $1 }' "$NODES_FILE"; }
|
|
regions_present() { cut -f3 "$NODES_FILE" | sort -u; }
|
|
|
|
# Outbound peers of node index i: the ring neighbour i+1 and chords at i + k*N/MESH_OUT (1-based, wrapping).
|
|
# With MESH_OUT=2 that is i+1 and i+N/2; inbound links double it, so each node sees about 4 peers.
|
|
peers_of() {
|
|
local i="$1" n; n=$(node_count)
|
|
local k step idx out=""
|
|
for k in $(seq 1 "$MESH_OUT"); do
|
|
if [ "$k" = 1 ]; then step=1; else step=$(( (k - 1) * n / MESH_OUT )); fi
|
|
idx=$(( (i - 1 + step) % n + 1 ))
|
|
[ "$idx" = "$i" ] && continue
|
|
out="$out $(node_ip "$idx"):$P2P_PORT"
|
|
done
|
|
printf '%s' "$out" | sed 's/^ //' | tr ' ' ','
|
|
}
|
|
|
|
SSH_OPTS=(-i "$SSH_KEY_FILE" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$HERE/build/known_hosts" -o ConnectTimeout=15 -o BatchMode=yes -o ServerAliveInterval=30)
|
|
nssh() { local ip="$1"; shift; ssh "${SSH_OPTS[@]}" "$SSH_USER@$ip" "$@"; }
|
|
nscp() { scp -q "${SSH_OPTS[@]}" "$@"; }
|
|
|
|
# Run a command on every node in parallel (PAR at a time), prefixing output with the node name.
|
|
# usage: on_all '<shell command>' (the command runs on the node through ssh)
|
|
on_all() {
|
|
local cmd="$1" par="${PAR:-10}"
|
|
mkdir -p "$BUILD_DIR"
|
|
node_names | xargs -P "$par" -I{} sh -c '
|
|
name="$1"; cmd="$2"; ip=$(awk -F"\t" -v n="$name" "\$1 == n { print \$4; exit }" "'"$NODES_FILE"'")
|
|
ssh -i "'"$SSH_KEY_FILE"'" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="'"$HERE/build/known_hosts"'" -o ConnectTimeout=15 -o BatchMode=yes "'"$SSH_USER"'@$ip" "$cmd" 2>&1 | sed "s/^/[$name] /"
|
|
' _ {} "$cmd"
|
|
}
|
|
|
|
# One RPC call on a node through the loopback wRPC JSON endpoint (node/wrpc.py is installed by provision.sh).
|
|
rpc() { local node="$1" method="$2" params="${3:-{\}}"; nssh "$(node_ip "$node")" "python3 /opt/igneum/bin/wrpc.py call $method '$params'"; }
|
|
|
|
results_dir_for() { local d="$RESULTS_DIR/${1:-$(date -u +%Y-%m-%d)}"; mkdir -p "$d"; printf '%s' "$d"; }
|
|
|
|
genesis_bits_decimal() { printf '%d' "$GENESIS_BITS"; }
|
|
|
|
confirm() {
|
|
if [ "${YES:-0}" = 1 ]; then return 0; fi
|
|
printf '%s [type yes]: ' "$1"; read -r a; [ "$a" = yes ] || die "not confirmed"
|
|
}
|