mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-22 03:14:57 +00:00
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:
parent
4528c11ebf
commit
ed4d37d0fa
6 changed files with 833 additions and 3 deletions
185
contabo/README.md
Normal file
185
contabo/README.md
Normal 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
61
contabo/aider.sh
Normal 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
76
contabo/claude.sh
Normal 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
411
contabo/lib/common.sh
Normal 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
66
contabo/openclaw.sh
Normal 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"
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue