diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 00000000..5d000b26 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,97 @@ +# infra/ + +Scripts for renting and provisioning a Scaleway Elastic Metal server for +benchmark / feature-testing work. + +| Script | Runs on | Purpose | +|----------------------------|---------|--------------------------------------------------------------------------------------------------------| +| `rent_baremetal.sh ` | local | Creates the server (hourly billing, Debian, fr-par-2) and hands off to `provision_server.sh`. | +| `provision_server.sh ` | local | Waits for sshd, runs `provision.sh` on the server over SSH. Re-runnable. | +| `provision.sh` | remote | Installs toolchain, creates `admin`/`app` users, clones `lambda_vm`, hardens sshd. | + +## Prerequisites + +1. Install `scw` and `jq`: + ```bash + brew install scw jq # macOS + ``` + +2. Create the `vm` scw profile (script refuses any other profile name): + ```bash + scw init --profile vm + ``` + +## Rent + provision a new server + +```bash +infra/rent_baremetal.sh test-1 +``` + +End to end in one command — creates the server, waits for both +`status=ready` and `install.status=completed`, then provisions it (apt +packages, `admin`/`app` users, Rust toolchain, gh CLI, Claude Code, +lambda-vm sysroot, repo clone, ssh hardening). + +Use a unique name (Scaleway rejects duplicates): + +```bash +infra/rent_baremetal.sh +``` + +After it finishes, log in as: + +```bash +ssh admin@ # passwordless sudo +ssh app@ # workload user, no sudo, has ~/lambda_vm cloned +``` + +Root SSH is disabled at the end of provisioning. + +## Re-provision an existing server + +If `provision.sh` failed partway, or you want to re-apply changes, point +`provision_server.sh` at the IP directly. It's idempotent. + +```bash +# Before hardening (root still works): +infra/provision_server.sh + +# After hardening (root SSH is dead, use admin): +SSH_USER=admin infra/provision_server.sh +``` + +The wrapper switches to `sudo bash -s` automatically when `SSH_USER` isn't +root. + +## Configuration + +Everything has a working default; override via env var only when needed. + +| Var | Default | Used by | Notes | +|---|---|---|---| +| `SCW_TYPE` | `EM-I320E-NVME` | `rent_baremetal.sh` | Scaleway commercial type. Must have an `hourly` offer in `$SCW_ZONE` or the script refuses. | +| `SCW_ZONE` | `fr-par-2` | `rent_baremetal.sh` | One of `fr-par-1`, `fr-par-2`, `nl-ams-1`, `nl-ams-2`, `pl-waw-2`, `pl-waw-3`. | +| `SCW_OS_ID` | `83640d93-...` (Debian 12) | `rent_baremetal.sh` | Must have `cloud_init_supported: true`. | +| `SCW_PROJECT_ID` | `946cfb34-...` (lambda_vm) | `rent_baremetal.sh` | Determines which scw IAM SSH keys get installed. | +| `READY_TIMEOUT` | `1800` (s) | `rent_baremetal.sh` | How long to wait for `status=ready && install.status=completed`. | +| `PROVISION_FILE` | `/provision.sh` | both wrappers | Path to the remote provisioning script. | +| `SSH_USER` | `root` | `provision_server.sh` | Switch to `admin` for re-runs after sshd hardening. | + +### `SCW_TYPE` options + +| Type | CPU | RAM | Disk | Price (€/h) | +|---|---|---|---|---| +| `EM-I220E-NVME` | AMD EPYC 8124P (16c/32t @ 2.5 GHz) | 128 GB | 2× 960 GB NVMe | 0.548 | +| `EM-I320E-NVME` | AMD EPYC 8224P (24c/48t @ 2.5 GHz) | 192 GB | 2× 1.92 TB NVMe | 0.822 (default) | +| `EM-I420E-NVME` | AMD EPYC 8324P (32c/64t @ 2.6 GHz) | 256 GB | 2× 1.92 TB NVMe | 1.096 | + +### `SCW_ZONE` options + +| Zone | Location | +|---|---| +| `fr-par-1` | Paris, France | +| `fr-par-2` | Paris, France (default) | +| `nl-ams-1` | Amsterdam, Netherlands | +| `nl-ams-2` | Amsterdam, Netherlands | +| `pl-waw-2` | Warsaw, Poland | +| `pl-waw-3` | Warsaw, Poland | diff --git a/infra/provision.sh b/infra/provision.sh new file mode 100755 index 00000000..405fd38d --- /dev/null +++ b/infra/provision.sh @@ -0,0 +1,181 @@ +#!/bin/bash +# Provision a freshly rented Scaleway Elastic Metal Debian server. +# Invoked remotely from infra/provision_server.sh as: +# ssh root@ bash -s < infra/provision.sh +# +# Idempotent — safe to re-run. + +set -euo pipefail + +log() { printf '\n=== %s ===\n' "$*"; } + +# --- 1. apt update + upgrade ------------------------------------------------- +log "apt update + upgrade" +export DEBIAN_FRONTEND=noninteractive +APT_OPTS=(-y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold) + +# Scaleway baremetal Debian ships grub-cloud-amd64; its postinst (fired as a +# trigger by initramfs-tools / shim-signed / kernel upgrades) runs grub-install +# against an ext2 root and fails ("will not proceed with blocklists"). The +# package isn't load-bearing on UEFI baremetal — purge it before any upgrade. +apt-get purge -y grub-cloud-amd64 2>/dev/null || true + +apt-get update -y +apt-get upgrade "${APT_OPTS[@]}" + +# --- 2. apt packages --------------------------------------------------------- +log "apt install base packages + clang/lld/llvm + xz-utils" +apt-get install "${APT_OPTS[@]}" \ + ca-certificates curl wget gnupg vim git zip unzip openssl libssl-dev jq \ + build-essential rsyslog htop rsync pkg-config locales ufw \ + clang lld llvm xz-utils + +# --- 3. users: admin (sudo) + app (no sudo) ---------------------------------- +log "users: admin (sudo) + app (no sudo)" +for u in admin app; do + if ! id "$u" >/dev/null 2>&1; then + useradd -m -s /bin/bash "$u" + fi +done +echo 'admin ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/90-admin +chmod 0440 /etc/sudoers.d/90-admin + +# --- 4. authorized_keys for admin and app ------------------------------------ +log "authorized_keys: propagate root's keys + append hardcoded team keys" +if [ ! -s /root/.ssh/authorized_keys ]; then + echo "ERROR: /root/.ssh/authorized_keys missing or empty — refusing to harden sshd." >&2 + exit 1 +fi +TEAM_KEYS=( + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFzvQKhE/xqRxHbit/dZNej7T5eVLmF8CAGL7to6o3QY joaquin@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA2GAeixuqP4XwujuSK9KDgdmyglGzlQQsXztnve+bra gabriel@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKQnPPUb4gzmsmjDP98mNKXbpHrp9bIIL7QiRjyWEG6f julian@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBzniAUYGJXguBjfz2+uGUUC7XLVmk58FhCsEBMx2r5k mauro@mail.com" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ6mrcWIyU+/LrNZLivNIOYr6ld/CXefoq1hyXLsHDfV it" +) +for u in admin app; do + install -d -m 0700 -o "$u" -g "$u" "/home/$u/.ssh" + install -m 0600 -o "$u" -g "$u" /root/.ssh/authorized_keys "/home/$u/.ssh/authorized_keys" + AUTH_FILE="/home/$u/.ssh/authorized_keys" + if [ -n "$(tail -c 1 "$AUTH_FILE")" ]; then + printf '\n' >> "$AUTH_FILE" + fi + for key in "${TEAM_KEYS[@]}"; do + if ! grep -qxF "$key" "$AUTH_FILE"; then + printf '%s\n' "$key" >> "$AUTH_FILE" + fi + done + chown "$u:$u" "$AUTH_FILE" +done + +# --- 5. GitHub CLI (gh) ----------------------------------------------------- +if ! command -v gh >/dev/null 2>&1; then + log "installing gh (GitHub CLI)" + mkdir -p -m 755 /etc/apt/keyrings + out=$(mktemp) + wget -nv -O "$out" https://cli.github.com/packages/githubcli-archive-keyring.gpg + cat "$out" > /etc/apt/keyrings/githubcli-archive-keyring.gpg + chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg + mkdir -p -m 755 /etc/apt/sources.list.d + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list + apt-get update -y + apt-get install "${APT_OPTS[@]}" gh +fi + +# --- 6. Rust toolchain for app (1.94.0 default + nightly-2026-02-01 + src) --- +log "Rust 1.94.0 + nightly-2026-02-01 (rust-src) for app" +sudo -u app -H bash -se <<'APP_RUST' +set -euo pipefail +if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain 1.94.0 --profile default +fi +export PATH="$HOME/.cargo/bin:$PATH" +grep -q 'cargo/env' "$HOME/.bashrc" 2>/dev/null \ + || echo '. "$HOME/.cargo/env"' >> "$HOME/.bashrc" +rustup toolchain install nightly-2026-02-01 --profile minimal --component rust-src +rustup component add rust-analyzer +APP_RUST + +# --- 7. Claude Code for app ------------------------------------------------- +log "Claude Code for app" +sudo -u app -H bash -se <<'APP_CLAUDE' +set -euo pipefail +export PATH="$HOME/.local/bin:$PATH" +if ! command -v claude >/dev/null 2>&1; then + curl -fsSL https://claude.ai/install.sh | bash +fi +PATH_LINE='export PATH="$HOME/.local/bin:$PATH"' +grep -qxF "$PATH_LINE" "$HOME/.bashrc" 2>/dev/null \ + || printf '%s\n' "$PATH_LINE" >> "$HOME/.bashrc" +APP_CLAUDE + +# --- 8. lambda-vm sysroot (rv64im) ------------------------------------------ +SYSROOT_DIR=/opt/lambda-vm-sysroot +SYSROOT_URL=https://lambda.alignedlayer.com/lambda-vm-sysroot-rv64im.tar.gz +if [ ! -d "$SYSROOT_DIR" ]; then + log "downloading sysroot to $SYSROOT_DIR" + curl -L "$SYSROOT_URL" -o /tmp/sysroot.tar.gz + mkdir -p /opt + tar -xzf /tmp/sysroot.tar.gz -C /opt + rm /tmp/sysroot.tar.gz +fi + +# --- 9. Clone lambda_vm (as app, public repo over HTTPS) --------------------- +REPO_DIR=/home/app/lambda_vm +REPO_URL=https://github.com/yetanotherco/lambda_vm.git +if [ ! -d "$REPO_DIR/.git" ]; then + log "cloning lambda_vm to $REPO_DIR (as app)" + sudo -u app -H git clone "$REPO_URL" "$REPO_DIR" +fi + +# --- 10. ethrex test fixture ------------------------------------------------ +ETHREX_FILE=/home/app/lambda_vm/executor/tests/ethrex_hoodi.bin +ETHREX_URL=https://lambda.alignedlayer.com/ethrex_hoodi.bin +if [ -d /home/app/lambda_vm/executor/tests ] && [ ! -f "$ETHREX_FILE" ]; then + log "downloading ethrex_hoodi.bin" + sudo -u app -H curl -L "$ETHREX_URL" -o "$ETHREX_FILE" +fi + +# --- 11. ufw firewall (default deny in, allow out, only ssh in) ------------- +log "ufw: default deny in / allow out, allow ssh (22/tcp) only" +ufw --force reset >/dev/null +ufw default deny incoming +ufw default allow outgoing +ufw allow 22/tcp +ufw --force enable + +# --- 12. /etc/environment + locale ------------------------------------------ +log "writing /etc/environment" +cat > /etc/environment <<'EOF' +LANG=en_US.UTF-8 +LC_ALL=C +LANGUAGE=en_US.UTF-8 +LC_TYPE=en_US.UTF-8 +LC_CTYPE=en_US.UTF-8 +EOF +locale-gen en_US.UTF-8 + +# --- 13. sshd hardening (last; reload won't drop existing session) ---------- +log "writing /etc/ssh/sshd_config.d/99-hardening.conf" +cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' +PermitRootLogin no +PasswordAuthentication no +AllowAgentForwarding no +AllowTcpForwarding no +PubkeyAuthentication yes +MaxAuthTries 5 +LoginGraceTime 30 +ClientAliveInterval 300 +ClientAliveCountMax 2 +X11Forwarding no +PermitEmptyPasswords no +PermitUserEnvironment no +LogLevel VERBOSE +EOF +chmod 0644 /etc/ssh/sshd_config.d/99-hardening.conf +sshd -t +systemctl reload ssh + +log "Done. Log in as admin@ (sudo) or app@ (no sudo). Root SSH is disabled." diff --git a/infra/provision_server.sh b/infra/provision_server.sh new file mode 100755 index 00000000..110455c3 --- /dev/null +++ b/infra/provision_server.sh @@ -0,0 +1,74 @@ +#!/bin/bash +# Run infra/provision.sh on a remote Scaleway baremetal server over SSH. +# Safe to run standalone after rent_baremetal.sh, or to re-provision an +# existing server. +# +# Usage: infra/provision_server.sh +# +# Env var overrides (all optional): +# SSH_USER default: root +# First-run servers accept root SSH; once provision.sh +# has hardened sshd, re-run as: SSH_USER=admin ... +# PROVISION_FILE default: /provision.sh +# +# SSH wait is indefinite — Ctrl+C to abort. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +SSH_USER="${SSH_USER:-root}" +PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" + +err() { echo -e "${RED}error:${NC} $*" >&2; } +info() { echo -e "${BOLD}$*${NC}"; } +ok() { echo -e "${GREEN}$*${NC}"; } + +if [ $# -lt 1 ] || [ -z "${1:-}" ]; then + err "missing " + echo "Usage: $0 " >&2 + exit 2 +fi +IP="$1" + +if [ ! -r "$PROVISION_FILE" ]; then + err "provision script not found or unreadable: $PROVISION_FILE" + exit 1 +fi +if ! command -v ssh >/dev/null 2>&1; then + err "ssh not found on PATH." + exit 1 +fi + +SSH_OPTS=(-o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 -o BatchMode=yes) + +info "Waiting for sshd on $SSH_USER@$IP (indefinite — Ctrl+C to abort)..." +attempt=1 +while ! ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" true 2>/dev/null; do + if [ $((attempt % 6)) -eq 0 ]; then + echo -e " ${YELLOW}still waiting (attempt $attempt, ~$((attempt * 10))s elapsed)${NC}" + fi + attempt=$((attempt + 1)) + sleep 10 +done +ok "sshd reachable on $SSH_USER@$IP (attempt $attempt)" + +if [ "$SSH_USER" = "root" ]; then + REMOTE_CMD="bash -s" +else + REMOTE_CMD="sudo bash -s" +fi + +info "Running $PROVISION_FILE on $SSH_USER@$IP..." +ssh "${SSH_OPTS[@]}" "$SSH_USER@$IP" "$REMOTE_CMD" < "$PROVISION_FILE" + +echo +ok "Provisioning complete." +echo " ssh admin@$IP # sudo" +echo " ssh app@$IP # no sudo" diff --git a/infra/rent_baremetal.sh b/infra/rent_baremetal.sh new file mode 100755 index 00000000..858318a4 --- /dev/null +++ b/infra/rent_baremetal.sh @@ -0,0 +1,190 @@ +#!/bin/bash +# Rent a Scaleway Elastic Metal server with HOURLY billing. +# +# Usage: infra/rent_baremetal.sh +# +# Env var overrides (all optional): +# SCW_ZONE default: fr-par-2 +# SCW_TYPE default: EM-I320E-NVME +# PROVISION_FILE default: /infra/provision.sh +# READY_TIMEOUT default: 1800 (seconds) +# +# Requires: scw, jq, ssh. +# To stop hourly charges when done: scw baremetal server delete zone= + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +SCW_ZONE="${SCW_ZONE:-fr-par-2}" +SCW_TYPE="${SCW_TYPE:-EM-I320E-NVME}" +SCW_OS_ID="${SCW_OS_ID:-83640d93-a0b8-45ad-9c9f-30cae48380a4}" # Debian +SCW_PROJECT_ID="${SCW_PROJECT_ID:-946cfb34-d351-48c4-8566-127e7727e15f}" +PROVISION_FILE="${PROVISION_FILE:-$SCRIPT_DIR/provision.sh}" +READY_TIMEOUT="${READY_TIMEOUT:-1800}" + +err() { echo -e "${RED}error:${NC} $*" >&2; } +info() { echo -e "${BOLD}$*${NC}"; } +ok() { echo -e "${GREEN}$*${NC}"; } + +if [ $# -lt 1 ] || [ -z "${1:-}" ]; then + err "missing " + echo "Usage: $0 " >&2 + exit 2 +fi +SERVER_NAME="$1" + +if ! command -v scw >/dev/null 2>&1; then + err "scw CLI not found on PATH. Install: https://github.com/scaleway/scaleway-cli" + exit 1 +fi +if ! command -v jq >/dev/null 2>&1; then + err "jq not found on PATH." + exit 1 +fi +if [ ! -r "$PROVISION_FILE" ]; then + err "provision script not found or unreadable: $PROVISION_FILE" + exit 1 +fi +if ! command -v ssh >/dev/null 2>&1; then + err "ssh not found on PATH." + exit 1 +fi + +EXPECTED_PROFILE="vm" +if [ -n "${SCW_PROFILE:-}" ]; then + ACTIVE_PROFILE="$SCW_PROFILE" +elif [ -r "$HOME/.config/scw/config.yaml" ]; then + ACTIVE_PROFILE=$(grep -E '^active_profile:' "$HOME/.config/scw/config.yaml" \ + | head -n1 | awk '{print $2}' | tr -d '"' | tr -d "'") +else + ACTIVE_PROFILE="" +fi +if [ "$ACTIVE_PROFILE" != "$EXPECTED_PROFILE" ]; then + err "active scw profile is '${ACTIVE_PROFILE:-}', expected '$EXPECTED_PROFILE'" + echo " Switch with: export SCW_PROFILE=$EXPECTED_PROFILE" >&2 + echo " Or set 'active_profile: $EXPECTED_PROFILE' in ~/.config/scw/config.yaml" >&2 + exit 1 +fi +ok "Using scw profile: $EXPECTED_PROFILE" + +info "Resolving hourly offer ID for type=$SCW_TYPE zone=$SCW_ZONE..." +OFFER_JSON=$(scw baremetal offer list \ + zone="$SCW_ZONE" \ + name="$SCW_TYPE" \ + subscription-period=hourly \ + -o json) +OFFER_ID=$(echo "$OFFER_JSON" | jq -r --arg n "$SCW_TYPE" '[.[] | select(.name == $n)] | .[0].id // empty') +if [ -z "$OFFER_ID" ]; then + err "no hourly offer named '$SCW_TYPE' in $SCW_ZONE — refusing to create a non-hourly server" + echo "$OFFER_JSON" >&2 + exit 1 +fi +ok "Hourly offer ID: $OFFER_ID" + +info "Enumerating SSH keys in project $SCW_PROJECT_ID..." +SSH_KEYS_JSON=$(scw iam ssh-key list project-id="$SCW_PROJECT_ID" -o json) +SSH_KEY_IDS=() +while IFS= read -r line; do + [ -n "$line" ] && SSH_KEY_IDS+=("$line") +done < <(echo "$SSH_KEYS_JSON" | jq -r '.[].id') +if [ ${#SSH_KEY_IDS[@]} -eq 0 ]; then + err "no SSH keys found in project $SCW_PROJECT_ID — register one first ('scw iam ssh-key create')" + exit 1 +fi +ok "Found ${#SSH_KEY_IDS[@]} SSH key(s) to install" + +SSH_KEY_ARGS=() +for i in "${!SSH_KEY_IDS[@]}"; do + SSH_KEY_ARGS+=("common-configuration.install.ssh-key-ids.${i}=${SSH_KEY_IDS[$i]}") +done + +info "Creating server name=$SERVER_NAME via batch-create..." +CREATE_JSON=$(scw baremetal server batch-create \ + zone="$SCW_ZONE" \ + common-configuration.offer-id="$OFFER_ID" \ + common-configuration.project-id="$SCW_PROJECT_ID" \ + common-configuration.name="$SERVER_NAME" \ + common-configuration.install.os-id="$SCW_OS_ID" \ + common-configuration.install.hostname="$SERVER_NAME" \ + "${SSH_KEY_ARGS[@]}" \ + servers.0.hostname="$SERVER_NAME" \ + -o json) + +SERVER_ID=$(echo "$CREATE_JSON" | jq -r '.servers[0].id // empty') +if [ -z "$SERVER_ID" ]; then + err "server create did not return an id. Raw response:" + echo "$CREATE_JSON" >&2 + exit 1 +fi +ok "Created server id=$SERVER_ID. Waiting up to ${READY_TIMEOUT}s for status=ready AND install.status=completed..." + +DEADLINE=$(( $(date +%s) + READY_TIMEOUT )) +LAST_STATE="" +while [ "$(date +%s)" -lt "$DEADLINE" ]; do + GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) + STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') + INSTALL_STATUS=$(echo "$GET_JSON" | jq -r '.install.status // "pending"') + STATE="$STATUS / install=$INSTALL_STATUS" + if [ "$STATE" != "$LAST_STATE" ]; then + echo -e " ${YELLOW}$STATE${NC}" + LAST_STATE="$STATE" + else + echo -n "." + fi + if [ "$STATUS" = "ready" ] && [ "$INSTALL_STATUS" = "completed" ]; then + echo + break + fi + if [ "$STATUS" = "error" ] || [ "$STATUS" = "locked" ]; then + echo + err "server entered terminal status: $STATUS" + echo "$GET_JSON" >&2 + exit 1 + fi + if [ "$INSTALL_STATUS" = "error" ]; then + echo + err "install entered terminal status: $INSTALL_STATUS" + echo "$GET_JSON" >&2 + exit 1 + fi + sleep 15 +done + +GET_JSON=$(scw baremetal server get "$SERVER_ID" zone="$SCW_ZONE" -o json) +FINAL_STATUS=$(echo "$GET_JSON" | jq -r '.status // empty') +FINAL_INSTALL=$(echo "$GET_JSON" | jq -r '.install.status // "pending"') +if [ "$FINAL_STATUS" != "ready" ] || [ "$FINAL_INSTALL" != "completed" ]; then + err "timed out after ${READY_TIMEOUT}s — last status=$FINAL_STATUS install=$FINAL_INSTALL" + exit 1 +fi + +PUBLIC_IP=$(echo "$GET_JSON" | jq -r '.ips[] | select(.version == "IPv4") | .address' | head -n1) + +echo +ok "Server ready." +echo " id: $SERVER_ID" +echo " name: $SERVER_NAME" +echo " zone: $SCW_ZONE" +echo " type: $SCW_TYPE (hourly offer $OFFER_ID)" +echo " ip: $PUBLIC_IP" +echo + +info "Wiping any stale known_hosts entry for $PUBLIC_IP (Scaleway recycles IPs)..." +ssh-keygen -R "$PUBLIC_IP" >/dev/null 2>&1 || true + +info "Handing off to provision_server.sh (Ctrl+C to skip and provision later)..." +PROVISION_FILE="$PROVISION_FILE" SSH_USER=root "$SCRIPT_DIR/provision_server.sh" "$PUBLIC_IP" + +echo +echo "To stop hourly charges:" +echo " scw baremetal server delete $SERVER_ID zone=$SCW_ZONE" +echo +echo "To re-provision this server later (sshd is hardened, so admin + sudo):" +echo " SSH_USER=admin infra/provision_server.sh $PUBLIC_IP"