mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-08 01:51:14 +00:00
Consistently use log_step for progress/status messages ("Waiting for...",
"Fetching...", "Creating...") and reserve log_info for success/completion
messages. This gives users a clear visual distinction between operations
that are still running (cyan) vs operations that have completed (green).
Also adds periodic progress updates to silent polling loops in ramnode,
cherry, and netcup IP wait functions so users see activity during long waits.
Agent: ux-engineer
Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
273 lines
8 KiB
Bash
Executable file
273 lines
8 KiB
Bash
Executable file
#!/bin/bash
|
|
# Cherry Servers-specific functions for Spawn
|
|
|
|
# Source shared provider-agnostic functions
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../../shared/common.sh" ]]; then
|
|
source "$SCRIPT_DIR/../../shared/common.sh"
|
|
else
|
|
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)"
|
|
fi
|
|
|
|
# ============================================================
|
|
# Cherry Servers Configuration
|
|
# ============================================================
|
|
|
|
CHERRY_API_BASE="https://api.cherryservers.com/v1"
|
|
CHERRY_DEFAULT_PLAN="${CHERRY_DEFAULT_PLAN:-cloud_vps_1}"
|
|
CHERRY_DEFAULT_REGION="${CHERRY_DEFAULT_REGION:-eu_nord_1}"
|
|
CHERRY_DEFAULT_IMAGE="${CHERRY_DEFAULT_IMAGE:-Ubuntu 24.04 64bit}"
|
|
|
|
# ============================================================
|
|
# JSON Helpers
|
|
# ============================================================
|
|
|
|
# Extract a field from a JSON object via stdin
|
|
# Usage: echo '{"id": 123}' | _cherry_json_field "id"
|
|
_cherry_json_field() {
|
|
local field="$1"
|
|
python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
print(data.get(sys.argv[1], ''))
|
|
except:
|
|
pass
|
|
" "$field" 2>&1
|
|
}
|
|
|
|
# Extract the primary IP address from a Cherry server info response
|
|
# Usage: echo '{"ip_addresses":[...]}' | _cherry_extract_primary_ip
|
|
_cherry_extract_primary_ip() {
|
|
python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
for addr in data.get('ip_addresses', []):
|
|
if addr.get('type') == 'primary-ip':
|
|
print(addr.get('address', ''))
|
|
break
|
|
except:
|
|
pass
|
|
" 2>&1
|
|
}
|
|
|
|
# ============================================================
|
|
# API Wrapper
|
|
# ============================================================
|
|
|
|
# Cherry Servers API wrapper - delegates to generic_cloud_api for retry logic
|
|
cherry_api() {
|
|
local method="$1"
|
|
local endpoint="$2"
|
|
local body="${3:-}"
|
|
generic_cloud_api "$CHERRY_API_BASE" "$CHERRY_AUTH_TOKEN" "$method" "$endpoint" "$body"
|
|
}
|
|
|
|
# ============================================================
|
|
# Authentication
|
|
# ============================================================
|
|
|
|
# Test Cherry Servers API token
|
|
test_cherry_token() {
|
|
local response
|
|
response=$(cherry_api GET "/projects")
|
|
printf '%s' "$response" | grep -q '"id"'
|
|
}
|
|
|
|
# Get Cherry Servers API token
|
|
ensure_cherry_token() {
|
|
ensure_api_token_with_provider \
|
|
"Cherry Servers" \
|
|
"CHERRY_AUTH_TOKEN" \
|
|
"$HOME/.config/spawn/cherry.json" \
|
|
"https://portal.cherryservers.com/" \
|
|
test_cherry_token
|
|
}
|
|
|
|
# ============================================================
|
|
# SSH Key Management
|
|
# ============================================================
|
|
|
|
# Check if SSH key is registered with Cherry Servers
|
|
cherry_check_ssh_key() {
|
|
check_ssh_key_by_fingerprint cherry_api "/ssh-keys" "$1"
|
|
}
|
|
|
|
# Register SSH key with Cherry Servers
|
|
cherry_register_ssh_key() {
|
|
local key_name="$1"
|
|
local pub_path="$2"
|
|
local pub_key
|
|
pub_key=$(cat "$pub_path")
|
|
local json_pub_key
|
|
json_pub_key=$(json_escape "$pub_key")
|
|
local register_body="{\"label\":\"$key_name\",\"key\":$json_pub_key}"
|
|
local register_response
|
|
register_response=$(cherry_api POST "/ssh-keys" "$register_body")
|
|
|
|
if printf '%s' "$register_response" | grep -q '"id"'; then
|
|
return 0
|
|
else
|
|
log_error "Failed to register SSH key"
|
|
log_error "Response: $register_response"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Ensure SSH key exists and is registered with Cherry Servers
|
|
ensure_ssh_key() {
|
|
ensure_ssh_key_with_provider cherry_check_ssh_key cherry_register_ssh_key "Cherry Servers"
|
|
}
|
|
|
|
# ============================================================
|
|
# Server Management
|
|
# ============================================================
|
|
|
|
# Get project ID (required for server creation)
|
|
get_cherry_project_id() {
|
|
check_python_available
|
|
|
|
local projects
|
|
projects=$(cherry_api GET "/projects")
|
|
|
|
local project_id
|
|
project_id=$(printf '%s' "$projects" | python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
if isinstance(data, list) and len(data) > 0:
|
|
print(data[0].get('id', ''))
|
|
except: pass
|
|
" 2>&1)
|
|
|
|
if [[ -z "$project_id" ]]; then
|
|
log_error "No project found in Cherry Servers account"
|
|
log_error "Create a project at https://portal.cherryservers.com/"
|
|
return 1
|
|
fi
|
|
|
|
printf '%s' "$project_id"
|
|
}
|
|
|
|
# Get server name (generate or prompt)
|
|
get_server_name() {
|
|
local server_name="${CHERRY_SERVER_NAME:-}"
|
|
|
|
if [[ -z "$server_name" ]]; then
|
|
server_name="spawn-$(date +%s)"
|
|
fi
|
|
|
|
printf '%s' "$server_name"
|
|
}
|
|
|
|
# Create server
|
|
# Sets CHERRY_SERVER_ID and CHERRY_SERVER_IP as exports
|
|
# Poll the Cherry Servers API until the server has an IP address
|
|
# Sets CHERRY_SERVER_IP on success
|
|
_cherry_wait_for_ip() {
|
|
local server_id="$1"
|
|
log_step "Waiting for IP address assignment..."
|
|
local ip_address=""
|
|
local attempts=0
|
|
local max_attempts=60
|
|
|
|
while [[ -z "$ip_address" ]] && [[ $attempts -lt $max_attempts ]]; do
|
|
sleep "${POLL_INTERVAL}"
|
|
|
|
local server_info
|
|
server_info=$(cherry_api GET "/servers/${server_id}")
|
|
|
|
ip_address=$(printf '%s' "$server_info" | _cherry_extract_primary_ip)
|
|
|
|
attempts=$((attempts + 1))
|
|
if [[ -z "$ip_address" ]] && [[ $((attempts % 5)) -eq 0 ]]; then
|
|
log_step "Still waiting for IP address... (attempt ${attempts}/${max_attempts})"
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$ip_address" ]]; then
|
|
log_error "Failed to get server IP address after ${max_attempts} attempts"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Server IP: $ip_address"
|
|
CHERRY_SERVER_IP="$ip_address"
|
|
export CHERRY_SERVER_IP
|
|
}
|
|
|
|
create_server() {
|
|
local hostname="$1"
|
|
local plan="${CHERRY_DEFAULT_PLAN}"
|
|
local region="${CHERRY_DEFAULT_REGION}"
|
|
local image="${CHERRY_DEFAULT_IMAGE}"
|
|
|
|
check_python_available
|
|
|
|
# Validate env var inputs to prevent injection into Python code
|
|
validate_resource_name "$hostname" || { log_error "Invalid hostname"; return 1; }
|
|
validate_resource_name "$plan" || { log_error "Invalid CHERRY_DEFAULT_PLAN"; return 1; }
|
|
validate_resource_name "$region" || { log_error "Invalid CHERRY_DEFAULT_REGION"; return 1; }
|
|
|
|
local project_id
|
|
project_id=$(get_cherry_project_id) || return 1
|
|
|
|
log_step "Creating Cherry Servers server..."
|
|
log_info "Plan: $plan, Region: $region, Image: $image"
|
|
|
|
local payload
|
|
payload=$(python3 -c "
|
|
import json, sys
|
|
data = {
|
|
'plan': sys.argv[1],
|
|
'region': sys.argv[2],
|
|
'image': sys.argv[3],
|
|
'hostname': sys.argv[4],
|
|
'ssh_keys': [int(sys.argv[5])]
|
|
}
|
|
print(json.dumps(data))
|
|
" "$plan" "$region" "$image" "$hostname" "${CHERRY_SSH_KEY_ID}")
|
|
|
|
local response
|
|
response=$(cherry_api POST "/projects/${project_id}/servers" "$payload")
|
|
|
|
local server_id
|
|
server_id=$(printf '%s' "$response" | _cherry_json_field "id")
|
|
|
|
if [[ -z "$server_id" ]]; then
|
|
log_error "Failed to create server"
|
|
log_error "Response: $response"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Server created with ID: $server_id"
|
|
CHERRY_SERVER_ID="$server_id"
|
|
export CHERRY_SERVER_ID
|
|
|
|
# Wait for IP assignment
|
|
_cherry_wait_for_ip "$server_id"
|
|
}
|
|
|
|
# ============================================================
|
|
# Execution Functions
|
|
# ============================================================
|
|
|
|
# SSH operations — delegates to shared helpers (SSH_USER defaults to root)
|
|
run_server() { ssh_run_server "$@"; }
|
|
upload_file() { ssh_upload_file "$@"; }
|
|
interactive_session() { ssh_interactive_session "$@"; }
|
|
verify_server_connectivity() { ssh_verify_connectivity "$@"; }
|
|
|
|
# Wait for cloud-init to complete
|
|
wait_for_cloud_init() {
|
|
local ip="$1"
|
|
local timeout="${2:-300}"
|
|
|
|
log_step "Waiting for system initialization..."
|
|
|
|
if ! run_server "$ip" "cloud-init status --wait --long" 2>/dev/null; then
|
|
log_warn "cloud-init wait timed out or not available, proceeding anyway"
|
|
else
|
|
log_info "System initialization complete"
|
|
fi
|
|
}
|