feat: Add IONOS Cloud provider with minute-based billing (#304)

- Budget European provider starting at $2/month for 1 vCore/1GB RAM
- REST API via CloudAPI v6 with Basic Auth (username + password)
- Datacenter-based resource organization (auto-creates if needed)
- Volume-based boot disks with cloud-init support
- Implemented 3 agents: claude, aider, goose
- 11 agents marked as missing for future implementation

Agent: cloud-scout-1

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-10 16:51:34 -08:00 committed by GitHub
parent 571e17c559
commit c167bc4f00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 873 additions and 1 deletions

101
ionos/README.md Normal file
View file

@ -0,0 +1,101 @@
# IONOS Cloud
IONOS Cloud is a budget-friendly European cloud provider offering VPS and cloud servers with flexible minute-based billing.
## Features
- **Ultra-cheap pricing**: Starting at $2/month for basic VPS (1 vCore, 1GB RAM, 10GB SSD)
- **Minute-based billing**: Pay only for what you use with Cloud Cubes
- **REST API**: Full CloudAPI v6 for programmatic control
- **Global locations**: Multiple datacenters across US and Europe
- **Root SSH access**: Full control over your servers
- **Unlimited traffic**: No bandwidth overage charges on Cloud VPS plans
## Authentication
IONOS Cloud uses Basic Authentication with your account credentials:
1. Go to [IONOS Data Center Designer (DCD)](https://dcd.ionos.com/)
2. Navigate to **Management → Users & Keys**
3. Create or retrieve your API credentials:
- **Username**: Your IONOS account email
- **Password**: Your API password/key
Set these as environment variables:
```bash
export IONOS_USERNAME="your-email@example.com"
export IONOS_PASSWORD="your-api-password"
```
Alternatively, spawn will prompt for them on first use and save to `~/.config/spawn/ionos.json`.
## Configuration
### Environment Variables
- `IONOS_USERNAME` (required): Your IONOS account email
- `IONOS_PASSWORD` (required): Your IONOS API password/key
- `IONOS_SERVER_NAME` (optional): Server name (will prompt if not set)
- `IONOS_CORES` (default: 2): Number of CPU cores
- `IONOS_RAM` (default: 2048): RAM in MB
- `IONOS_DISK_SIZE` (default: 20): Boot disk size in GB
- `IONOS_LOCATION` (default: us/las): Datacenter location
### Available Locations
- **US**: `us/las` (Las Vegas), `us/ewr` (Newark)
- **Europe**: `de/fra` (Frankfurt), `de/fkb` (Karlsruhe), `gb/lhr` (London)
## Usage
```bash
# Run Claude Code on IONOS Cloud
spawn claude ionos
# Run with custom resources
IONOS_CORES=4 IONOS_RAM=4096 spawn claude ionos
# Run Aider
spawn aider ionos
# Run Goose
spawn goose ionos
```
## Pricing
IONOS offers two main compute options:
### Cloud VPS (Fixed monthly plans)
- **Basic**: $2/month - 1 vCore, 1GB RAM, 10GB SSD, unlimited traffic
- **Standard**: $5/month - 2 vCores, 2GB RAM, 80GB SSD, unlimited traffic
### Cloud Cubes (Flexible pay-as-you-go)
- Minute-based billing
- Scale resources on demand
- Perfect for short-lived AI agent sessions
Spawn creates servers in the Cloud Cubes model by default for maximum flexibility.
## Notes
- **Datacenter Management**: IONOS organizes resources into "datacenters" (logical containers). Spawn will create one automatically if you don't have any.
- **Provisioning Time**: Initial datacenter + server creation can take 3-5 minutes. Subsequent servers in the same datacenter provision faster (~2 minutes).
- **SSH Keys**: SSH keys are registered per-datacenter. Spawn handles this automatically.
- **Cloud-init Support**: Full cloud-init support via `userData` in volume creation.
- **Volume-based Boot**: Servers boot from volumes (similar to AWS EBS). The boot volume is created first, then attached to the server.
## API Reference
- [IONOS Cloud API v6 Documentation](https://api.ionos.com/docs/cloud/v6/)
- [DCD (Data Center Designer)](https://dcd.ionos.com/)
- [IONOS Cloud Console](https://cloud.ionos.com/)
## Implemented Agents
- ✅ Claude Code (`ionos/claude.sh`)
- ✅ Aider (`ionos/aider.sh`)
- ✅ Goose (`ionos/goose.sh`)
See `manifest.json` for the full list of implemented and missing agents.

75
ionos/aider.sh Normal file
View file

@ -0,0 +1,75 @@
#!/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=ionos/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/ionos/lib/common.sh)"
fi
log_info "Aider on IONOS Cloud"
echo ""
# 1. Resolve IONOS credentials
ensure_ionos_credentials
# 2. Generate 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 "${IONOS_SERVER_IP}"
wait_for_cloud_init "${IONOS_SERVER_IP}" 60
# 5. Verify Aider is installed (fallback to manual install)
log_warn "Verifying Aider installation..."
if ! run_server "${IONOS_SERVER_IP}" "command -v aider" >/dev/null 2>&1; then
log_warn "Aider not found, installing manually..."
run_server "${IONOS_SERVER_IP}" "pip install aider-chat"
fi
# Verify installation succeeded
if ! run_server "${IONOS_SERVER_IP}" "command -v aider &> /dev/null && aider --version &> /dev/null"; then
log_error "Aider installation verification failed"
log_error "The 'aider' command is not available or not working properly on server ${IONOS_SERVER_IP}"
exit 1
fi
log_info "Aider 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 5181)
fi
# 7. Get model ID
log_info "Model selection for Aider"
echo ""
log_info "Aider supports OpenRouter models via the openrouter/MODEL_ID format"
log_info "Examples: openrouter/auto, openrouter/anthropic/claude-3-5-sonnet"
echo ""
MODEL_ID=$(get_interactive_value "MODEL_ID" "Enter model ID" "openrouter/auto") || return 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${IONOS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"MODEL_ID=${MODEL_ID}"
echo ""
log_info "IONOS server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${IONOS_SERVER_ID}, IP: ${IONOS_SERVER_IP})"
echo ""
# 8. Start Aider interactively
log_warn "Starting Aider..."
sleep 1
clear
interactive_session "${IONOS_SERVER_IP}" "source ~/.zshrc && aider --model \${MODEL_ID}"

76
ionos/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=ionos/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/ionos/lib/common.sh)"
fi
log_info "Claude Code on IONOS Cloud"
echo ""
# 1. Resolve IONOS credentials
ensure_ionos_credentials
# 2. Generate 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 "${IONOS_SERVER_IP}"
wait_for_cloud_init "${IONOS_SERVER_IP}" 60
# 5. Verify Claude Code is installed (fallback to manual install)
log_warn "Verifying Claude Code installation..."
if ! run_server "${IONOS_SERVER_IP}" "command -v claude" >/dev/null 2>&1; then
log_warn "Claude Code not found, installing manually..."
run_server "${IONOS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
fi
# Verify installation succeeded
if ! run_server "${IONOS_SERVER_IP}" "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 ${IONOS_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 "${IONOS_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 ${IONOS_SERVER_IP}" \
"run_server ${IONOS_SERVER_IP}"
echo ""
log_info "IONOS server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${IONOS_SERVER_ID}, IP: ${IONOS_SERVER_IP})"
echo ""
# 9. Start Claude Code interactively
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "${IONOS_SERVER_IP}" "source ~/.zshrc && claude"

67
ionos/goose.sh Normal file
View file

@ -0,0 +1,67 @@
#!/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=ionos/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/ionos/lib/common.sh)"
fi
log_info "Goose on IONOS Cloud"
echo ""
# 1. Resolve IONOS credentials
ensure_ionos_credentials
# 2. Generate 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 "${IONOS_SERVER_IP}"
wait_for_cloud_init "${IONOS_SERVER_IP}" 60
# 5. Verify Goose is installed (fallback to manual install)
log_warn "Verifying Goose installation..."
if ! run_server "${IONOS_SERVER_IP}" "command -v goose" >/dev/null 2>&1; then
log_warn "Goose not found, installing manually..."
run_server "${IONOS_SERVER_IP}" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash"
fi
# Verify installation succeeded
if ! run_server "${IONOS_SERVER_IP}" "command -v goose &> /dev/null && goose --version &> /dev/null"; then
log_error "Goose installation verification failed"
log_error "The 'goose' command is not available or not working properly on server ${IONOS_SERVER_IP}"
exit 1
fi
log_info "Goose 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 5182)
fi
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${IONOS_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"GOOSE_PROVIDER=openrouter"
echo ""
log_info "IONOS server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${IONOS_SERVER_ID}, IP: ${IONOS_SERVER_IP})"
echo ""
# 7. Start Goose interactively
log_warn "Starting Goose..."
sleep 1
clear
interactive_session "${IONOS_SERVER_IP}" "source ~/.zshrc && goose"

521
ionos/lib/common.sh Normal file
View file

@ -0,0 +1,521 @@
#!/bin/bash
set -eo pipefail
# Common bash functions for IONOS Cloud 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
# ============================================================
# IONOS Cloud specific functions
# ============================================================
readonly IONOS_API_BASE="https://api.ionos.com/cloudapi/v6"
# SSH_OPTS is now defined in shared/common.sh
# Centralized curl wrapper for IONOS API
# IONOS uses Basic Auth with username (email) and password (API token)
ionos_api() {
local method="$1"
local endpoint="$2"
local body="${3:-}"
# IONOS API uses Basic Auth
local auth_header="Authorization: Basic $(printf '%s:%s' "${IONOS_USERNAME}" "${IONOS_PASSWORD}" | base64)"
local response
if [[ "$method" == "GET" || "$method" == "DELETE" ]]; then
response=$(curl -s -X "$method" \
-H "$auth_header" \
-H "Content-Type: application/json" \
"${IONOS_API_BASE}${endpoint}")
else
response=$(curl -s -X "$method" \
-H "$auth_header" \
-H "Content-Type: application/json" \
-d "$body" \
"${IONOS_API_BASE}${endpoint}")
fi
echo "$response"
}
test_ionos_credentials() {
local response
response=$(ionos_api GET "/datacenters?depth=1&limit=1")
if echo "$response" | grep -q '"httpStatus"'; then
# Parse error details
local error_msg
error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('messages',[{}])[0].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. Verify your credentials at: https://dcd.ionos.com/ → Management → Users & Keys"
log_error " 2. Ensure IONOS_USERNAME is your account email"
log_error " 3. Ensure IONOS_PASSWORD is your valid API password/token"
return 1
fi
return 0
}
# Ensure IONOS credentials are available (env vars → config file → prompt+save)
ensure_ionos_credentials() {
local config_file="$HOME/.config/spawn/ionos.json"
# Check environment variables first
if [[ -z "${IONOS_USERNAME:-}" ]] || [[ -z "${IONOS_PASSWORD:-}" ]]; then
# Try loading from config file
if [[ -f "$config_file" ]]; then
log_info "Loading IONOS credentials from $config_file"
IONOS_USERNAME=$(python3 -c "import json; print(json.load(open('$config_file')).get('username',''))" 2>/dev/null || echo "")
IONOS_PASSWORD=$(python3 -c "import json; print(json.load(open('$config_file')).get('password',''))" 2>/dev/null || echo "")
export IONOS_USERNAME IONOS_PASSWORD
fi
fi
# If still missing, prompt user
if [[ -z "${IONOS_USERNAME:-}" ]] || [[ -z "${IONOS_PASSWORD:-}" ]]; then
log_warn "IONOS Cloud credentials not found"
echo ""
log_info "Get credentials at: https://dcd.ionos.com/ → Management → Users & Keys"
echo ""
IONOS_USERNAME=$(safe_read "Enter IONOS username (email): ") || return 1
IONOS_PASSWORD=$(safe_read "Enter IONOS password/API key: ") || return 1
export IONOS_USERNAME IONOS_PASSWORD
# Test credentials before saving
log_info "Testing IONOS credentials..."
if ! test_ionos_credentials; then
log_error "Invalid IONOS credentials"
return 1
fi
# Save to config file
mkdir -p "$(dirname "$config_file")"
python3 -c "import json; json.dump({'username': '$IONOS_USERNAME', 'password': '$IONOS_PASSWORD'}, open('$config_file', 'w'))"
chmod 600 "$config_file"
log_info "Credentials saved to $config_file"
else
# Test existing credentials
log_info "Testing IONOS credentials..."
if ! test_ionos_credentials; then
log_error "Invalid IONOS credentials. Please check IONOS_USERNAME and IONOS_PASSWORD."
return 1
fi
fi
return 0
}
# Check if SSH key is registered with IONOS
ionos_check_ssh_key() {
local fingerprint="$1"
# IONOS doesn't provide SSH key listing in CloudAPI v6
# We'll skip the check and try to register
return 1
}
# Register SSH key with IONOS datacenter
ionos_register_ssh_key() {
local key_name="$1"
local pub_path="$2"
local datacenter_id="$3"
local pub_key
pub_key=$(cat "$pub_path")
local json_pub_key
json_pub_key=$(json_escape "$pub_key")
local register_body
register_body=$(python3 -c "
import json
body = {
'properties': {
'name': '$key_name',
'publicKey': json.loads($json_pub_key)
}
}
print(json.dumps(body))
")
local register_response
register_response=$(ionos_api POST "/datacenters/${datacenter_id}/sshkeys" "$register_body")
if echo "$register_response" | grep -q '"httpStatus"'; then
# Parse error details
local error_msg
error_msg=$(echo "$register_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); msgs=d.get('messages',[]); print(msgs[0].get('message','Unknown error') if msgs else '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 (must be valid ed25519 public key)"
log_error " - API credentials lack write permissions"
return 1
fi
return 0
}
# Ensure SSH key exists locally and is registered with IONOS
ensure_ssh_key() {
local key_path="$HOME/.ssh/spawn_ed25519"
local pub_path="${key_path}.pub"
generate_ssh_key_if_missing "$key_path"
local fingerprint
fingerprint=$(get_ssh_fingerprint "$pub_path")
log_info "SSH key fingerprint: $fingerprint"
}
# Get server name from env var or prompt
get_server_name() {
local server_name
server_name=$(get_resource_name "IONOS_SERVER_NAME" "Enter server name: ") || return 1
if ! validate_server_name "$server_name"; then
return 1
fi
echo "$server_name"
}
# Ensure datacenter exists or create one
ensure_datacenter() {
local location="${IONOS_LOCATION:-us/las}"
log_warn "Checking for existing IONOS datacenter..."
# List datacenters
local response
response=$(ionos_api GET "/datacenters?depth=1")
# Check if we have any datacenters
local dc_count
dc_count=$(echo "$response" | python3 -c "import json,sys; print(len(json.loads(sys.stdin.read()).get('items',[])))" 2>/dev/null || echo "0")
if [[ "$dc_count" -gt 0 ]]; then
# Use the first datacenter
IONOS_DATACENTER_ID=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['items'][0]['id'])")
local dc_name
dc_name=$(echo "$response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['items'][0]['properties']['name'])")
log_info "Using existing datacenter: $dc_name (ID: $IONOS_DATACENTER_ID)"
else
# Create a new datacenter
log_warn "No datacenter found, creating new datacenter..."
local dc_body
dc_body=$(python3 -c "
import json
body = {
'properties': {
'name': 'spawn-datacenter',
'description': 'Spawn datacenter for AI agents',
'location': '$location'
}
}
print(json.dumps(body))
")
local dc_response
dc_response=$(ionos_api POST "/datacenters" "$dc_body")
if echo "$dc_response" | grep -q '"httpStatus"'; then
log_error "Failed to create datacenter"
local error_msg
error_msg=$(echo "$dc_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); msgs=d.get('messages',[]); print(msgs[0].get('message','Unknown error') if msgs else 'Unknown error')" 2>/dev/null || echo "$dc_response")
log_error "API Error: $error_msg"
return 1
fi
IONOS_DATACENTER_ID=$(echo "$dc_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['id'])")
log_info "Datacenter created: $IONOS_DATACENTER_ID"
fi
export IONOS_DATACENTER_ID
return 0
}
# Create a IONOS server with cloud-init
create_server() {
local name="$1"
local cores="${IONOS_CORES:-2}"
local ram="${IONOS_RAM:-2048}"
local disk_size="${IONOS_DISK_SIZE:-20}"
# Validate env var inputs
validate_resource_name "$name" || { log_error "Invalid server name"; return 1; }
log_warn "Creating IONOS server '$name' (cores: $cores, ram: ${ram}MB, disk: ${disk_size}GB)..."
# Ensure we have a datacenter
ensure_datacenter || return 1
# Get Ubuntu 24.04 image ID
log_info "Finding Ubuntu 24.04 image..."
local images_response
images_response=$(ionos_api GET "/images?depth=2")
local image_id
image_id=$(echo "$images_response" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
for img in data.get('items', []):
props = img.get('properties', {})
name = props.get('name', '').lower()
image_type = props.get('imageType', '')
if 'ubuntu' in name and '24' in name and image_type == 'HDD':
print(img['id'])
break
" 2>/dev/null)
if [[ -z "$image_id" ]]; then
log_error "Could not find Ubuntu 24.04 image"
return 1
fi
log_info "Using image ID: $image_id"
# Get cloud-init userdata
local userdata
userdata=$(get_cloud_init_userdata)
local userdata_json
userdata_json=$(echo "$userdata" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))")
# Create volume first
log_warn "Creating boot volume..."
local volume_body
volume_body=$(python3 -c "
import json
body = {
'properties': {
'name': '${name}-boot',
'type': 'HDD',
'size': $disk_size,
'availabilityZone': 'AUTO',
'image': '$image_id',
'imagePassword': 'TempPass123!',
'userData': json.loads($userdata_json)
}
}
print(json.dumps(body))
")
local volume_response
volume_response=$(ionos_api POST "/datacenters/${IONOS_DATACENTER_ID}/volumes" "$volume_body")
if echo "$volume_response" | grep -q '"httpStatus"'; then
log_error "Failed to create volume"
local error_msg
error_msg=$(echo "$volume_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); msgs=d.get('messages',[]); print(msgs[0].get('message','Unknown error') if msgs else 'Unknown error')" 2>/dev/null || echo "$volume_response")
log_error "API Error: $error_msg"
return 1
fi
local volume_id
volume_id=$(echo "$volume_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['id'])")
log_info "Volume created: $volume_id"
# Wait for volume to be ready
log_warn "Waiting for volume provisioning..."
local max_wait=120
local waited=0
while [[ $waited -lt $max_wait ]]; do
local vol_status
vol_status=$(ionos_api GET "/datacenters/${IONOS_DATACENTER_ID}/volumes/${volume_id}")
local state
state=$(echo "$vol_status" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('metadata',{}).get('state',''))" 2>/dev/null || echo "")
if [[ "$state" == "AVAILABLE" ]]; then
log_info "Volume ready"
break
fi
sleep 5
waited=$((waited + 5))
done
# Register SSH key with datacenter
log_warn "Registering SSH key..."
local key_path="$HOME/.ssh/spawn_ed25519"
local pub_path="${key_path}.pub"
ionos_register_ssh_key "spawn-key-$(date +%s)" "$pub_path" "${IONOS_DATACENTER_ID}" || log_warn "SSH key registration failed, continuing anyway..."
# Create server
log_warn "Creating server instance..."
local server_body
server_body=$(python3 -c "
import json
body = {
'properties': {
'name': '$name',
'cores': $cores,
'ram': $ram,
'availabilityZone': 'AUTO',
'cpuFamily': 'AMD_OPTERON'
}
}
print(json.dumps(body))
")
local server_response
server_response=$(ionos_api POST "/datacenters/${IONOS_DATACENTER_ID}/servers" "$server_body")
if echo "$server_response" | grep -q '"httpStatus"'; then
log_error "Failed to create server"
local error_msg
error_msg=$(echo "$server_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); msgs=d.get('messages',[]); print(msgs[0].get('message','Unknown error') if msgs else 'Unknown error')" 2>/dev/null || echo "$server_response")
log_error "API Error: $error_msg"
return 1
fi
IONOS_SERVER_ID=$(echo "$server_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['id'])")
log_info "Server created: $IONOS_SERVER_ID"
export IONOS_SERVER_ID
# Attach volume to server
log_warn "Attaching volume to server..."
local attach_response
attach_response=$(ionos_api POST "/datacenters/${IONOS_DATACENTER_ID}/servers/${IONOS_SERVER_ID}/volumes" "{\"id\": \"${volume_id}\"}")
if echo "$attach_response" | grep -q '"httpStatus"'; then
log_error "Failed to attach volume"
return 1
fi
log_info "Volume attached successfully"
# Wait for server to get an IP
log_warn "Waiting for server to get IP address..."
max_wait=180
waited=0
while [[ $waited -lt $max_wait ]]; do
local server_status
server_status=$(ionos_api GET "/datacenters/${IONOS_DATACENTER_ID}/servers/${IONOS_SERVER_ID}?depth=3")
IONOS_SERVER_IP=$(echo "$server_status" | python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
entities = data.get('entities', {})
nics = entities.get('nics', {}).get('items', [])
for nic in nics:
props = nic.get('properties', {})
ips = props.get('ips', [])
if ips:
print(ips[0])
break
" 2>/dev/null || echo "")
if [[ -n "$IONOS_SERVER_IP" ]]; then
log_info "Server IP: $IONOS_SERVER_IP"
export IONOS_SERVER_IP
break
fi
sleep 5
waited=$((waited + 5))
done
if [[ -z "$IONOS_SERVER_IP" ]]; then
log_error "Failed to get server IP address"
return 1
fi
log_info "Server created successfully: ID=$IONOS_SERVER_ID, IP=$IONOS_SERVER_IP"
}
# Wait for SSH connectivity
verify_server_connectivity() {
local ip="$1"
local max_attempts=${2:-60}
# SSH_OPTS is defined in shared/common.sh
# shellcheck disable=SC2154
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 IONOS server
destroy_server() {
local datacenter_id="$1"
local server_id="$2"
log_warn "Destroying server $server_id in datacenter $datacenter_id..."
local response
response=$(ionos_api DELETE "/datacenters/${datacenter_id}/servers/${server_id}")
if echo "$response" | grep -q '"httpStatus"'; then
log_error "Failed to destroy server: $response"
return 1
fi
log_info "Server $server_id destroyed"
}
# List all IONOS servers
list_servers() {
local response
response=$(ionos_api GET "/datacenters?depth=3")
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
found_servers = False
for dc in data.get('items', []):
dc_name = dc.get('properties', {}).get('name', 'N/A')
dc_id = dc.get('id', 'N/A')
servers = dc.get('entities', {}).get('servers', {}).get('items', [])
if servers:
if not found_servers:
print(f\"{'DATACENTER':<25} {'NAME':<20} {'ID':<12} {'CORES':<6} {'RAM':<8}\")
print('-' * 75)
found_servers = True
for s in servers:
props = s.get('properties', {})
name = props.get('name', 'N/A')
sid = s.get('id', 'N/A')
cores = props.get('cores', 'N/A')
ram = props.get('ram', 'N/A')
print(f'{dc_name:<25} {name:<20} {sid:<12} {cores:<6} {ram:<8}')
if not found_servers:
print('No servers found')
" <<< "$response"
}

View file

@ -626,6 +626,23 @@
"runtime": "docker"
},
"notes": "Developer-first platform with free Hobby plan (100GB bandwidth, 500 build minutes). Requires API key from https://dashboard.render.com/u/settings/api-keys. SSH not available on free tier."
},
"ionos": {
"name": "IONOS Cloud",
"description": "IONOS Cloud servers via REST API with minute-based billing",
"url": "https://cloud.ionos.com/",
"type": "api",
"auth": "IONOS_USERNAME + IONOS_PASSWORD",
"provision_method": "POST /cloudapi/v6/datacenters/{id}/servers with volumes",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"cores": 2,
"ram": 2048,
"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."
}
},
"matrix": {
@ -1003,6 +1020,21 @@
"koyeb/continue": "missing",
"northflank/continue": "missing",
"railway/continue": "missing",
"render/continue": "missing"
"render/continue": "missing",
"ionos/claude": "implemented",
"ionos/openclaw": "missing",
"ionos/nanoclaw": "missing",
"ionos/aider": "implemented",
"ionos/goose": "implemented",
"ionos/codex": "missing",
"ionos/interpreter": "missing",
"ionos/gemini": "missing",
"ionos/amazonq": "missing",
"ionos/cline": "missing",
"ionos/gptme": "missing",
"ionos/opencode": "missing",
"ionos/plandex": "missing",
"ionos/kilocode": "missing",
"ionos/continue": "missing"
}
}