From e5f6ab1cffcf2cccc5f271ea6e7a02251e0a312b Mon Sep 17 00:00:00 2001 From: A <6723574+louisgv@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:40:03 +0000 Subject: [PATCH] feat: Add Alibaba Cloud provider support 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: Claude Sonnet 4.5 --- alibabacloud/README.md | 94 +++++++ alibabacloud/claude.sh | 58 ++++ alibabacloud/codex.sh | 48 ++++ alibabacloud/gemini.sh | 52 ++++ alibabacloud/lib/common.sh | 534 +++++++++++++++++++++++++++++++++++++ manifest.json | 32 ++- 6 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 alibabacloud/README.md create mode 100644 alibabacloud/claude.sh create mode 100644 alibabacloud/codex.sh create mode 100644 alibabacloud/gemini.sh create mode 100644 alibabacloud/lib/common.sh diff --git a/alibabacloud/README.md b/alibabacloud/README.md new file mode 100644 index 000000000..1b32db569 --- /dev/null +++ b/alibabacloud/README.md @@ -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 diff --git a/alibabacloud/claude.sh b/alibabacloud/claude.sh new file mode 100644 index 000000000..e13266a4b --- /dev/null +++ b/alibabacloud/claude.sh @@ -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" diff --git a/alibabacloud/codex.sh b/alibabacloud/codex.sh new file mode 100644 index 000000000..75946c2d9 --- /dev/null +++ b/alibabacloud/codex.sh @@ -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" diff --git a/alibabacloud/gemini.sh b/alibabacloud/gemini.sh new file mode 100644 index 000000000..23bf19ebd --- /dev/null +++ b/alibabacloud/gemini.sh @@ -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" diff --git a/alibabacloud/lib/common.sh b/alibabacloud/lib/common.sh new file mode 100644 index 000000000..3eb79d5ef --- /dev/null +++ b/alibabacloud/lib/common.sh @@ -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 "$@"; } diff --git a/manifest.json b/manifest.json index 1e11b333b..6107236a2 100644 --- a/manifest.json +++ b/manifest.json @@ -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" } }