mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-06 16:31:08 +00:00
feat: Add Alibaba Cloud provider support (#1002)
Adds Alibaba Cloud (Aliyun) ECS provider with 3 initial agent implementations. Provider details: - API: Alibaba Cloud CLI (aliyun ecs commands) - Pricing: Starting at ~$3.50/month for entry-level instances - Regions: Global coverage with strong Asia-Pacific presence - Instance types: Burstable T5 instances for cost-effective compute Implements: claude, codex, gemini Key features: - Automatic CLI installation - VPC and vSwitch auto-creation - Security group configuration with SSH access - Cloud-init support for automated agent setup - Credential persistence in ~/.config/spawn/alibabacloud.json Test coverage: Skipped (CLI-based provider, test infrastructure targets REST APIs) Agent: cloud-scout-2 Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
69d08e6b1d
commit
0d9307a907
6 changed files with 817 additions and 1 deletions
94
alibabacloud/README.md
Normal file
94
alibabacloud/README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Alibaba Cloud
|
||||
|
||||
Alibaba Cloud ECS (Elastic Compute Service) instances via Alibaba Cloud CLI. [Alibaba Cloud](https://www.alibabacloud.com/)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The scripts will automatically install the Alibaba Cloud CLI (`aliyun`) if not present.
|
||||
|
||||
Get your Access Key credentials from: https://ram.console.aliyun.com/manage/ak
|
||||
|
||||
## Agents
|
||||
|
||||
#### Claude Code
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/alibabacloud/claude.sh)
|
||||
```
|
||||
|
||||
#### Codex CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/alibabacloud/codex.sh)
|
||||
```
|
||||
|
||||
#### Gemini CLI
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/alibabacloud/gemini.sh)
|
||||
```
|
||||
|
||||
## Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
ALIYUN_INSTANCE_NAME=dev-mk1 \
|
||||
ALIYUN_ACCESS_KEY_ID=your-access-key-id \
|
||||
ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret \
|
||||
ALIYUN_REGION=cn-hangzhou \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://openrouter.ai/labs/spawn/alibabacloud/claude.sh)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `ALIYUN_ACCESS_KEY_ID` - Alibaba Cloud Access Key ID
|
||||
- `ALIYUN_ACCESS_KEY_SECRET` - Alibaba Cloud Access Key Secret
|
||||
- `ALIYUN_REGION` - Region (default: `cn-hangzhou`)
|
||||
- `ALIYUN_INSTANCE_NAME` - Instance name (default: prompted)
|
||||
- `ALIYUN_INSTANCE_TYPE` - Instance type (default: `ecs.t5-lc1m2.small`)
|
||||
- `ALIYUN_IMAGE_ID` - Image ID (default: Ubuntu 24.04)
|
||||
- `OPENROUTER_API_KEY` - OpenRouter API key
|
||||
|
||||
## Regions
|
||||
|
||||
Common Alibaba Cloud regions:
|
||||
- `cn-hangzhou` - China (Hangzhou)
|
||||
- `cn-shanghai` - China (Shanghai)
|
||||
- `cn-beijing` - China (Beijing)
|
||||
- `cn-shenzhen` - China (Shenzhen)
|
||||
- `ap-southeast-1` - Singapore
|
||||
- `ap-southeast-5` - Jakarta
|
||||
- `us-west-1` - US (Silicon Valley)
|
||||
- `us-east-1` - US (Virginia)
|
||||
- `eu-central-1` - Germany (Frankfurt)
|
||||
|
||||
Full list: https://www.alibabacloud.com/help/en/ecs/user-guide/regions-and-zones
|
||||
|
||||
## Instance Types
|
||||
|
||||
The default instance type is `ecs.t5-lc1m2.small` (1 vCPU, 2GB RAM, burstable).
|
||||
|
||||
Other affordable options:
|
||||
- `ecs.t5-lc1m1.small` - 1 vCPU, 1GB RAM (cheaper)
|
||||
- `ecs.t5-lc1m2.small` - 1 vCPU, 2GB RAM (default)
|
||||
- `ecs.t5-lc2m4.large` - 2 vCPU, 4GB RAM (more power)
|
||||
|
||||
Full list: https://www.alibabacloud.com/help/en/ecs/user-guide/overview-of-instance-families
|
||||
|
||||
## Pricing
|
||||
|
||||
Pricing varies by region and instance type. As of 2026:
|
||||
- Entry-level instances start at ~$3.50/month
|
||||
- Pay-as-you-go billing available
|
||||
- Reserved instances offer up to 79% discount
|
||||
|
||||
Check current pricing: https://www.alibabacloud.com/pricing
|
||||
|
||||
## Notes
|
||||
|
||||
- Credentials are saved to `~/.config/spawn/alibabacloud.json` after first use
|
||||
- The Alibaba Cloud CLI is automatically installed if not present
|
||||
- Instances are created with cloud-init for automated setup
|
||||
- SSH keys are automatically registered with Alibaba Cloud
|
||||
- A VPC and vSwitch are created if none exist in the region
|
||||
- A security group with SSH access (port 22) is created automatically
|
||||
58
alibabacloud/claude.sh
Normal file
58
alibabacloud/claude.sh
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=alibabacloud/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/alibabacloud/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Claude Code on Alibaba Cloud"
|
||||
echo ""
|
||||
|
||||
ensure_aliyun_credentials
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${ALIYUN_INSTANCE_IP}"
|
||||
wait_for_cloud_init "${ALIYUN_INSTANCE_IP}" 60
|
||||
|
||||
log_step "Verifying Claude Code installation..."
|
||||
if ! run_server "${ALIYUN_INSTANCE_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 "${ALIYUN_INSTANCE_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 "${ALIYUN_INSTANCE_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 ${ALIYUN_INSTANCE_IP}" \
|
||||
"run_server ${ALIYUN_INSTANCE_IP}"
|
||||
|
||||
echo ""
|
||||
log_info "Alibaba Cloud instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (ID: ${ALIYUN_INSTANCE_ID}, IP: ${ALIYUN_INSTANCE_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Claude Code..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${ALIYUN_INSTANCE_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"
|
||||
48
alibabacloud/codex.sh
Normal file
48
alibabacloud/codex.sh
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=alibabacloud/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/alibabacloud/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Codex CLI on Alibaba Cloud"
|
||||
echo ""
|
||||
|
||||
ensure_aliyun_credentials
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${ALIYUN_INSTANCE_IP}"
|
||||
wait_for_cloud_init "${ALIYUN_INSTANCE_IP}" 60
|
||||
|
||||
log_step "Installing Codex CLI..."
|
||||
run_server "${ALIYUN_INSTANCE_IP}" "npm install -g @openai/codex"
|
||||
log_info "Codex CLI 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 "${ALIYUN_INSTANCE_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"OPENAI_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"OPENAI_BASE_URL=https://openrouter.ai/api/v1"
|
||||
|
||||
echo ""
|
||||
log_info "Alibaba Cloud instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (ID: ${ALIYUN_INSTANCE_ID}, IP: ${ALIYUN_INSTANCE_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Codex..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${ALIYUN_INSTANCE_IP}" "source ~/.zshrc && codex"
|
||||
52
alibabacloud/gemini.sh
Normal file
52
alibabacloud/gemini.sh
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#!/bin/bash
|
||||
# shellcheck disable=SC2154
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=alibabacloud/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/alibabacloud/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Gemini CLI on Alibaba Cloud"
|
||||
echo ""
|
||||
|
||||
ensure_aliyun_credentials
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity "${ALIYUN_INSTANCE_IP}"
|
||||
wait_for_cloud_init "${ALIYUN_INSTANCE_IP}" 60
|
||||
|
||||
log_step "Installing Gemini CLI..."
|
||||
run_server "${ALIYUN_INSTANCE_IP}" "npm install -g @google/gemini-cli"
|
||||
log_info "Gemini CLI 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 "${ALIYUN_INSTANCE_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"GEMINI_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"OPENAI_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"OPENAI_BASE_URL=https://openrouter.ai/api/v1"
|
||||
|
||||
echo ""
|
||||
log_info "Alibaba Cloud instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (ID: ${ALIYUN_INSTANCE_ID}, IP: ${ALIYUN_INSTANCE_IP})"
|
||||
echo ""
|
||||
|
||||
log_step "Starting Gemini..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${ALIYUN_INSTANCE_IP}" "source ~/.zshrc && gemini"
|
||||
534
alibabacloud/lib/common.sh
Normal file
534
alibabacloud/lib/common.sh
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
#!/bin/bash
|
||||
# Common bash functions for Alibaba Cloud 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
|
||||
|
||||
# ============================================================
|
||||
# Alibaba Cloud specific functions
|
||||
# ============================================================
|
||||
|
||||
# Configurable timeout/delay constants
|
||||
INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} # Delay between instance status checks
|
||||
|
||||
# Install Alibaba Cloud CLI if not present
|
||||
ensure_aliyun_cli() {
|
||||
if command -v aliyun &> /dev/null; then
|
||||
log_info "Alibaba Cloud CLI is already installed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_step "Installing Alibaba Cloud CLI..."
|
||||
if ! bash -c "$(curl -fsSL https://aliyuncli.alicdn.com/install.sh)"; then
|
||||
log_error "Failed to install Alibaba Cloud CLI"
|
||||
log_error "How to fix:"
|
||||
log_error " 1. Install manually from: https://www.alibabacloud.com/help/en/cli"
|
||||
log_error " 2. Ensure curl and bash are available"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if ! command -v aliyun &> /dev/null; then
|
||||
log_error "Alibaba Cloud CLI installation completed but 'aliyun' command not found"
|
||||
log_error "You may need to restart your shell or add it to PATH"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Alibaba Cloud CLI installed successfully"
|
||||
}
|
||||
|
||||
# Test Alibaba Cloud credentials by listing regions
|
||||
test_aliyun_credentials() {
|
||||
local response
|
||||
response=$(aliyun ecs DescribeRegions --output json 2>&1 || echo "")
|
||||
|
||||
if echo "$response" | grep -q '"Regions"'; then
|
||||
log_info "Credentials validated"
|
||||
return 0
|
||||
else
|
||||
log_error "Credential validation failed"
|
||||
log_error "API Error: $response"
|
||||
log_error ""
|
||||
log_error "How to fix:"
|
||||
log_error " 1. Get your Access Key ID and Secret from:"
|
||||
log_error " https://ram.console.aliyun.com/manage/ak"
|
||||
log_error " 2. Ensure the AccessKey has ECS permissions"
|
||||
log_error " 3. Check the region is correct (default: cn-hangzhou)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure Alibaba Cloud credentials are configured
|
||||
ensure_aliyun_credentials() {
|
||||
ensure_aliyun_cli
|
||||
|
||||
# Check if already configured
|
||||
if aliyun configure list 2>/dev/null | grep -q "Profile"; then
|
||||
if test_aliyun_credentials; then
|
||||
return 0
|
||||
fi
|
||||
log_warn "Existing credentials invalid, need to reconfigure"
|
||||
fi
|
||||
|
||||
# Get credentials from env vars or config file or prompt
|
||||
local config_file="$HOME/.config/spawn/alibabacloud.json"
|
||||
local ak_id="${ALIYUN_ACCESS_KEY_ID:-}"
|
||||
local ak_secret="${ALIYUN_ACCESS_KEY_SECRET:-}"
|
||||
local region="${ALIYUN_REGION:-cn-hangzhou}"
|
||||
|
||||
# Try loading from config file
|
||||
if [[ -z "$ak_id" ]] && [[ -f "$config_file" ]]; then
|
||||
log_step "Loading credentials from $config_file"
|
||||
local creds
|
||||
creds=$(_load_json_config_fields "$config_file" "access_key_id" "access_key_secret" "region" 2>/dev/null || echo "")
|
||||
if [[ -n "$creds" ]]; then
|
||||
IFS=$'\t' read -r ak_id ak_secret region <<< "$creds"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Prompt for missing credentials
|
||||
if [[ -z "$ak_id" ]] || [[ -z "$ak_secret" ]]; then
|
||||
log_info "Alibaba Cloud credentials not found"
|
||||
log_info "Get your Access Key from: https://ram.console.aliyun.com/manage/ak"
|
||||
echo ""
|
||||
|
||||
if [[ -z "$ak_id" ]]; then
|
||||
printf "Enter Access Key ID: "
|
||||
ak_id=$(safe_read)
|
||||
fi
|
||||
|
||||
if [[ -z "$ak_secret" ]]; then
|
||||
printf "Enter Access Key Secret: "
|
||||
ak_secret=$(safe_read)
|
||||
fi
|
||||
|
||||
# Save to config file
|
||||
mkdir -p "$(dirname "$config_file")"
|
||||
_save_json_config "$config_file" \
|
||||
"access_key_id" "$ak_id" \
|
||||
"access_key_secret" "$ak_secret" \
|
||||
"region" "$region"
|
||||
log_info "Credentials saved to $config_file"
|
||||
fi
|
||||
|
||||
# Configure aliyun CLI
|
||||
log_step "Configuring Alibaba Cloud CLI..."
|
||||
aliyun configure set \
|
||||
--mode AK \
|
||||
--access-key-id "$ak_id" \
|
||||
--access-key-secret "$ak_secret" \
|
||||
--region "$region" \
|
||||
--language en
|
||||
|
||||
# Verify
|
||||
if test_aliyun_credentials; then
|
||||
return 0
|
||||
else
|
||||
log_error "Credential configuration failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if SSH key pair exists in Alibaba Cloud
|
||||
aliyun_check_ssh_key() {
|
||||
local key_name="$1"
|
||||
local response
|
||||
response=$(aliyun ecs DescribeKeyPairs --KeyPairName "$key_name" --output json 2>/dev/null || echo "{}")
|
||||
|
||||
if echo "$response" | grep -q '"KeyPairName"'; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Register SSH key with Alibaba Cloud
|
||||
aliyun_register_ssh_key() {
|
||||
local key_name="$1"
|
||||
local pub_path="$2"
|
||||
local pub_key
|
||||
pub_key=$(cat "$pub_path")
|
||||
|
||||
local response
|
||||
response=$(aliyun ecs ImportKeyPair \
|
||||
--KeyPairName "$key_name" \
|
||||
--PublicKeyBody "$pub_key" \
|
||||
--output json 2>&1 || echo "")
|
||||
|
||||
if echo "$response" | grep -q '"KeyPairName"'; then
|
||||
log_info "SSH key registered successfully"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to register SSH key"
|
||||
log_error "API Error: $response"
|
||||
log_error ""
|
||||
log_error "Common causes:"
|
||||
log_error " - SSH key already registered with this name"
|
||||
log_error " - Invalid SSH key format (must be valid RSA or ed25519 public key)"
|
||||
log_error " - Access Key lacks ECS write permissions"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure SSH key exists locally and is registered with Alibaba Cloud
|
||||
ensure_ssh_key() {
|
||||
ensure_ssh_key_with_provider aliyun_check_ssh_key aliyun_register_ssh_key "Alibaba Cloud"
|
||||
}
|
||||
|
||||
# Get server name from env var or prompt
|
||||
get_server_name() {
|
||||
get_validated_server_name "ALIYUN_INSTANCE_NAME" "Enter instance name: "
|
||||
}
|
||||
|
||||
# Wait for Alibaba Cloud ECS instance to become running
|
||||
# Sets: ALIYUN_INSTANCE_IP
|
||||
# Usage: _wait_for_aliyun_instance INSTANCE_ID [MAX_ATTEMPTS]
|
||||
_wait_for_aliyun_instance() {
|
||||
local instance_id="$1"
|
||||
local max_attempts="${2:-60}"
|
||||
local attempt=0
|
||||
|
||||
log_step "Waiting for instance to start (max ${max_attempts} attempts)..."
|
||||
|
||||
while [[ $attempt -lt $max_attempts ]]; do
|
||||
local response
|
||||
response=$(aliyun ecs DescribeInstances \
|
||||
--InstanceIds "[\"$instance_id\"]" \
|
||||
--output json 2>/dev/null || echo "{}")
|
||||
|
||||
local status ip_address
|
||||
status=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
instances = data.get('Instances', {}).get('Instance', [])
|
||||
if instances:
|
||||
print(instances[0].get('Status', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
ip_address=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
instances = data.get('Instances', {}).get('Instance', [])
|
||||
if instances:
|
||||
ips = instances[0].get('PublicIpAddress', {}).get('IpAddress', [])
|
||||
if ips:
|
||||
print(ips[0])
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ "$status" == "Running" ]] && [[ -n "$ip_address" ]]; then
|
||||
ALIYUN_INSTANCE_IP="$ip_address"
|
||||
export ALIYUN_INSTANCE_IP
|
||||
log_info "Instance is running (IP: ${ALIYUN_INSTANCE_IP})"
|
||||
return 0
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
if [[ $attempt -lt $max_attempts ]]; then
|
||||
log_step "Instance status: $status (attempt $attempt/$max_attempts)"
|
||||
sleep "$INSTANCE_STATUS_POLL_DELAY"
|
||||
fi
|
||||
done
|
||||
|
||||
log_error "Instance did not become ready within expected time"
|
||||
log_error "Check instance status: aliyun ecs DescribeInstances --InstanceIds '[\"$instance_id\"]'"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Create security group if it doesn't exist
|
||||
# Usage: _ensure_security_group
|
||||
_ensure_security_group() {
|
||||
local vpc_id="$1"
|
||||
local sg_name="${ALIYUN_SECURITY_GROUP_NAME:-spawn-default}"
|
||||
|
||||
# Check if security group already exists
|
||||
local response
|
||||
response=$(aliyun ecs DescribeSecurityGroups \
|
||||
--VpcId "$vpc_id" \
|
||||
--SecurityGroupName "$sg_name" \
|
||||
--output json 2>/dev/null || echo "{}")
|
||||
|
||||
local sg_id
|
||||
sg_id=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
groups = data.get('SecurityGroups', {}).get('SecurityGroup', [])
|
||||
if groups:
|
||||
print(groups[0].get('SecurityGroupId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$sg_id" ]]; then
|
||||
echo "$sg_id"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create new security group
|
||||
log_step "Creating security group '$sg_name'..."
|
||||
response=$(aliyun ecs CreateSecurityGroup \
|
||||
--VpcId "$vpc_id" \
|
||||
--SecurityGroupName "$sg_name" \
|
||||
--Description "Created by spawn for AI agent instances" \
|
||||
--output json 2>&1 || echo "")
|
||||
|
||||
sg_id=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('SecurityGroupId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$sg_id" ]]; then
|
||||
log_error "Failed to create security group: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Add SSH rule
|
||||
log_step "Adding SSH rule to security group..."
|
||||
aliyun ecs AuthorizeSecurityGroup \
|
||||
--SecurityGroupId "$sg_id" \
|
||||
--IpProtocol tcp \
|
||||
--PortRange "22/22" \
|
||||
--SourceCidrIp "0.0.0.0/0" \
|
||||
--output json >/dev/null 2>&1
|
||||
|
||||
echo "$sg_id"
|
||||
}
|
||||
|
||||
# Create Alibaba Cloud ECS instance
|
||||
# Sets: ALIYUN_INSTANCE_ID, ALIYUN_INSTANCE_IP
|
||||
create_server() {
|
||||
local name="$1"
|
||||
local region="${ALIYUN_REGION:-cn-hangzhou}"
|
||||
local instance_type="${ALIYUN_INSTANCE_TYPE:-ecs.t5-lc1m2.small}"
|
||||
local image_id="${ALIYUN_IMAGE_ID:-ubuntu_24_04_x64_20G_alibase_20240812.vhd}"
|
||||
|
||||
# Validate inputs to prevent injection
|
||||
validate_resource_name "$instance_type" || { log_error "Invalid ALIYUN_INSTANCE_TYPE"; return 1; }
|
||||
validate_region_name "$region" || { log_error "Invalid ALIYUN_REGION"; return 1; }
|
||||
|
||||
# Get or create VPC
|
||||
log_step "Checking for VPC in region $region..."
|
||||
local vpc_response
|
||||
vpc_response=$(aliyun ecs DescribeVpcs --output json 2>/dev/null || echo "{}")
|
||||
local vpc_id
|
||||
vpc_id=$(echo "$vpc_response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
vpcs = data.get('Vpcs', {}).get('Vpc', [])
|
||||
if vpcs:
|
||||
print(vpcs[0].get('VpcId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$vpc_id" ]]; then
|
||||
log_step "Creating VPC..."
|
||||
local create_vpc_response
|
||||
create_vpc_response=$(aliyun ecs CreateVpc \
|
||||
--CidrBlock "172.16.0.0/12" \
|
||||
--VpcName "spawn-vpc" \
|
||||
--output json 2>&1 || echo "")
|
||||
vpc_id=$(echo "$create_vpc_response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('VpcId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$vpc_id" ]]; then
|
||||
log_error "Failed to create VPC: $create_vpc_response"
|
||||
return 1
|
||||
fi
|
||||
sleep 3 # Wait for VPC to be ready
|
||||
fi
|
||||
log_info "Using VPC: $vpc_id"
|
||||
|
||||
# Get or create vSwitch
|
||||
log_step "Checking for vSwitch..."
|
||||
local vswitch_response
|
||||
vswitch_response=$(aliyun ecs DescribeVSwitches --VpcId "$vpc_id" --output json 2>/dev/null || echo "{}")
|
||||
local vswitch_id
|
||||
vswitch_id=$(echo "$vswitch_response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
vswitches = data.get('VSwitches', {}).get('VSwitch', [])
|
||||
if vswitches:
|
||||
print(vswitches[0].get('VSwitchId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$vswitch_id" ]]; then
|
||||
# Get first availability zone
|
||||
local zone_id
|
||||
zone_id=$(aliyun ecs DescribeZones --output json 2>/dev/null | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
zones = data.get('Zones', {}).get('Zone', [])
|
||||
if zones:
|
||||
print(zones[0].get('ZoneId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$zone_id" ]]; then
|
||||
log_error "Failed to get availability zone"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_step "Creating vSwitch in zone $zone_id..."
|
||||
local create_vs_response
|
||||
create_vs_response=$(aliyun ecs CreateVSwitch \
|
||||
--VpcId "$vpc_id" \
|
||||
--ZoneId "$zone_id" \
|
||||
--CidrBlock "172.16.0.0/24" \
|
||||
--VSwitchName "spawn-vswitch" \
|
||||
--output json 2>&1 || echo "")
|
||||
vswitch_id=$(echo "$create_vs_response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
print(data.get('VSwitchId', ''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$vswitch_id" ]]; then
|
||||
log_error "Failed to create vSwitch: $create_vs_response"
|
||||
return 1
|
||||
fi
|
||||
sleep 3 # Wait for vSwitch to be ready
|
||||
fi
|
||||
log_info "Using vSwitch: $vswitch_id"
|
||||
|
||||
# Get or create security group
|
||||
local security_group_id
|
||||
security_group_id=$(_ensure_security_group "$vpc_id")
|
||||
if [[ -z "$security_group_id" ]]; then
|
||||
log_error "Failed to get or create security group"
|
||||
return 1
|
||||
fi
|
||||
log_info "Using security group: $security_group_id"
|
||||
|
||||
# Get SSH key name
|
||||
local key_name="spawn-$(whoami)-$(hostname)"
|
||||
|
||||
# Prepare userdata for cloud-init
|
||||
local userdata
|
||||
userdata=$(get_cloud_init_userdata)
|
||||
local userdata_b64
|
||||
userdata_b64=$(echo "$userdata" | base64 -w0 2>/dev/null || echo "$userdata" | base64)
|
||||
|
||||
log_step "Creating Alibaba Cloud ECS instance '$name'..."
|
||||
log_step " Instance type: $instance_type"
|
||||
log_step " Region: $region"
|
||||
log_step " Image: $image_id"
|
||||
|
||||
local create_response
|
||||
create_response=$(aliyun ecs RunInstances \
|
||||
--InstanceName "$name" \
|
||||
--InstanceType "$instance_type" \
|
||||
--ImageId "$image_id" \
|
||||
--SecurityGroupId "$security_group_id" \
|
||||
--VSwitchId "$vswitch_id" \
|
||||
--KeyPairName "$key_name" \
|
||||
--InternetMaxBandwidthOut 1 \
|
||||
--UserData "$userdata_b64" \
|
||||
--SystemDisk.Category cloud_efficiency \
|
||||
--SystemDisk.Size 20 \
|
||||
--output json 2>&1 || echo "")
|
||||
|
||||
local instance_id
|
||||
instance_id=$(echo "$create_response" | python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.loads(sys.stdin.read())
|
||||
instances = data.get('InstanceIdSets', {}).get('InstanceIdSet', [])
|
||||
if instances:
|
||||
print(instances[0])
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$instance_id" ]]; then
|
||||
log_error "Failed to create instance"
|
||||
log_error "API Error: $create_response"
|
||||
log_error ""
|
||||
log_error "Common causes:"
|
||||
log_error " - Insufficient quota in region $region"
|
||||
log_error " - Invalid instance type: $instance_type"
|
||||
log_error " - Invalid image ID: $image_id"
|
||||
log_error " - Network configuration issue (VPC/vSwitch)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
ALIYUN_INSTANCE_ID="$instance_id"
|
||||
export ALIYUN_INSTANCE_ID
|
||||
log_info "Instance created: $instance_id"
|
||||
|
||||
# Start the instance
|
||||
log_step "Starting instance..."
|
||||
aliyun ecs StartInstance --InstanceId "$instance_id" --output json >/dev/null 2>&1
|
||||
|
||||
# Wait for instance to be ready
|
||||
_wait_for_aliyun_instance "$instance_id" 60
|
||||
}
|
||||
|
||||
# Upload file to instance
|
||||
# Usage: upload_file SERVER_IP LOCAL_PATH REMOTE_PATH
|
||||
upload_file() {
|
||||
local server_ip="$1"
|
||||
local local_path="$2"
|
||||
local remote_path="$3"
|
||||
# shellcheck disable=SC2086
|
||||
scp $SSH_OPTS "$local_path" "root@${server_ip}:${remote_path}"
|
||||
}
|
||||
|
||||
# Run command on instance
|
||||
# Usage: run_server SERVER_IP COMMAND
|
||||
run_server() {
|
||||
local server_ip="$1"
|
||||
shift
|
||||
# shellcheck disable=SC2086
|
||||
ssh $SSH_OPTS "root@${server_ip}" "$@"
|
||||
}
|
||||
|
||||
# Start interactive session
|
||||
# Usage: interactive_session SERVER_IP [COMMAND]
|
||||
interactive_session() {
|
||||
local server_ip="$1"
|
||||
local command="${2:-bash}"
|
||||
# shellcheck disable=SC2086
|
||||
ssh $SSH_OPTS -t "root@${server_ip}" "$command"
|
||||
}
|
||||
|
||||
verify_server_connectivity() { ssh_verify_connectivity "$@"; }
|
||||
|
|
@ -810,6 +810,21 @@
|
|||
"location": "fi",
|
||||
"image": "ubuntu2404"
|
||||
}
|
||||
},
|
||||
"alibabacloud": {
|
||||
"name": "Alibaba Cloud",
|
||||
"description": "Alibaba Cloud ECS instances via CLI",
|
||||
"url": "https://www.alibabacloud.com/",
|
||||
"type": "cli",
|
||||
"auth": "ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET",
|
||||
"provision_method": "aliyun ecs RunInstances with UserData",
|
||||
"exec_method": "ssh root@IP",
|
||||
"interactive_method": "ssh -t root@IP",
|
||||
"defaults": {
|
||||
"instance_type": "ecs.t5-lc1m2.small",
|
||||
"region": "cn-hangzhou",
|
||||
"image_id": "ubuntu_24_04_x64_20G_alibase_20240812.vhd"
|
||||
}
|
||||
}
|
||||
},
|
||||
"matrix": {
|
||||
|
|
@ -1367,6 +1382,21 @@
|
|||
"webdock/opencode": "missing",
|
||||
"webdock/plandex": "missing",
|
||||
"webdock/kilocode": "missing",
|
||||
"webdock/continue": "missing"
|
||||
"webdock/continue": "missing",
|
||||
"alibabacloud/claude": "implemented",
|
||||
"alibabacloud/codex": "implemented",
|
||||
"alibabacloud/gemini": "implemented",
|
||||
"alibabacloud/aider": "missing",
|
||||
"alibabacloud/amazonq": "missing",
|
||||
"alibabacloud/cline": "missing",
|
||||
"alibabacloud/continue": "missing",
|
||||
"alibabacloud/goose": "missing",
|
||||
"alibabacloud/gptme": "missing",
|
||||
"alibabacloud/interpreter": "missing",
|
||||
"alibabacloud/kilocode": "missing",
|
||||
"alibabacloud/nanoclaw": "missing",
|
||||
"alibabacloud/openclaw": "missing",
|
||||
"alibabacloud/opencode": "missing",
|
||||
"alibabacloud/plandex": "missing"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue