mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
- .devcontainer/Dockerfile: add PostgreSQL client libs, OpenSSL/libffi headers, gfortran/BLAS for scipy, rclone; install full Python dependency set (boto3, psycopg2-binary, fastapi, uvicorn, notion-client, httpx, pytest, numpy, scipy, etc.) in uv-managed venv; add rclone S3 gateway init script as ENTRYPOINT - .devcontainer/devcontainer.json: switch from build to pre-built image (localhost/research
159 lines
6.4 KiB
Bash
159 lines
6.4 KiB
Bash
#!/usr/bin/env bash
|
|
# garage-node-bootstrap.sh
|
|
#
|
|
# Installs Garage v2.3.0 on a remote Tailscale node, drops a config derived
|
|
# from garage.node-template.toml, copies the cluster secret, and enables the
|
|
# systemd service. Runs entirely over SSH.
|
|
#
|
|
# Usage:
|
|
# bash garage-node-bootstrap.sh <tailscale-ip> [ssh-user] [ssh-port]
|
|
#
|
|
# Prerequisites on the target node:
|
|
# - SSH key access (run: ssh-copy-id -i ~/.ssh/id_ed25519 user@ip)
|
|
# - systemd
|
|
# - curl or wget (for binary download)
|
|
#
|
|
# After all nodes are bootstrapped, run garage-cluster-init.sh to assign
|
|
# layout roles and wire the cluster together.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
NODE_IP="${1:?Usage: $0 <tailscale-ip> [user] [port]}"
|
|
SSH_USER="${2:-allaun}"
|
|
SSH_PORT="${3:-22}"
|
|
SSH_KEY="${HOME}/.ssh/id_ed25519"
|
|
SSH="ssh -i ${SSH_KEY} -p ${SSH_PORT} -o ConnectTimeout=15 -o BatchMode=yes -o StrictHostKeyChecking=accept-new"
|
|
|
|
GARAGE_VERSION="v2.3.0"
|
|
GARAGE_URL="https://garagehq.deuxfleurs.fr/_releases/${GARAGE_VERSION}/x86_64-unknown-linux-musl/garage"
|
|
PRIMARY_NODE_ID="3e08a71b73fa2b1099301844d1f199caab50f7a9209b9929d9bfb2bfeeb937f4"
|
|
CLUSTER_SECRET=$(sudo cat /etc/garage/cluster-secret)
|
|
|
|
log() { echo "[garage-bootstrap] $*" >&2; }
|
|
|
|
# ── 1. Detect node arch and pick binary ───────────────────────────────────────
|
|
log "Probing ${SSH_USER}@${NODE_IP}..."
|
|
ARCH=$($SSH "${SSH_USER}@${NODE_IP}" "uname -m" 2>&1)
|
|
case "$ARCH" in
|
|
x86_64) ARCH_TRIPLE="x86_64-unknown-linux-musl" ;;
|
|
aarch64) ARCH_TRIPLE="aarch64-unknown-linux-musl" ;;
|
|
armv6l) ARCH_TRIPLE="armv6l-unknown-linux-musleabihf" ;;
|
|
*)
|
|
log "ERROR: Unknown arch ${ARCH} — add to case statement"
|
|
exit 1
|
|
;;
|
|
esac
|
|
BINARY_URL="https://garagehq.deuxfleurs.fr/_releases/${GARAGE_VERSION}/${ARCH_TRIPLE}/garage"
|
|
log " arch: ${ARCH} → ${ARCH_TRIPLE}"
|
|
|
|
# ── 2. Probe available data directories ───────────────────────────────────────
|
|
DATA_PATH=$($SSH "${SSH_USER}@${NODE_IP}" bash -s << 'REMOTE'
|
|
# Find the mount with the most free space that isn't / or tmpfs
|
|
df -h --output=target,avail 2>/dev/null \
|
|
| grep -v "^Filesystem\|tmpfs\|devtmpfs\|/boot\|/run\|/sys\|/dev" \
|
|
| sort -k2 -h | tail -1 | awk '{print $1}'
|
|
REMOTE
|
|
)
|
|
# Fall back to / if nothing better found
|
|
DATA_PATH="${DATA_PATH:-/}"
|
|
log " data path: ${DATA_PATH}"
|
|
|
|
META_DIR="${DATA_PATH}/var/lib/garage/meta"
|
|
DATA_DIR="${DATA_PATH}/var/lib/garage/data"
|
|
[ "$DATA_PATH" = "/" ] && META_DIR="/var/lib/garage/meta" && DATA_DIR="/var/lib/garage/data"
|
|
|
|
# ── 3. Install on remote ───────────────────────────────────────────────────────
|
|
log "Installing Garage ${GARAGE_VERSION} on ${NODE_IP}..."
|
|
$SSH "${SSH_USER}@${NODE_IP}" bash -s << REMOTE
|
|
set -euo pipefail
|
|
# Download binary
|
|
curl -sL "${BINARY_URL}" -o /tmp/garage --max-time 120
|
|
chmod +x /tmp/garage
|
|
sudo mv /tmp/garage /usr/local/bin/garage
|
|
garage --version
|
|
|
|
# Create directories and user
|
|
sudo useradd -r -s /bin/false -d /var/lib/garage garage 2>/dev/null || true
|
|
sudo mkdir -p "${META_DIR}" "${DATA_DIR}" /etc/garage /var/log/garage
|
|
sudo chown -R garage:garage /var/lib/garage /etc/garage /var/log/garage 2>/dev/null || true
|
|
echo "Garage installed"
|
|
REMOTE
|
|
|
|
# ── 4. Copy cluster secret ────────────────────────────────────────────────────
|
|
log "Copying cluster secret..."
|
|
echo "${CLUSTER_SECRET}" | $SSH "${SSH_USER}@${NODE_IP}" \
|
|
"sudo tee /etc/garage/cluster-secret > /dev/null && sudo chmod 600 /etc/garage/cluster-secret"
|
|
|
|
# ── 5. Generate admin token and drop config ───────────────────────────────────
|
|
NODE_ADMIN_TOKEN=$(openssl rand -hex 32)
|
|
CONFIG=$(sed \
|
|
-e "s|NODE_TAILSCALE_IP|${NODE_IP}|g" \
|
|
-e "s|META_DIR|${META_DIR}|g" \
|
|
-e "s|DATA_DIR|${DATA_DIR}|g" \
|
|
-e "s|PRIMARY_NODE_ID|${PRIMARY_NODE_ID}|g" \
|
|
"${SCRIPT_DIR}/garage.node-template.toml")
|
|
|
|
echo "${CONFIG}" | $SSH "${SSH_USER}@${NODE_IP}" \
|
|
"sudo tee /etc/garage/garage.toml > /dev/null"
|
|
echo "GARAGE_ADMIN_TOKEN=${NODE_ADMIN_TOKEN}" | $SSH "${SSH_USER}@${NODE_IP}" \
|
|
"sudo tee /etc/garage/garage.env > /dev/null && sudo chmod 600 /etc/garage/garage.env"
|
|
|
|
# ── 6. Install and start systemd service ──────────────────────────────────────
|
|
log "Installing systemd service on ${NODE_IP}..."
|
|
cat "${SCRIPT_DIR}/garage.service" | $SSH "${SSH_USER}@${NODE_IP}" \
|
|
"sudo tee /etc/systemd/system/garage.service > /dev/null"
|
|
|
|
$SSH "${SSH_USER}@${NODE_IP}" bash -s << 'REMOTE'
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable garage
|
|
sudo systemctl start garage
|
|
sleep 2
|
|
sudo systemctl status garage --no-pager | head -8
|
|
REMOTE
|
|
|
|
# ── 7. Get node ID and print connect command ───────────────────────────────────
|
|
log "Fetching node ID..."
|
|
sleep 2
|
|
NODE_ID=$($SSH "${SSH_USER}@${NODE_IP}" \
|
|
"sudo garage -c /etc/garage/garage.toml --admin-token ${NODE_ADMIN_TOKEN} node id 2>/dev/null | head -1")
|
|
|
|
log ""
|
|
log "Bootstrap complete for ${NODE_IP}."
|
|
log "Node ID: ${NODE_ID}"
|
|
log ""
|
|
log "To connect this node to the cluster, run on qfox-1:"
|
|
log " sudo garage -c /etc/garage/garage.toml node connect ${NODE_ID}"
|
|
log ""
|
|
log "Then run: bash garage-cluster-init.sh (after all nodes are bootstrapped)"
|
|
|
|
# Append to node registry
|
|
python3 - << PYEOF
|
|
import json, os
|
|
from datetime import datetime, timezone
|
|
|
|
registry_path = "${SCRIPT_DIR}/../node-registry.json"
|
|
entry = {
|
|
"hostname": "${NODE_IP}",
|
|
"tailscale_ip": "${NODE_IP}",
|
|
"ssh_user": "${SSH_USER}",
|
|
"ssh_port": ${SSH_PORT},
|
|
"garage_node_id": "${NODE_ID}",
|
|
"garage_data_dir": "${DATA_DIR}",
|
|
"added_at": datetime.now(timezone.utc).isoformat()
|
|
}
|
|
|
|
registry = []
|
|
if os.path.exists(registry_path):
|
|
with open(registry_path) as f:
|
|
try:
|
|
registry = json.load(f)
|
|
except Exception:
|
|
registry = []
|
|
|
|
registry = [r for r in registry if r.get("tailscale_ip") != entry["tailscale_ip"]]
|
|
registry.append(entry)
|
|
with open(registry_path, "w") as f:
|
|
json.dump(registry, f, indent=2)
|
|
print(f"Registered {entry['tailscale_ip']} in node-registry.json")
|
|
PYEOF
|