feat: Add Contabo cloud provider with claude, aider, openclaw (#353)

Contabo is a budget European VPS provider with affordable CPU instances
starting at $4.95/month. Ideal for AI agents using remote API inference.

Key features:
- Budget-friendly pricing ($4.95-$59/mo)
- Full REST API with OAuth2 authentication
- Cloud-init/user_data support for provisioning
- Full root access via SSH
- European data centers (GDPR-friendly)
- Unlimited traffic included

Implementation:
- contabo/lib/common.sh: OAuth2 token flow, instance provisioning, SSH access
- contabo/claude.sh: Claude Code deployment (implemented)
- contabo/aider.sh: Aider deployment (implemented)
- contabo/openclaw.sh: OpenClaw deployment (implemented)
- manifest.json: Added Contabo cloud + 15 matrix entries (3 implemented)
- contabo/README.md: Complete usage guide with API setup

Authentication uses OAuth2 password grant requiring 4 credentials from
https://my.contabo.com/api/details: Client ID, Client Secret, API User,
API Password.

Agent: cloud-scout-1

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
A 2026-02-10 20:40:42 -08:00 committed by GitHub
parent 4528c11ebf
commit ed4d37d0fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 833 additions and 3 deletions

185
contabo/README.md Normal file
View file

@ -0,0 +1,185 @@
# Contabo Cloud
Deploy AI agents on [Contabo](https://contabo.com/) - a budget-friendly European VPS provider with affordable CPU instances starting at $4.95/month.
## Why Contabo?
- **Budget-friendly**: VPS plans starting at $4.95/month
- **European provider**: Multiple European data centers (Germany, UK, etc.)
- **Full root access**: Complete control over your instances
- **REST API**: Full API support for automation
- **Cloud-init support**: Easy provisioning with user_data
- **Fast provisioning**: Instances typically ready in 2-5 minutes
## Prerequisites
### API Credentials
Get your API credentials from the [Contabo Customer Control Panel](https://my.contabo.com/api/details):
1. Log in to https://my.contabo.com
2. Navigate to API → API Details
3. Note down all 4 required values:
- **Client ID**: OAuth2 client identifier
- **Client Secret**: OAuth2 client secret
- **API User**: Your API username (email)
- **API Password**: Your API password
Set them as environment variables:
```bash
export CONTABO_CLIENT_ID="your_client_id"
export CONTABO_CLIENT_SECRET="your_client_secret"
export CONTABO_API_USER="your_api_user@example.com"
export CONTABO_API_PASSWORD="your_api_password"
```
Or the script will prompt you interactively and save them to `~/.config/spawn/contabo.json`.
### SSH Key
The scripts will automatically:
- Generate an SSH key at `~/.ssh/spawn_ed25519` (if it doesn't exist)
- Register it with Contabo as a secret
- Use it to access your instances
## Usage
### Deploy Claude Code
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/claude.sh)
```
### Deploy Aider
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/aider.sh)
```
### Deploy OpenClaw
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/openclaw.sh)
```
## Environment Variables
### Required (if not set, you'll be prompted):
- `CONTABO_CLIENT_ID` - OAuth2 client ID from Contabo API details
- `CONTABO_CLIENT_SECRET` - OAuth2 client secret
- `CONTABO_API_USER` - API username (email address)
- `CONTABO_API_PASSWORD` - API password
### Optional:
- `CONTABO_SERVER_NAME` - Instance display name (default: prompt)
- `CONTABO_PRODUCT_ID` - VPS product ID (default: `V45` = 2 vCPU, 8GB RAM)
- `CONTABO_REGION` - Region code (default: `EU`)
- `CONTABO_IMAGE_ID` - OS image (default: `ubuntu-24.04`)
- `CONTABO_PERIOD` - Billing period in months (default: `1`)
- `OPENROUTER_API_KEY` - Your OpenRouter API key (or use OAuth)
### Common Product IDs
| Product ID | vCPUs | RAM | Disk | Price/month |
|------------|-------|-------|---------|-------------|
| V8 | 4 | 6 GB | 50 GB | ~$6.99 |
| V45 | 2 | 8 GB | 100 GB | ~$8.99 |
| V16 | 6 | 16 GB | 400 GB | ~$14.99 |
| V32 | 8 | 30 GB | 800 GB | ~$26.99 |
Check [Contabo VPS pricing](https://contabo.com/en/vps/) for current rates.
### Available Regions
- `EU` - European data centers (Germany, UK)
- `US-east` - US East Coast
- `US-central` - US Central
- `US-west` - US West Coast
- `SIN` - Singapore
## How It Works
1. **Authentication**: Uses OAuth2 password grant flow
- Exchanges client credentials + user credentials for an access token
- Token is cached for the session
- API calls include `Authorization: Bearer <token>` header
2. **SSH Key Management**:
- Registers your SSH public key as a Contabo secret
- Includes the secret ID in instance creation request
- Root access is enabled by default
3. **Instance Provisioning**:
- Creates instance via `POST /v1/compute/instances`
- Includes cloud-init userData for agent installation
- Polls instance status until "running"
- Extracts public IPv4 address
4. **Agent Setup**:
- Waits for SSH connectivity
- Waits for cloud-init to complete
- Verifies agent installation (or installs manually)
- Injects OpenRouter API key
- Starts interactive session
## Pricing
Contabo offers **monthly billing** (not hourly):
- Minimum commitment: 1 month
- Billed monthly in advance
- No prorated refunds for early termination
- Unlimited traffic included
**Example costs:**
- VPS S (V45): 2 vCPU, 8GB RAM → ~$8.99/month
- VPS M (V16): 6 vCPU, 16GB RAM → ~$14.99/month
Check [current pricing](https://contabo.com/en/vps/) on their website.
## Troubleshooting
### Authentication Errors
If you see "Failed to obtain Contabo OAuth token":
1. Verify all 4 credentials are correct
2. Check API user has API access enabled
3. Ensure API password is set (not your login password)
4. Visit https://my.contabo.com/api/details to verify
### Instance Creation Fails
Common issues:
- **Insufficient balance**: Add funds to your account
- **Account limits**: Contact support to increase limits
- **Product unavailable**: Try different `CONTABO_PRODUCT_ID` or `CONTABO_REGION`
### SSH Connection Timeout
- Contabo instances can take 2-5 minutes to provision
- Check instance status in Customer Control Panel
- Verify SSH key was registered correctly
- Check firewall settings allow SSH (port 22)
### Cloud-init Not Completing
- Check `/var/log/cloud-init-output.log` on the instance
- Verify image supports cloud-init (default Ubuntu images do)
- May need to wait longer (use higher timeout)
## API Documentation
- Official API docs: https://api.contabo.com/
- Customer Control Panel: https://my.contabo.com/
- API Details: https://my.contabo.com/api/details
## Notes
- Contabo uses **monthly billing**, not hourly/per-second like many other clouds
- **Budget-friendly** but less flexible than pay-per-hour providers
- Good for **long-running development environments** or **stable workloads**
- **European data centers** make it GDPR-friendly
- **Full root access** on all VPS instances
- **Unlimited traffic** included in all plans

61
contabo/aider.sh Normal file
View file

@ -0,0 +1,61 @@
#!/bin/bash
set -eo pipefail
# Source common functions - try local file first, fall back to remote
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=contabo/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/lib/common.sh)"
fi
log_info "Aider on Contabo"
echo ""
# 1. Resolve Contabo credentials
ensure_contabo_credentials
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get server name and create server
SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "${CONTABO_SERVER_IP}"
wait_for_cloud_init "${CONTABO_SERVER_IP}" 60
# 5. Install Aider
log_warn "Installing Aider..."
run_server "${CONTABO_SERVER_IP}" "pip install aider-chat"
# 6. Get OpenRouter API key
echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi
# 7. Get model ID
log_info "Aider natively supports OpenRouter"
printf "Enter model ID [openrouter/auto]: "
MODEL_ID=$(safe_read) || MODEL_ID=""
MODEL_ID="${MODEL_ID:-openrouter/auto}"
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${CONTABO_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
echo ""
log_info "Contabo instance setup completed successfully!"
log_info "Instance: ${SERVER_NAME} (ID: ${CONTABO_INSTANCE_ID}, IP: ${CONTABO_SERVER_IP})"
echo ""
# 8. Start Aider interactively
log_warn "Starting Aider with model: ${MODEL_ID}..."
sleep 1
clear
interactive_session "${CONTABO_SERVER_IP}" "source ~/.zshrc && aider --model ${MODEL_ID}"

76
contabo/claude.sh Normal file
View file

@ -0,0 +1,76 @@
#!/bin/bash
set -eo pipefail
# Source common functions - try local file first, fall back to remote
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=contabo/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/lib/common.sh)"
fi
log_info "Claude Code on Contabo"
echo ""
# 1. Resolve Contabo credentials
ensure_contabo_credentials
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get server name and create server
SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "${CONTABO_SERVER_IP}"
wait_for_cloud_init "${CONTABO_SERVER_IP}" 60
# 5. Verify Claude Code is installed (fallback to manual install)
log_warn "Verifying Claude Code installation..."
if ! run_server "${CONTABO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
log_warn "Claude Code not found, installing manually..."
run_server "${CONTABO_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
# Verify installation succeeded
if ! run_server "${CONTABO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude &> /dev/null && claude --version &> /dev/null"; then
log_error "Claude Code installation verification failed"
log_error "The 'claude' command is not available or not working properly on server ${CONTABO_SERVER_IP}"
exit 1
fi
log_info "Claude Code installation verified successfully"
# 6. Get OpenRouter API key
echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${CONTABO_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0"
# 8. Configure Claude Code settings
setup_claude_code_config "${OPENROUTER_API_KEY}" \
"upload_file ${CONTABO_SERVER_IP}" \
"run_server ${CONTABO_SERVER_IP}"
echo ""
log_info "Contabo instance setup completed successfully!"
log_info "Instance: ${SERVER_NAME} (ID: ${CONTABO_INSTANCE_ID}, IP: ${CONTABO_SERVER_IP})"
echo ""
# 9. Start Claude Code interactively
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${CONTABO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"

411
contabo/lib/common.sh Normal file
View file

@ -0,0 +1,411 @@
#!/bin/bash
set -eo pipefail
# Common bash functions for Contabo spawn scripts
# ============================================================
# Provider-agnostic functions
# ============================================================
# Source shared provider-agnostic functions (local or remote fallback)
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
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
# ============================================================
# Contabo specific functions
# ============================================================
readonly CONTABO_API_BASE="https://api.contabo.com/v1"
readonly CONTABO_AUTH_URL="https://auth.contabo.com/auth/realms/contabo/protocol/openid-connect/token"
# Get OAuth access token from Contabo
# Requires: CONTABO_CLIENT_ID, CONTABO_CLIENT_SECRET, CONTABO_API_USER, CONTABO_API_PASSWORD
get_contabo_access_token() {
local response
response=$(curl -fsSL \
-d "client_id=${CONTABO_CLIENT_ID}" \
-d "client_secret=${CONTABO_CLIENT_SECRET}" \
--data-urlencode "username=${CONTABO_API_USER}" \
--data-urlencode "password=${CONTABO_API_PASSWORD}" \
-d "grant_type=password" \
"${CONTABO_AUTH_URL}" 2>&1) || {
log_error "Failed to obtain Contabo OAuth token"
log_error "Response: $response"
return 1
}
if echo "$response" | grep -q '"error"'; then
log_error "OAuth authentication failed: $response"
return 1
fi
local token
token=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('access_token',''))" 2>/dev/null)
if [[ -z "$token" ]]; then
log_error "Failed to extract access token from response"
return 1
fi
echo "$token"
}
# Centralized curl wrapper for Contabo API
contabo_api() {
local method="$1"
local endpoint="$2"
local body="${3:-}"
# Get or refresh access token
if [[ -z "${CONTABO_ACCESS_TOKEN:-}" ]]; then
CONTABO_ACCESS_TOKEN=$(get_contabo_access_token) || return 1
export CONTABO_ACCESS_TOKEN
fi
local url="${CONTABO_API_BASE}${endpoint}"
local response
if [[ "$method" == "GET" ]]; then
response=$(curl -fsSL -X GET \
-H "Authorization: Bearer ${CONTABO_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
"$url" 2>&1) || {
log_error "GET request failed: $response"
return 1
}
elif [[ "$method" == "POST" ]]; then
response=$(curl -fsSL -X POST \
-H "Authorization: Bearer ${CONTABO_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-H "x-request-id: spawn-$(date +%s)" \
-d "$body" \
"$url" 2>&1) || {
log_error "POST request failed: $response"
return 1
}
elif [[ "$method" == "DELETE" ]]; then
response=$(curl -fsSL -X DELETE \
-H "Authorization: Bearer ${CONTABO_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
"$url" 2>&1) || {
log_error "DELETE request failed: $response"
return 1
}
else
log_error "Unsupported HTTP method: $method"
return 1
fi
echo "$response"
}
# Test Contabo credentials
test_contabo_credentials() {
local response
response=$(contabo_api GET "/compute/instances?page=1&size=1")
if echo "$response" | grep -q '"error"'; then
local error_msg
error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','No details available'))" 2>/dev/null || echo "Unable to parse error")
log_error "API Error: $error_msg"
log_error ""
log_error "How to fix:"
log_error " 1. Get credentials from: https://my.contabo.com/api/details"
log_error " 2. Ensure you have all 4 required values:"
log_error " - Client ID"
log_error " - Client Secret"
log_error " - API User (username/email)"
log_error " - API Password"
return 1
fi
return 0
}
# Ensure Contabo credentials are available
ensure_contabo_credentials() {
local config_file="$HOME/.config/spawn/contabo.json"
# Try to load from config file first
if [[ -f "$config_file" ]]; then
CONTABO_CLIENT_ID=$(python3 -c "import json; print(json.load(open('$config_file')).get('client_id',''))" 2>/dev/null)
CONTABO_CLIENT_SECRET=$(python3 -c "import json; print(json.load(open('$config_file')).get('client_secret',''))" 2>/dev/null)
CONTABO_API_USER=$(python3 -c "import json; print(json.load(open('$config_file')).get('api_user',''))" 2>/dev/null)
CONTABO_API_PASSWORD=$(python3 -c "import json; print(json.load(open('$config_file')).get('api_password',''))" 2>/dev/null)
export CONTABO_CLIENT_ID CONTABO_CLIENT_SECRET CONTABO_API_USER CONTABO_API_PASSWORD
fi
# Prompt for missing credentials
if [[ -z "${CONTABO_CLIENT_ID:-}" ]]; then
log_info "Get your Contabo API credentials from: https://my.contabo.com/api/details"
printf "Enter Contabo Client ID: "
CONTABO_CLIENT_ID=$(safe_read) || return 1
export CONTABO_CLIENT_ID
fi
if [[ -z "${CONTABO_CLIENT_SECRET:-}" ]]; then
printf "Enter Contabo Client Secret: "
CONTABO_CLIENT_SECRET=$(safe_read) || return 1
export CONTABO_CLIENT_SECRET
fi
if [[ -z "${CONTABO_API_USER:-}" ]]; then
printf "Enter Contabo API User (username/email): "
CONTABO_API_USER=$(safe_read) || return 1
export CONTABO_API_USER
fi
if [[ -z "${CONTABO_API_PASSWORD:-}" ]]; then
printf "Enter Contabo API Password: "
CONTABO_API_PASSWORD=$(safe_read) || return 1
export CONTABO_API_PASSWORD
fi
# Test credentials
log_info "Testing Contabo credentials..."
if ! test_contabo_credentials; then
return 1
fi
# Save to config file
mkdir -p "$(dirname "$config_file")"
python3 -c "
import json
config = {
'client_id': '$CONTABO_CLIENT_ID',
'client_secret': '$CONTABO_CLIENT_SECRET',
'api_user': '$CONTABO_API_USER',
'api_password': '$CONTABO_API_PASSWORD'
}
with open('$config_file', 'w') as f:
json.dump(config, f, indent=2)
" && chmod 600 "$config_file"
log_info "Credentials verified and saved to $config_file"
}
# Check if SSH key is registered with Contabo
contabo_check_ssh_key() {
local fingerprint="$1"
local existing_keys
existing_keys=$(contabo_api GET "/compute/secrets")
echo "$existing_keys" | grep -q "$fingerprint"
}
# Register SSH key with Contabo as a secret
contabo_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="{\"name\":\"$key_name\",\"type\":\"ssh\",\"value\":$json_pub_key}"
local register_response
register_response=$(contabo_api POST "/compute/secrets" "$register_body")
if echo "$register_response" | grep -q '"error"'; then
local error_msg
error_msg=$(echo "$register_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','Unknown error'))" 2>/dev/null || echo "$register_response")
log_error "API Error: $error_msg"
log_error ""
log_error "Common causes:"
log_error " - SSH key already registered with this name"
log_error " - Invalid SSH key format"
return 1
fi
return 0
}
# Ensure SSH key exists locally and is registered with Contabo
ensure_ssh_key() {
ensure_ssh_key_with_provider contabo_check_ssh_key contabo_register_ssh_key "Contabo"
}
# Get server name from env var or prompt
get_server_name() {
local server_name
server_name=$(get_resource_name "CONTABO_SERVER_NAME" "Enter server name: ") || return 1
if ! validate_server_name "$server_name"; then
return 1
fi
echo "$server_name"
}
# Create a Contabo instance with cloud-init
create_server() {
local name="$1"
# Use env vars or defaults
local region="${CONTABO_REGION:-EU}"
local product_id="${CONTABO_PRODUCT_ID:-V45}" # VPS S SSD (2 vCPU, 8 GB RAM)
local image_id="${CONTABO_IMAGE_ID:-ubuntu-24.04}"
local period="${CONTABO_PERIOD:-1}" # 1 month
log_warn "Creating Contabo instance '$name' (product: $product_id, region: $region)..."
# Get all SSH secret IDs
local ssh_secrets_response
ssh_secrets_response=$(contabo_api GET "/compute/secrets")
local ssh_secret_ids
ssh_secret_ids=$(echo "$ssh_secrets_response" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
secrets = [s['secretId'] for s in data.get('data', []) if s.get('type') == 'ssh']
print(json.dumps(secrets))
" 2>/dev/null || echo "[]")
# Get cloud-init userdata
local userdata
userdata=$(get_cloud_init_userdata)
# Build request body
local body
body=$(echo "$userdata" | python3 -c "
import json, sys
userdata = sys.stdin.read()
body = {
'displayName': '$name',
'productId': '$product_id',
'region': '$region',
'imageId': '$image_id',
'period': $period,
'sshKeys': $ssh_secret_ids,
'userData': userdata,
'defaultUser': 'root'
}
print(json.dumps(body))
")
local response
response=$(contabo_api POST "/compute/instances" "$body")
# Check for errors
if echo "$response" | grep -q '"error"' || ! echo "$response" | grep -q '"instanceId"'; then
log_error "Failed to create Contabo instance"
local error_msg
error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message','Unknown error'))" 2>/dev/null || echo "$response")
log_error "API Error: $error_msg"
log_error ""
log_error "Common issues:"
log_error " - Insufficient account balance"
log_error " - Product/region unavailable"
log_error " - Account limits reached"
return 1
fi
# Extract instance ID
CONTABO_INSTANCE_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('data',[{}])[0].get('instanceId',''))")
export CONTABO_INSTANCE_ID
log_info "Instance created: ID=$CONTABO_INSTANCE_ID"
log_info "Waiting for instance to be provisioned..."
# Wait for instance to be running and get IP
local max_attempts=60
local attempt=1
while [[ $attempt -le $max_attempts ]]; do
sleep 5
local instance_info
instance_info=$(contabo_api GET "/compute/instances/$CONTABO_INSTANCE_ID")
local status
status=$(echo "$instance_info" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('data',[{}])[0].get('status',''))")
if [[ "$status" == "running" ]]; then
CONTABO_SERVER_IP=$(echo "$instance_info" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read()).get('data',[{}])[0]
ip = data.get('ipConfig', {}).get('v4', {}).get('ip', '')
print(ip)
")
export CONTABO_SERVER_IP
log_info "Instance running with IP: $CONTABO_SERVER_IP"
return 0
fi
log_info "Instance status: $status (attempt $attempt/$max_attempts)"
attempt=$((attempt + 1))
done
log_error "Instance failed to reach running state within timeout"
return 1
}
# Wait for SSH connectivity
verify_server_connectivity() {
local ip="$1"
local max_attempts=${2:-30}
generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "echo ok" "SSH connectivity" "$max_attempts" 5
}
# Run a command on the server
run_server() {
local ip="$1"
local cmd="$2"
# shellcheck disable=SC2086
ssh $SSH_OPTS "root@$ip" "$cmd"
}
# Upload a file to the server
upload_file() {
local ip="$1"
local local_path="$2"
local remote_path="$3"
# shellcheck disable=SC2086
scp $SSH_OPTS "$local_path" "root@$ip:$remote_path"
}
# Start an interactive SSH session
interactive_session() {
local ip="$1"
local cmd="$2"
# shellcheck disable=SC2086
ssh -t $SSH_OPTS "root@$ip" "$cmd"
}
# Destroy a Contabo instance
destroy_server() {
local instance_id="$1"
log_warn "Destroying instance $instance_id..."
local response
response=$(contabo_api DELETE "/compute/instances/$instance_id")
if echo "$response" | grep -q '"error"'; then
log_error "Failed to destroy instance: $response"
return 1
fi
log_info "Instance $instance_id destroyed"
}
# List all Contabo instances
list_servers() {
local response
response=$(contabo_api GET "/compute/instances")
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
instances = data.get('data', [])
if not instances:
print('No instances found')
sys.exit(0)
print(f\"{'NAME':<25} {'ID':<15} {'STATUS':<12} {'IP':<16} {'PRODUCT':<10}\")
print('-' * 78)
for inst in instances:
name = inst.get('displayName', 'N/A')
iid = str(inst.get('instanceId', 'N/A'))
status = inst.get('status', 'N/A')
ip = inst.get('ipConfig', {}).get('v4', {}).get('ip', 'N/A')
product = inst.get('productId', 'N/A')
print(f'{name:<25} {iid:<15} {status:<12} {ip:<16} {product:<10}')
" <<< "$response"
}

66
contabo/openclaw.sh Normal file
View file

@ -0,0 +1,66 @@
#!/bin/bash
set -eo pipefail
# Source common functions - try local file first, fall back to remote
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
# shellcheck source=contabo/lib/common.sh
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
source "${SCRIPT_DIR}/lib/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/contabo/lib/common.sh)"
fi
log_info "OpenClaw on Contabo"
echo ""
# 1. Resolve Contabo credentials
ensure_contabo_credentials
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get server name and create server
SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "${CONTABO_SERVER_IP}"
wait_for_cloud_init "${CONTABO_SERVER_IP}" 60
# 5. Install OpenClaw
log_warn "Installing OpenClaw..."
run_server "${CONTABO_SERVER_IP}" "bun install -g openclaw"
# 6. Get OpenRouter API key
echo ""
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
log_info "Using OpenRouter API key from environment"
else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi
# 7. Get model ID
log_info "OpenClaw natively supports OpenRouter"
printf "Enter model ID [openrouter/auto]: "
MODEL_ID=$(safe_read) || MODEL_ID=""
MODEL_ID="${MODEL_ID:-openrouter/auto}"
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${CONTABO_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api"
echo ""
log_info "Contabo instance setup completed successfully!"
log_info "Instance: ${SERVER_NAME} (ID: ${CONTABO_INSTANCE_ID}, IP: ${CONTABO_SERVER_IP})"
log_info "Starting OpenClaw gateway in background..."
run_server "${CONTABO_SERVER_IP}" "nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
sleep 2
echo ""
# 8. Start OpenClaw TUI interactively
log_warn "Starting OpenClaw TUI..."
sleep 1
clear
interactive_session "${CONTABO_SERVER_IP}" "source ~/.zshrc && openclaw tui"

View file

@ -642,7 +642,7 @@
"disk_size": 20,
"location": "us/las"
},
"notes": "Budget European cloud provider with minute-based billing starting at $2/month. Requires IONOS_USERNAME (email) and IONOS_PASSWORD (API key) from https://dcd.ionos.com/ → Management → Users & Keys. Datacenters are created automatically if needed."
"notes": "Budget European cloud provider with minute-based billing starting at $2/month. Requires IONOS_USERNAME (email) and IONOS_PASSWORD (API key) from https://dcd.ionos.com/ \u2192 Management \u2192 Users & Keys. Datacenters are created automatically if needed."
},
"exoscale": {
"name": "Exoscale",
@ -659,6 +659,22 @@
"template": "Linux Ubuntu 24.04 LTS 64-bit"
},
"notes": "European cloud provider with per-second billing. Multiple zones available: ch-gva-2, ch-dk-2, de-fra-1, de-muc-1, at-vie-1, at-vie-2, bg-sof-1. Uses 'ubuntu' user for SSH. Requires exo CLI (auto-installed). Create API keys at https://portal.exoscale.com/iam/api-keys"
},
"contabo": {
"name": "Contabo",
"description": "Contabo budget VPS cloud via REST API with OAuth",
"url": "https://contabo.com/",
"type": "api",
"auth": "CONTABO_CLIENT_ID + CONTABO_CLIENT_SECRET + CONTABO_API_USER + CONTABO_API_PASSWORD",
"provision_method": "POST /v1/compute/instances with userData (cloud-init)",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"product": "V45",
"region": "EU",
"image": "ubuntu-24.04"
},
"notes": "Budget European VPS provider starting at $4.95/mo. Uses OAuth2 password grant (client credentials + user credentials). Requires 4 values from https://my.contabo.com/api/details: Client ID, Client Secret, API User, API Password."
}
},
"matrix": {
@ -1066,6 +1082,21 @@
"exoscale/opencode": "implemented",
"exoscale/plandex": "implemented",
"exoscale/kilocode": "implemented",
"exoscale/continue": "implemented"
"exoscale/continue": "implemented",
"contabo/claude": "implemented",
"contabo/openclaw": "implemented",
"contabo/nanoclaw": "missing",
"contabo/aider": "implemented",
"contabo/goose": "missing",
"contabo/codex": "missing",
"contabo/interpreter": "missing",
"contabo/gemini": "missing",
"contabo/amazonq": "missing",
"contabo/cline": "missing",
"contabo/gptme": "missing",
"contabo/opencode": "missing",
"contabo/plandex": "missing",
"contabo/kilocode": "missing",
"contabo/continue": "missing"
}
}
}