# 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}" # nameindexregionip, 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 '' (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" }