mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
feat: add Gcore cloud provider with 3 agent scripts (#1079)
Add Gcore (gcore.com) as a new cloud provider supporting global edge cloud instances via REST API with hourly billing. Implements full test infrastructure including mock fixtures, URL stripping, body validation, and live recording support. - gcore/lib/common.sh: Cloud library with apikey auth, project auto-detection - gcore/claude.sh, aider.sh, goose.sh: Agent deployment scripts - manifest.json: Cloud definition + 15 matrix entries (3 implemented, 12 missing) - test/mock.sh: URL stripping for Gcore path-parameter API, body validation, synthetic responses - test/record.sh: Endpoints, auth, API caller, error detection, live cycle - test/fixtures/gcore/: 8 fixture files for mock testing Co-authored-by: OpenRouter Bot <noreply@openrouter.ai> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b1361de14
commit
514bc7abc9
17 changed files with 888 additions and 2 deletions
44
gcore/README.md
Normal file
44
gcore/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Gcore
|
||||
|
||||
Gcore Cloud instances via REST API. [Gcore](https://gcore.com/cloud)
|
||||
|
||||
## Agents
|
||||
|
||||
#### Claude Code
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcore/claude.sh)
|
||||
```
|
||||
|
||||
#### Aider
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcore/aider.sh)
|
||||
```
|
||||
|
||||
#### Goose
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcore/goose.sh)
|
||||
```
|
||||
|
||||
## Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
GCORE_SERVER_NAME=dev-mk1 \
|
||||
GCORE_API_TOKEN=your-token \
|
||||
GCORE_PROJECT_ID=12345 \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/gcore/claude.sh)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `GCORE_API_TOKEN` | Gcore API token | (prompted) |
|
||||
| `GCORE_PROJECT_ID` | Gcore project ID | (auto-detected) |
|
||||
| `GCORE_SERVER_NAME` | Instance hostname | (prompted) |
|
||||
| `GCORE_REGION` | Gcore region | `ed-1` |
|
||||
| `GCORE_FLAVOR` | Instance flavor | `g1-standard-1-2` |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter API key | (prompted/OAuth) |
|
||||
51
gcore/aider.sh
Normal file
51
gcore/aider.sh
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=gcore/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/gcore/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Aider on Gcore"
|
||||
echo ""
|
||||
|
||||
ensure_gcore_token
|
||||
ensure_gcore_project
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${GCORE_SERVER_IP}"
|
||||
|
||||
log_step "Waiting for cloud-init to complete..."
|
||||
generic_ssh_wait "root" "${GCORE_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "cloud-init" 60 5
|
||||
|
||||
log_step "Installing Aider..."
|
||||
run_server "${GCORE_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
|
||||
log_info "Aider installed"
|
||||
|
||||
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
|
||||
|
||||
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1
|
||||
|
||||
log_step "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${GCORE_SERVER_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
|
||||
|
||||
echo ""
|
||||
log_info "Gcore instance setup completed successfully!"
|
||||
log_info "Server: ${SERVER_NAME} (ID: ${GCORE_SERVER_ID}, IP: ${GCORE_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Aider..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${GCORE_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
|
||||
61
gcore/claude.sh
Normal file
61
gcore/claude.sh
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=gcore/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/gcore/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Claude Code on Gcore"
|
||||
echo ""
|
||||
|
||||
ensure_gcore_token
|
||||
ensure_gcore_project
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${GCORE_SERVER_IP}"
|
||||
|
||||
log_step "Waiting for cloud-init to complete..."
|
||||
generic_ssh_wait "root" "${GCORE_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "cloud-init" 60 5
|
||||
|
||||
log_step "Verifying Claude Code installation..."
|
||||
if ! run_server "${GCORE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
|
||||
log_step "Claude Code not found, installing manually..."
|
||||
run_server "${GCORE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
|
||||
fi
|
||||
log_info "Claude Code is installed"
|
||||
|
||||
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_step "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${GCORE_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"
|
||||
|
||||
setup_claude_code_config "${OPENROUTER_API_KEY}" \
|
||||
"upload_file ${GCORE_SERVER_IP}" \
|
||||
"run_server ${GCORE_SERVER_IP}"
|
||||
|
||||
echo ""
|
||||
log_info "Gcore instance setup completed successfully!"
|
||||
log_info "Server: ${SERVER_NAME} (ID: ${GCORE_SERVER_ID}, IP: ${GCORE_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Claude Code..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${GCORE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
|
||||
55
gcore/goose.sh
Normal file
55
gcore/goose.sh
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=gcore/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/gcore/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Goose on Gcore"
|
||||
echo ""
|
||||
|
||||
ensure_gcore_token
|
||||
ensure_gcore_project
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${GCORE_SERVER_IP}"
|
||||
|
||||
log_step "Waiting for cloud-init to complete..."
|
||||
generic_ssh_wait "root" "${GCORE_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "cloud-init" 60 5
|
||||
|
||||
log_step "Installing Goose..."
|
||||
run_server "${GCORE_SERVER_IP}" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash"
|
||||
|
||||
if ! run_server "${GCORE_SERVER_IP}" "command -v goose &> /dev/null && goose --version &> /dev/null"; then
|
||||
log_install_failed "Goose" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash" "${GCORE_SERVER_IP}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Goose installation verified successfully"
|
||||
|
||||
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_step "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${GCORE_SERVER_IP}" upload_file run_server \
|
||||
"GOOSE_PROVIDER=openrouter" \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
|
||||
|
||||
echo ""
|
||||
log_info "Gcore instance setup completed successfully!"
|
||||
log_info "Server: ${SERVER_NAME} (ID: ${GCORE_SERVER_ID}, IP: ${GCORE_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Goose..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${GCORE_SERVER_IP}" "source ~/.zshrc && goose"
|
||||
457
gcore/lib/common.sh
Normal file
457
gcore/lib/common.sh
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
#!/bin/bash
|
||||
# Common bash functions for Gcore spawn scripts
|
||||
|
||||
# Bash safety flags
|
||||
set -eo pipefail
|
||||
|
||||
# ============================================================
|
||||
# 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
|
||||
|
||||
# ============================================================
|
||||
# Gcore specific functions
|
||||
# ============================================================
|
||||
|
||||
readonly GCORE_API_BASE="https://api.gcore.com"
|
||||
SPAWN_DASHBOARD_URL="https://portal.gcore.com/"
|
||||
|
||||
# Configurable timeout/delay constants
|
||||
INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5}
|
||||
|
||||
# Gcore API uses "apikey" auth header (not Bearer)
|
||||
gcore_api() {
|
||||
local method="$1"
|
||||
local endpoint="$2"
|
||||
local body="${3:-}"
|
||||
generic_cloud_api_custom_auth "$GCORE_API_BASE" "$method" "$endpoint" "$body" 3 \
|
||||
-H "Authorization: apikey ${GCORE_API_TOKEN}"
|
||||
}
|
||||
|
||||
ensure_gcore_token() {
|
||||
ensure_api_token_with_provider \
|
||||
"Gcore" \
|
||||
"GCORE_API_TOKEN" \
|
||||
"$HOME/.config/spawn/gcore.json" \
|
||||
"https://portal.gcore.com/cloud/profile/api-tokens" \
|
||||
test_gcore_token
|
||||
}
|
||||
|
||||
test_gcore_token() {
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/regions/${GCORE_PROJECT_ID:-}")
|
||||
# If no project ID set yet, try listing projects instead
|
||||
if [[ -z "${GCORE_PROJECT_ID:-}" ]]; then
|
||||
response=$(gcore_api GET "/cloud/v1/projects")
|
||||
fi
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure project ID is set (required for all Gcore cloud API calls)
|
||||
ensure_gcore_project() {
|
||||
if [[ -n "${GCORE_PROJECT_ID:-}" ]]; then
|
||||
log_info "Using Gcore project ID from environment"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try loading from config
|
||||
local config_file="$HOME/.config/spawn/gcore.json"
|
||||
if [[ -f "$config_file" ]]; then
|
||||
local saved_project
|
||||
saved_project=$(python3 -c "import json; d=json.load(open('$config_file')); print(d.get('project_id',''))" 2>/dev/null || true)
|
||||
if [[ -n "$saved_project" ]]; then
|
||||
GCORE_PROJECT_ID="$saved_project"
|
||||
export GCORE_PROJECT_ID
|
||||
log_info "Using Gcore project ID from config"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Auto-detect: use the first available project
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/projects")
|
||||
local project_id
|
||||
project_id=$(python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
projects = data if isinstance(data, list) else data.get('results', data.get('projects', []))
|
||||
if projects:
|
||||
print(projects[0]['id'])
|
||||
" <<< "$response" 2>/dev/null)
|
||||
|
||||
if [[ -z "$project_id" ]]; then
|
||||
log_error "Failed to detect Gcore project ID"
|
||||
log_error "Set GCORE_PROJECT_ID environment variable manually"
|
||||
return 1
|
||||
fi
|
||||
|
||||
GCORE_PROJECT_ID="$project_id"
|
||||
export GCORE_PROJECT_ID
|
||||
log_info "Auto-detected Gcore project: $GCORE_PROJECT_ID"
|
||||
|
||||
# Save to config
|
||||
if [[ -f "$config_file" ]]; then
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$config_file', 'r+') as f:
|
||||
d = json.load(f)
|
||||
d['project_id'] = '$project_id'
|
||||
f.seek(0); f.truncate()
|
||||
json.dump(d, f, indent=2)
|
||||
" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if SSH key is registered with Gcore
|
||||
gcore_check_ssh_key() {
|
||||
local fingerprint="$1"
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/ssh_keys/${GCORE_PROJECT_ID}")
|
||||
local results
|
||||
results=$(python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
items = data.get('results', data if isinstance(data, list) else [])
|
||||
for k in items:
|
||||
print(k.get('fingerprint', ''))
|
||||
" <<< "$response" 2>/dev/null)
|
||||
echo "$results" | grep -q "$fingerprint"
|
||||
}
|
||||
|
||||
# Register SSH key with Gcore
|
||||
gcore_register_ssh_key() {
|
||||
local key_name="$1"
|
||||
local pub_path="$2"
|
||||
local pub_key
|
||||
pub_key=$(cat "$pub_path")
|
||||
local json_pub_key json_name
|
||||
json_pub_key=$(json_escape "$pub_key")
|
||||
json_name=$(json_escape "$key_name")
|
||||
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key,\"project_id\":${GCORE_PROJECT_ID}}"
|
||||
local register_response
|
||||
register_response=$(gcore_api POST "/cloud/v1/ssh_keys/${GCORE_PROJECT_ID}" "$register_body")
|
||||
|
||||
if echo "$register_response" | grep -q '"id"'; then
|
||||
return 0
|
||||
else
|
||||
local error_msg
|
||||
error_msg=$(echo "$register_response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message', d.get('detail', 'Unknown error')))" 2>/dev/null || echo "$register_response")
|
||||
log_error "API Error: $error_msg"
|
||||
|
||||
log_warn "Common causes:"
|
||||
log_warn " - SSH key already registered with this name"
|
||||
log_warn " - Invalid SSH key format (must be valid ed25519 public key)"
|
||||
log_warn " - API key lacks write permissions"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_ssh_key() {
|
||||
ensure_ssh_key_with_provider gcore_check_ssh_key gcore_register_ssh_key "Gcore"
|
||||
}
|
||||
|
||||
get_server_name() {
|
||||
get_validated_server_name "GCORE_SERVER_NAME" "Enter server name: "
|
||||
}
|
||||
|
||||
# Get Ubuntu image ID
|
||||
get_ubuntu_image_id() {
|
||||
local region="${1:-ed-1}"
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/images/${GCORE_PROJECT_ID}/${region}")
|
||||
local image_id
|
||||
image_id=$(python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
images = data.get('results', data if isinstance(data, list) else [])
|
||||
best = None
|
||||
for img in images:
|
||||
name = img.get('display_name', img.get('name', '')).lower()
|
||||
os_distro = img.get('os_distro', '').lower()
|
||||
if 'ubuntu' in name or 'ubuntu' in os_distro:
|
||||
if '24.04' in name or 'noble' in name:
|
||||
print(img['id'])
|
||||
sys.exit(0)
|
||||
if '22.04' in name or 'jammy' in name:
|
||||
if best is None:
|
||||
best = img['id']
|
||||
elif best is None:
|
||||
best = img['id']
|
||||
if best:
|
||||
print(best)
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
" <<< "$response" 2>/dev/null)
|
||||
|
||||
if [[ -z "$image_id" ]]; then
|
||||
log_error "Failed to find Ubuntu image in region ${region}"
|
||||
log_error "Try a different GCORE_REGION"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$image_id"
|
||||
}
|
||||
|
||||
# Get the first available flavor matching our requirements
|
||||
get_flavor_id() {
|
||||
local region="${1:-ed-1}"
|
||||
local desired_flavor="${GCORE_FLAVOR:-g1-standard-1-2}"
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/flavors/${GCORE_PROJECT_ID}/${region}")
|
||||
local flavor_name
|
||||
flavor_name=$(python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
flavors = data.get('results', data if isinstance(data, list) else [])
|
||||
desired = sys.argv[1]
|
||||
# Try exact match first
|
||||
for f in flavors:
|
||||
if f.get('name', '') == desired or f.get('flavor_id', '') == desired:
|
||||
print(f.get('flavor_id', f.get('name', '')))
|
||||
sys.exit(0)
|
||||
# Fallback: find any small flavor (1-2 vCPUs, 1-4 GB RAM)
|
||||
for f in flavors:
|
||||
vcpus = f.get('vcpus', 0)
|
||||
ram = f.get('ram', 0)
|
||||
if 1 <= vcpus <= 2 and 1024 <= ram <= 4096:
|
||||
print(f.get('flavor_id', f.get('name', '')))
|
||||
sys.exit(0)
|
||||
# Last resort: use first available
|
||||
if flavors:
|
||||
print(flavors[0].get('flavor_id', flavors[0].get('name', '')))
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
" "$desired_flavor" <<< "$response" 2>/dev/null)
|
||||
|
||||
if [[ -z "$flavor_name" ]]; then
|
||||
log_error "Failed to find a suitable flavor in region ${region}"
|
||||
log_error "Try a different GCORE_REGION or set GCORE_FLAVOR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$flavor_name"
|
||||
}
|
||||
|
||||
# Get SSH key name for instance creation
|
||||
get_ssh_key_name() {
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/ssh_keys/${GCORE_PROJECT_ID}")
|
||||
local ssh_key_name
|
||||
ssh_key_name=$(python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
items = data.get('results', data if isinstance(data, list) else [])
|
||||
if items:
|
||||
# Prefer a key with 'spawn' in the name
|
||||
for k in items:
|
||||
if 'spawn' in k.get('name', '').lower():
|
||||
print(k['name'])
|
||||
sys.exit(0)
|
||||
print(items[0]['name'])
|
||||
" <<< "$response" 2>/dev/null)
|
||||
|
||||
if [[ -z "$ssh_key_name" ]]; then
|
||||
log_error "No SSH keys found in your Gcore account"
|
||||
log_error "Register a key at: https://portal.gcore.com/cloud/ssh-keys"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$ssh_key_name"
|
||||
}
|
||||
|
||||
# Generate cloud-init userdata script for Gcore instances
|
||||
get_cloud_init_userdata() {
|
||||
cat << 'CLOUD_INIT_EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl unzip git zsh
|
||||
# Install Bun
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
# Install Claude Code
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
# Configure PATH
|
||||
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /root/.bashrc
|
||||
echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /root/.zshrc
|
||||
# Signal completion
|
||||
touch /root/.cloud-init-complete
|
||||
CLOUD_INIT_EOF
|
||||
}
|
||||
|
||||
# Build the JSON request body for instance creation
|
||||
build_create_instance_body() {
|
||||
local name="$1" flavor="$2" region="$3"
|
||||
local image_id="$4" ssh_key_name="$5"
|
||||
local init_script="$6"
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
name = sys.argv[1]
|
||||
flavor = sys.argv[2]
|
||||
region = sys.argv[3]
|
||||
image_id = sys.argv[4]
|
||||
ssh_key_name = sys.argv[5]
|
||||
user_data = sys.stdin.read()
|
||||
|
||||
body = {
|
||||
'name': name,
|
||||
'flavor': flavor,
|
||||
'keypair_name': ssh_key_name,
|
||||
'volumes': [{
|
||||
'source': 'image',
|
||||
'image_id': image_id,
|
||||
'size': 20,
|
||||
'boot_index': 0,
|
||||
'delete_on_termination': True
|
||||
}],
|
||||
'interfaces': [{
|
||||
'type': 'external'
|
||||
}],
|
||||
'user_data': user_data
|
||||
}
|
||||
print(json.dumps(body))
|
||||
" "$name" "$flavor" "$region" "$image_id" "$ssh_key_name" <<< "$init_script"
|
||||
}
|
||||
|
||||
# Wait for a Gcore instance to become ACTIVE and retrieve its public IP
|
||||
# Sets: GCORE_SERVER_IP
|
||||
wait_for_gcore_instance() {
|
||||
local server_id="$1"
|
||||
local max_attempts=${2:-60}
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
generic_wait_for_instance gcore_api "/cloud/v1/instances/${GCORE_PROJECT_ID}/${region}/${server_id}" \
|
||||
"active" "d.get('vm_state',d.get('status','').lower())" \
|
||||
"[a.get('addr','') for net in d.get('addresses',{}).values() for a in net if a.get('type','')=='fixed' and '.' in a.get('addr','')][0] if d.get('addresses') else ''" \
|
||||
GCORE_SERVER_IP "Instance" "${max_attempts}"
|
||||
}
|
||||
|
||||
# Handle Gcore instance creation API error response
|
||||
_handle_gcore_create_error() {
|
||||
local response="$1"
|
||||
|
||||
log_error "Failed to create Gcore instance"
|
||||
|
||||
local error_msg
|
||||
error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('message', d.get('detail', 'Unknown error')))" 2>/dev/null || echo "$response")
|
||||
log_error "API Error: $error_msg"
|
||||
|
||||
log_warn "Common issues:"
|
||||
log_warn " - Insufficient account balance"
|
||||
log_warn " - Flavor unavailable in region (try different GCORE_FLAVOR or GCORE_REGION)"
|
||||
log_warn " - Instance limit reached"
|
||||
log_warn "Check your dashboard: https://portal.gcore.com/"
|
||||
}
|
||||
|
||||
create_server() {
|
||||
local name="$1"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
|
||||
# Validate env var inputs
|
||||
validate_region_name "$region" || { log_error "Invalid GCORE_REGION"; return 1; }
|
||||
|
||||
log_step "Creating Gcore instance '$name' (region: $region)..."
|
||||
|
||||
# Gather required resource IDs
|
||||
local flavor image_id ssh_key_name
|
||||
flavor=$(get_flavor_id "$region") || return 1
|
||||
image_id=$(get_ubuntu_image_id "$region") || return 1
|
||||
ssh_key_name=$(get_ssh_key_name) || return 1
|
||||
|
||||
log_info "Using flavor: $flavor, image: $image_id"
|
||||
|
||||
# Build request body with cloud-init userdata
|
||||
local init_script
|
||||
init_script=$(get_cloud_init_userdata)
|
||||
|
||||
local body
|
||||
body=$(build_create_instance_body "$name" "$flavor" "$region" "$image_id" "$ssh_key_name" "$init_script")
|
||||
|
||||
local response
|
||||
response=$(gcore_api POST "/cloud/v2/instances/${GCORE_PROJECT_ID}/${region}" "$body")
|
||||
|
||||
# Gcore v2 create returns a task; extract instance ID from tasks array
|
||||
local instance_id
|
||||
instance_id=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
d = json.loads(sys.stdin.read())
|
||||
# v2 returns {\"tasks\": [\"task-uuid\"], \"instances\": [\"instance-uuid\"]}
|
||||
instances = d.get('instances', [])
|
||||
if instances:
|
||||
print(instances[0])
|
||||
sys.exit(0)
|
||||
# Fallback: try direct id field
|
||||
if 'id' in d:
|
||||
print(d['id'])
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
" 2>/dev/null)
|
||||
|
||||
if [[ -z "${instance_id:-}" ]]; then
|
||||
_handle_gcore_create_error "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
GCORE_SERVER_ID="$instance_id"
|
||||
export GCORE_SERVER_ID
|
||||
log_info "Instance created: ID=$GCORE_SERVER_ID"
|
||||
|
||||
wait_for_gcore_instance "$GCORE_SERVER_ID"
|
||||
}
|
||||
|
||||
# SSH operations -- delegates to shared helpers (SSH_USER defaults to root)
|
||||
verify_server_connectivity() { ssh_verify_connectivity "$@"; }
|
||||
run_server() { ssh_run_server "$@"; }
|
||||
upload_file() { ssh_upload_file "$@"; }
|
||||
interactive_session() { ssh_interactive_session "$@"; }
|
||||
|
||||
destroy_server() {
|
||||
local server_id="$1"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
log_step "Destroying instance $server_id..."
|
||||
gcore_api DELETE "/cloud/v1/instances/${GCORE_PROJECT_ID}/${region}/$server_id"
|
||||
log_info "Instance $server_id destroyed"
|
||||
}
|
||||
|
||||
list_servers() {
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
local response
|
||||
response=$(gcore_api GET "/cloud/v1/instances/${GCORE_PROJECT_ID}/${region}")
|
||||
python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
items = data.get('results', data if isinstance(data, list) else [])
|
||||
if not items:
|
||||
print('No instances found')
|
||||
sys.exit(0)
|
||||
print(f\"{'NAME':<25} {'ID':<40} {'STATUS':<12} {'IP':<16} {'FLAVOR':<15}\")
|
||||
print('-' * 108)
|
||||
for i in items:
|
||||
name = i.get('name', 'N/A')
|
||||
iid = i['id']
|
||||
status = i.get('vm_state', i.get('status', 'N/A'))
|
||||
flavor = i.get('flavor', {}).get('flavor_id', i.get('flavor', {}).get('name', 'N/A')) if isinstance(i.get('flavor'), dict) else i.get('flavor', 'N/A')
|
||||
ip = 'N/A'
|
||||
for net_addrs in i.get('addresses', {}).values():
|
||||
for a in net_addrs:
|
||||
if a.get('type') == 'fixed' and '.' in a.get('addr', ''):
|
||||
ip = a['addr']
|
||||
break
|
||||
if ip != 'N/A':
|
||||
break
|
||||
print(f'{name:<25} {iid:<40} {status:<12} {ip:<16} {flavor:<15}')
|
||||
" <<< "$response"
|
||||
}
|
||||
|
|
@ -842,6 +842,22 @@
|
|||
"location": "nl1"
|
||||
},
|
||||
"notes": "Global cloud provider with locations in EU, US, and Asia. Simple X-API-KEY auth. Async server creation with task polling. Requires SERVERSPACE_API_KEY from https://my.serverspace.io/project/api"
|
||||
},
|
||||
"gcore": {
|
||||
"name": "Gcore",
|
||||
"description": "Gcore Cloud instances via REST API",
|
||||
"url": "https://gcore.com/cloud",
|
||||
"type": "api",
|
||||
"auth": "GCORE_API_TOKEN",
|
||||
"provision_method": "POST /cloud/v2/instances/{project_id}/{region_id}",
|
||||
"exec_method": "ssh root@IP",
|
||||
"interactive_method": "ssh -t root@IP",
|
||||
"defaults": {
|
||||
"flavor": "g1-standard-1-2",
|
||||
"region": "ed-1",
|
||||
"image": "Ubuntu 24.04"
|
||||
},
|
||||
"notes": "Global edge cloud with 180+ PoPs, hourly billing. Requires project_id in API paths."
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
|
|
@ -1429,6 +1445,21 @@
|
|||
"serverspace/opencode": "missing",
|
||||
"serverspace/plandex": "missing",
|
||||
"serverspace/kilocode": "missing",
|
||||
"serverspace/continue": "missing"
|
||||
"serverspace/continue": "missing",
|
||||
"gcore/claude": "implemented",
|
||||
"gcore/aider": "implemented",
|
||||
"gcore/goose": "implemented",
|
||||
"gcore/openclaw": "missing",
|
||||
"gcore/nanoclaw": "missing",
|
||||
"gcore/codex": "missing",
|
||||
"gcore/interpreter": "missing",
|
||||
"gcore/gemini": "missing",
|
||||
"gcore/amazonq": "missing",
|
||||
"gcore/cline": "missing",
|
||||
"gcore/gptme": "missing",
|
||||
"gcore/opencode": "missing",
|
||||
"gcore/plandex": "missing",
|
||||
"gcore/kilocode": "missing",
|
||||
"gcore/continue": "missing"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
test/fixtures/gcore/_api_assertions.sh
vendored
Normal file
2
test/fixtures/gcore/_api_assertions.sh
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
assert_api_called "GET" "/cloud/v1/ssh_keys/" "fetches SSH keys"
|
||||
assert_api_called "POST" "/cloud/v2/instances/" "creates instance"
|
||||
4
test/fixtures/gcore/_env.sh
vendored
Normal file
4
test/fixtures/gcore/_env.sh
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export GCORE_API_TOKEN="test-token-gcore"
|
||||
export GCORE_PROJECT_ID="12345"
|
||||
export GCORE_SERVER_NAME="test-srv"
|
||||
export GCORE_REGION="ed-1"
|
||||
11
test/fixtures/gcore/_metadata.json
vendored
Normal file
11
test/fixtures/gcore/_metadata.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cloud": "gcore",
|
||||
"recorded_at": "2026-02-14T00:00:00Z",
|
||||
"fixtures": {
|
||||
"projects": {"endpoint": "/cloud/v1/projects", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"ssh_keys": {"endpoint": "/cloud/v1/ssh_keys/12345", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"instances": {"endpoint": "/cloud/v1/instances/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"images": {"endpoint": "/cloud/v1/images/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"flavors": {"endpoint": "/cloud/v1/flavors/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"}
|
||||
}
|
||||
}
|
||||
4
test/fixtures/gcore/create_server.json
vendored
Normal file
4
test/fixtures/gcore/create_server.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tasks": ["task-uuid-1234"],
|
||||
"instances": ["instance-uuid-5678"]
|
||||
}
|
||||
35
test/fixtures/gcore/flavors.json
vendored
Normal file
35
test/fixtures/gcore/flavors.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"flavor_id": "g1-standard-1-2",
|
||||
"name": "g1-standard-1-2",
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.014,
|
||||
"price_per_month": 10.0
|
||||
},
|
||||
{
|
||||
"flavor_id": "g1-standard-2-4",
|
||||
"name": "g1-standard-2-4",
|
||||
"vcpus": 2,
|
||||
"ram": 4096,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.028,
|
||||
"price_per_month": 20.0
|
||||
},
|
||||
{
|
||||
"flavor_id": "g1-standard-4-8",
|
||||
"name": "g1-standard-4-8",
|
||||
"vcpus": 4,
|
||||
"ram": 8192,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.056,
|
||||
"price_per_month": 40.0
|
||||
}
|
||||
]
|
||||
}
|
||||
27
test/fixtures/gcore/images.json
vendored
Normal file
27
test/fixtures/gcore/images.json
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"count": 2,
|
||||
"results": [
|
||||
{
|
||||
"id": "img-ubuntu-2404",
|
||||
"name": "ubuntu-24.04-x64",
|
||||
"display_name": "Ubuntu 24.04 LTS",
|
||||
"os_distro": "ubuntu",
|
||||
"os_version": "24.04",
|
||||
"status": "active",
|
||||
"min_disk": 10,
|
||||
"min_ram": 512,
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "img-ubuntu-2204",
|
||||
"name": "ubuntu-22.04-x64",
|
||||
"display_name": "Ubuntu 22.04 LTS",
|
||||
"os_distro": "ubuntu",
|
||||
"os_version": "22.04",
|
||||
"status": "active",
|
||||
"min_disk": 10,
|
||||
"min_ram": 512,
|
||||
"created_at": "2025-06-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
test/fixtures/gcore/instances.json
vendored
Normal file
4
test/fixtures/gcore/instances.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"count": 0,
|
||||
"results": []
|
||||
}
|
||||
11
test/fixtures/gcore/projects.json
vendored
Normal file
11
test/fixtures/gcore/projects.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"count": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "Default project",
|
||||
"state": "active",
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
test/fixtures/gcore/ssh_keys.json
vendored
Normal file
14
test/fixtures/gcore/ssh_keys.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"count": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": "ssh-key-uuid-1234",
|
||||
"name": "spawn-test-key",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHmcVdzydp72a/B69nmENZvCvjuk7xGpKdi5CvhkmNsv test@test",
|
||||
"fingerprint": "af:0d:c5:57:a8:fd:b2:82:5e:d4:c1:65:f0:0c:8a:9d",
|
||||
"state": "active",
|
||||
"project_id": 12345,
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -267,6 +267,13 @@ _strip_api_base() {
|
|||
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
|
||||
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
|
||||
https://api.serverspace.io/api/v1*) ENDPOINT="${URL#https://api.serverspace.io/api/v1}" ;;
|
||||
https://api.gcore.com/cloud/v*/instances/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/instances/[^/]*/[^/]*/|/instances/|') ;;
|
||||
https://api.gcore.com/cloud/v*/instances/*/*) ENDPOINT="/instances" ;;
|
||||
https://api.gcore.com/cloud/v*/*/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*/[^/]*/|/\1/|') ;;
|
||||
https://api.gcore.com/cloud/v*/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*/[^/]*$|/\1|') ;;
|
||||
https://api.gcore.com/cloud/v*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*$|/\1|') ;;
|
||||
https://api.gcore.com/cloud/v*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/||; s|^|/|') ;;
|
||||
https://api.gcore.com*) ENDPOINT="${URL#https://api.gcore.com}" !!
|
||||
esac
|
||||
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
|
||||
}
|
||||
|
|
@ -294,6 +301,7 @@ _validate_body() {
|
|||
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
|
||||
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
|
||||
serverspace) case "$EP_CLEAN" in /servers) _check_fields "name location_id image_id cpu ram_mb" ;; esac ;;
|
||||
gcore) case "$EP_CLEAN" in /instances) _check_fields "name flavor volumes interfaces" ;; esac !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +321,7 @@ _synthetic_active_response() {
|
|||
civo) printf '{"id":"test-uuid-1234","hostname":"test-srv","status":"ACTIVE","public_ip":"10.0.0.1","size":"g4s.small"}' ;;
|
||||
scaleway) printf '{"server":{"id":"test-uuid-1234","name":"test-srv","state":"running","public_ip":{"address":"10.0.0.1"},"public_ips":[{"address":"10.0.0.1"}]}}' ;;
|
||||
serverspace) printf '{"id":"test-uuid-1234","name":"test-srv","status":"Active","nics":[{"ip_address":"10.0.0.1"}]}' ;;
|
||||
gcore) printf '{"id":"instance-uuid-5678","name":"test-srv","vm_state":"active","status":"ACTIVE","addresses":{"public":[ {"addr":"10.0.0.1","type":"fixed"}]},"flavor":{"flavor_id":"g1-standard-1-2"}}' !!
|
||||
*) printf '{}' ;;
|
||||
esac
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ ERRORS=0
|
|||
PROMPT_FOR_CREDS=true
|
||||
|
||||
# All clouds with REST APIs that we can record from
|
||||
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock serverspace"
|
||||
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock serverspace gcore"
|
||||
|
||||
# --- Endpoint registry ---
|
||||
# Format: "fixture_name:endpoint"
|
||||
|
|
@ -157,6 +157,14 @@ get_endpoints() {
|
|||
"locations:/locations" \
|
||||
"images:/images"
|
||||
;;
|
||||
gcore)
|
||||
printf '%s\n' \
|
||||
"projects:/cloud/v1/projects" \
|
||||
"ssh_keys:/cloud/v1/ssh_keys/${GCORE_PROJECT_ID:-MISSING}" \
|
||||
"instances:/cloud/v1/instances/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}" \
|
||||
"images:/cloud/v1/images/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}" \
|
||||
"flavors:/cloud/v1/flavors/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}"
|
||||
!!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +285,7 @@ get_auth_env_var() {
|
|||
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
|
||||
webdock) printf "WEBDOCK_API_TOKEN" ;;
|
||||
serverspace) printf "SERVERSPACE_API_KEY" ;;
|
||||
gcore) printf "GCORE_API_TOKEN" !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -427,6 +436,7 @@ call_api() {
|
|||
cloudsigma) cloudsigma_api GET "$endpoint" ;;
|
||||
webdock) webdock_api GET "$endpoint" ;;
|
||||
serverspace) serverspace_api GET "$endpoint" ;;
|
||||
gcore) gcore_api GET "$endpoint" !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +488,9 @@ elif cloud == 'webdock':
|
|||
elif cloud == 'serverspace':
|
||||
# ServerSpace returns error objects with 'error' field
|
||||
sys.exit(0 if 'error' in d and d['error'] else 1)
|
||||
elif cloud == 'gcore':
|
||||
# Gcore returns error objects with 'message' or 'detail' fields
|
||||
sys.exit(0 if ('message' in d and len(d) <= 3 and not any(k in d for k in ('count','results','id','name'))) or ('detail' in d and len(d) <= 2) else 1)
|
||||
else:
|
||||
sys.exit(1)
|
||||
" "$cloud" 2>/dev/null
|
||||
|
|
@ -508,6 +521,7 @@ _record_live_cycle() {
|
|||
civo) _live_civo "$fixture_dir" ;;
|
||||
atlanticnet) _live_atlanticnet "$fixture_dir" ;;
|
||||
serverspace) _live_serverspace "$fixture_dir" ;;
|
||||
gcore) _live_gcore "$fixture_dir" !!
|
||||
*) return 0 ;; # No live cycle for this cloud yet
|
||||
esac
|
||||
}
|
||||
|
|
@ -869,6 +883,58 @@ _live_serverspace() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_live_gcore_body() {
|
||||
local fixture_dir="$1"
|
||||
local name="spawn-record-$(date +%s)"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
local project_id="${GCORE_PROJECT_ID:-}"
|
||||
printf '%b\n' " ${CYAN}live${NC} Creating test instance '${name}' (g1-standard-1-2, ${region})..." >&2
|
||||
|
||||
local ssh_keys_response
|
||||
ssh_keys_response=$(gcore_api GET "/cloud/v1/ssh_keys/${project_id}")
|
||||
local ssh_key_name
|
||||
ssh_key_name=$(echo "$ssh_keys_response" | python3 -c "
|
||||
import json, sys
|
||||
d = json.loads(sys.stdin.read())
|
||||
keys = d.get('results', [])
|
||||
print(keys[0]['name'] if keys else '')
|
||||
" 2>/dev/null) || ssh_key_name=""
|
||||
|
||||
local image_id
|
||||
image_id=$(echo "$(gcore_api GET "/cloud/v1/images/${project_id}/${region}")" | python3 -c "
|
||||
import json, sys
|
||||
d = json.loads(sys.stdin.read())
|
||||
for img in d.get('results', []):
|
||||
if 'ubuntu' in img.get('name', '').lower() and '24' in img.get('os_version', ''):
|
||||
print(img['id']); break
|
||||
else:
|
||||
imgs = d.get('results', [])
|
||||
if imgs: print(imgs[0]['id'])
|
||||
" 2>/dev/null) || image_id=""
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
body = {
|
||||
'name': sys.argv[1],
|
||||
'flavor': 'g1-standard-1-2',
|
||||
'volumes': [{'source': 'image', 'image_id': sys.argv[3], 'size': 20, 'boot_index': 0}],
|
||||
'interfaces': [{'type': 'external'}]
|
||||
}
|
||||
if sys.argv[2]:
|
||||
body['keypair_name'] = sys.argv[2]
|
||||
print(json.dumps(body))
|
||||
" "$name" "$ssh_key_name" "$image_id"
|
||||
}
|
||||
|
||||
_live_gcore() {
|
||||
local project_id="${GCORE_PROJECT_ID:-}"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
_live_create_delete_cycle "$1" gcore_api \
|
||||
"/cloud/v2/instances/${project_id}/${region}" \
|
||||
"/cloud/v1/instances/${project_id}/${region}/{id}" \
|
||||
"d.get('instances',[''])[0]" _live_gcore_body 5
|
||||
}
|
||||
|
||||
# --- Record one cloud ---
|
||||
# Check credentials and prompt if needed; returns 1 to skip this cloud
|
||||
_record_ensure_credentials() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue