diff --git a/.claude/skills/setup-agent-team/discovery.sh b/.claude/skills/setup-agent-team/discovery.sh index adede4f61..205a851a7 100755 --- a/.claude/skills/setup-agent-team/discovery.sh +++ b/.claude/skills/setup-agent-team/discovery.sh @@ -201,12 +201,25 @@ For each candidate, verify: - Has affordable CPU instances (pay-per-hour or pay-per-second pricing) - Is actually available (not waitlisted/invite-only) -**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update: -1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording -2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133) -3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format - -Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely. +**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files: + +**`test/record.sh`** (fixture recording): +1. Add the cloud to `ALL_RECORDABLE_CLOUDS` +2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints +3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var +4. Add a case in `call_api()` to dispatch to the cloud's API function +5. Add a case in `has_api_error()` for the cloud's error response format +6. Add a `_live_{cloud}()` function for create/delete server fixture recording + +**`test/mock.sh`** (mock test infrastructure — ALL of these): +1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings. +2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`) +3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint) +4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.) +5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()` +6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()` + +Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely. ### Agent Scout (spawn 1, only if justified) Research new AI agents, BUT only add one if there's REAL community demand: @@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds. Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest. -MANDATORY: When adding a new cloud, also add it to the test infrastructure: +MANDATORY: When adding a new cloud, also update the test infrastructure: 1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function -2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block) +2. test/mock.sh — add ALL of these: + - _strip_api_base(): URL-stripping case for the cloud's API base URL + - _validate_body(): body validation case with server creation endpoint + required fields + - assert_cloud_api_calls(): expected API calls (SSH key fetch + server create) + - setup_env_for_cloud(): test env vars (API token, server name, plan, region) Only add a new AGENT if you find one with real community buzz: - 1000+ GitHub stars diff --git a/.claude/skills/setup-agent-team/qa-cycle.sh b/.claude/skills/setup-agent-team/qa-cycle.sh index 1e898ea03..c54638dcf 100644 --- a/.claude/skills/setup-agent-team/qa-cycle.sh +++ b/.claude/skills/setup-agent-team/qa-cycle.sh @@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do fi done -# Close stale qa PRs (open > 2 hours) +# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \ --jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true for pr_num in $STALE_PRS; do if [[ -n "$pr_num" ]]; then - PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN" - if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then - log "Merging stale QA PR #${pr_num}..." - gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true - else - log "Closing unmergeable stale QA PR #${pr_num}..." - gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle. + log "Closing stale QA PR #${pr_num}..." + gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle. -- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true - fi fi done @@ -228,8 +222,14 @@ check_timeout || exit 0 log "=== Phase 1: Record fixtures ===" RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt" +RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json" rm -f "${RECORD_OUTPUT}" +# Initialize failure tracker if missing +if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then + printf '{}' > "${RECORD_FAILURES_FILE}" +fi + RECORD_EXIT=0 bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$? @@ -408,6 +408,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru fi fi +# --- Track consecutive Phase 1 failures per cloud --- +# Parse the final recording output to determine which clouds failed +FINAL_RECORD_FAILED="" +FINAL_RECORD_SUCCEEDED="" +if [[ -f "${RECORD_OUTPUT}" ]]; then + _current_cloud="" + _cloud_had_error="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + # Save previous cloud result + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi + _current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + _cloud_had_error="" + ;; + *"fail "*) + _cloud_had_error="true" + ;; + *"done "*) + # Cloud section ended + ;; + esac + done < "${RECORD_OUTPUT}" + # Handle last cloud + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi +fi +FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //') +FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //') + +# Update the persistent failure tracker +if [[ -f "${RECORD_FAILURES_FILE}" ]]; then + python3 -c " +import json, sys + +tracker_path = sys.argv[1] +failed = sys.argv[2].split() if sys.argv[2] else [] +succeeded = sys.argv[3].split() if sys.argv[3] else [] + +try: + with open(tracker_path) as f: + tracker = json.load(f) +except (json.JSONDecodeError, FileNotFoundError): + tracker = {} + +# Increment consecutive failures for failed clouds +for cloud in failed: + tracker[cloud] = tracker.get(cloud, 0) + 1 + +# Reset counter for clouds that succeeded +for cloud in succeeded: + tracker[cloud] = 0 + +with open(tracker_path, 'w') as f: + json.dump(tracker, f, indent=2, sort_keys=True) + +# Output clouds that hit the threshold (3+ consecutive failures) +escalate = [c for c, count in tracker.items() if count >= 3] +if escalate: + print(' '.join(escalate)) +" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true + + ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true) + rm -f /tmp/spawn-qa-escalate.txt + + if [[ -n "${ESCALATE_CLOUDS}" ]]; then + for cloud in ${ESCALATE_CLOUDS}; do + consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+") + log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles" + + # Check if an issue already exists for this cloud + existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \ + --search "fixture recording failing ${cloud}" \ + --json number --jq '.[0].number' 2>/dev/null) || existing_issue="" + + if [[ -z "${existing_issue}" ]]; then + gh issue create --repo OpenRouterTeam/spawn \ + --title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \ + --body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \ + --label "bug" \ + 2>&1 | tee -a "${LOG_FILE}" || true + log "Phase 1: Created GitHub issue for ${cloud} persistent failure" + else + log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate" + fi + done + fi +fi + rm -f "${RECORD_OUTPUT}" check_timeout || exit 0 @@ -448,6 +549,15 @@ else FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true) fi + # Capture full mock test output per-cloud for richer agent context + MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output" + rm -rf "${MOCK_OUTPUT_DIR}" + mkdir -p "${MOCK_OUTPUT_DIR}" + for cloud in $FAILED_CLOUDS; do + log "Phase 3: Capturing full mock test output for ${cloud}..." + bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true + done + AGENT_PIDS="" for cloud in $FAILED_CLOUDS; do check_timeout || break @@ -456,25 +566,21 @@ else cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true) failing_scripts="" failing_agents="" - error_context="" for combo in $cloud_failures; do agent=$(printf '%s' "$combo" | cut -d/ -f2) script_path="${cloud}/${agent}.sh" failing_scripts="${failing_scripts} ${script_path}" failing_agents="${failing_agents} ${agent}" - if [[ -f "${LOG_FILE}" ]]; then - ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true) - if [[ -n "$ctx" ]]; then - error_context="${error_context} ---- ${script_path} --- -${ctx} -" - fi - fi done failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //') failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //') + # Use full mock test output as error context (not just 10 lines) + error_context="" + if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then + error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log") + fi + fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ') log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}" @@ -582,6 +688,9 @@ FIXEOF done git worktree prune 2>/dev/null || true + # Clean up per-cloud mock output + rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true + log "Phase 3: Fix agents complete" fi diff --git a/.claude/skills/setup-trigger-service/qa-cycle.sh b/.claude/skills/setup-trigger-service/qa-cycle.sh new file mode 120000 index 000000000..0c41e86a9 --- /dev/null +++ b/.claude/skills/setup-trigger-service/qa-cycle.sh @@ -0,0 +1 @@ +../setup-agent-team/qa-cycle.sh \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..5051e9166 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +name: Test + +on: + pull_request: + branches: [main] + +concurrency: + group: test-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + mock-tests: + name: Mock Tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Run mock tests + run: | + bash test/mock.sh 2>&1 | tee /tmp/mock-output.log + - name: Post summary + if: always() + run: | + echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 678dbe0ad..0ff056993 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ .docs/ __pycache__/ .claude/skills/*/start-*.sh +.claude/settings.local.json cli/cli.js # Sensitive files — never commit secrets or private keys @@ -15,3 +16,5 @@ id_rsa id_ed25519 credentials.json service-account.json +.claude/settings.json +.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index c1804f03c..da3786daf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it: - Run tests with `bun test` - Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"` +### Mock Test Infrastructure (MANDATORY for new clouds) + +When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`: + +| Function | What to add | +|---|---| +| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) | +| `_validate_body()` | Server creation endpoint + required POST fields | +| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) | +| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) | + +And in `test/record.sh`: + +| Function | What to add | +|---|---| +| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list | +| `get_endpoints()` | GET-only API endpoints for fixture recording | +| `get_auth_env_var()` | Auth env var name mapping | +| `call_api()` | API function dispatcher | +| `has_api_error()` | Error response detection for the cloud's API | + +Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds. + ## CLI Version Management **CRITICAL: Bump the version on every CLI change!** diff --git a/genesiscloud/claude.sh b/genesiscloud/claude.sh index dd5ebbc07..3a480d411 100644 --- a/genesiscloud/claude.sh +++ b/genesiscloud/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${GENESIS_SERVER_IP}" wait_for_cloud_init "${GENESIS_SERVER_IP}" 60 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/runpod/claude.sh b/runpod/claude.sh new file mode 100644 index 000000000..66ee62dd6 --- /dev/null +++ b/runpod/claude.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=runpod/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/runpod/lib/common.sh)" +fi + +log_info "Claude Code on RunPod" +echo "" + +# 1. Ensure RunPod API key is configured +ensure_runpod_token + +# 2. Ensure SSH key exists (user must add to RunPod console) +ensure_ssh_key + +# 3. Get pod name and create pod +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for SSH connectivity and install base tools +verify_server_connectivity +install_base_tools + +# 5. Verify Claude Code is installed (fallback to manual install) +log_step "Verifying Claude Code installation..." +if ! run_server "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then + log_step "Claude Code not found, installing manually..." + run_server "${RUNPOD_POD_ID}" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +# 6. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 7. Inject environment variables +log_step "Setting up environment variables..." +inject_env_vars_ssh "${RUNPOD_POD_ID}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=" \ + "CLAUDE_CODE_SKIP_ONBOARDING=1" \ + "CLAUDE_CODE_ENABLE_TELEMETRY=0" + +# 8. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${RUNPOD_POD_ID}" \ + "run_server ${RUNPOD_POD_ID}" + +echo "" +log_info "RunPod pod setup completed successfully!" +log_info "Pod: ${SERVER_NAME} (ID: ${RUNPOD_POD_ID})" +echo "" + +# 9. Start Claude Code interactively +log_step "Starting Claude Code..." +sleep 1 +clear +interactive_session "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/test/fixtures/civo/_metadata.json b/test/fixtures/civo/_metadata.json index 5a6bd8299..c36fd95c4 100644 --- a/test/fixtures/civo/_metadata.json +++ b/test/fixtures/civo/_metadata.json @@ -1,9 +1,13 @@ { "cloud": "civo", - "recorded_at": "2026-02-11T02:07:07Z", + "recorded_at": "2026-02-13T06:37:22Z", "fixtures": { - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T02:07:02Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T02:07:03Z"}, - "ssh_keys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-11T02:07:04Z"} + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:10Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T06:37:11Z"}, + "sshkeys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-13T06:37:12Z"}, + "networks": {"endpoint": "/networks", "recorded_at": "2026-02-13T06:37:12Z"}, + "disk_images": {"endpoint": "/disk_images", "recorded_at": "2026-02-13T06:37:13Z"}, + "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-13T06:37:16Z"}, + "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:22Z"} } } diff --git a/test/fixtures/civo/create_server.json b/test/fixtures/civo/create_server.json index 2d769384e..2d9511cd2 100644 --- a/test/fixtures/civo/create_server.json +++ b/test/fixtures/civo/create_server.json @@ -1,18 +1,18 @@ { "account_id": "d2449447-ac9e-4a4e-90a8-bb1aae89cb9a", "attached_volumes": [], - "civostatsd_token": "22672e8b-d4f7-4970-bfec-e654593b700d", + "civostatsd_token": "d6da608c-b760-4b0a-9b45-c1e6a25f407f", "cpu_cores": 1, - "created_at": "2026-02-11T03:01:19Z", + "created_at": "2026-02-13T06:37:16Z", "disk_gb": 25, - "firewall_id": "a12fd86a-a517-46de-90e7-3c9428b164ca", - "hostname": "spawn-record-1770778876", - "id": "d17c67d9-7636-471f-9219-654573e09e56", - "initial_password": "25TA6WIKKr", + "firewall_id": "11a86926-bfbb-4744-aaed-979a3ae85086", + "hostname": "spawn-record-1770964633", + "id": "7b31ce22-7ac5-4dad-a9e7-38fd76d82849", + "initial_password": "RSSnLdac9e", "initial_user": "root", - "name": "spawn-record-1770778876-daaf-a54d25", - "namespace_id": "cust-default-d2449447-75057246b722", - "network_id": "1773d9a7-fec1-4478-b45a-665e39528c79", + "name": "spawn-record-1770964633-2d22-031d88", + "namespace_id": "cust-default-d2449447-75059009d0bd", + "network_id": "aee434ed-e2b8-4e5d-8eb2-97ef3d3062b3", "notes": "", "placement_rule": { "affinity_rules": null, @@ -21,11 +21,11 @@ "ram_mb": 1024, "reverse_dns": "", "script": "", - "size": "g4s.xsmall", - "source_id": "ubuntu-noble", + "size": "g3.xsmall", + "source_id": "1.35.0-k3s1", "source_type": "diskimage", "status": "BUILDING", "tags": [], - "template_id": "d2c2c728-512d-4087-906a-8486fcf4a1c7", + "template_id": "7864fa48-5ce3-4758-a1c1-3b2eeba8c80d", "volume_type": "standard" } diff --git a/test/fixtures/civo/delete_server.json b/test/fixtures/civo/delete_server.json new file mode 100644 index 000000000..f36db5a8e --- /dev/null +++ b/test/fixtures/civo/delete_server.json @@ -0,0 +1,4 @@ +{ + "id": "7b31ce22-7ac5-4dad-a9e7-38fd76d82849", + "result": "success" +} diff --git a/test/fixtures/civo/disk_images.json b/test/fixtures/civo/disk_images.json new file mode 100644 index 000000000..08af03882 --- /dev/null +++ b/test/fixtures/civo/disk_images.json @@ -0,0 +1,516 @@ +[ + { + "created_at": "2026-01-26T21:36:35Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": true, + "id": "7864fa48-5ce3-4758-a1c1-3b2eeba8c80d", + "initial_user": "root", + "label": "1.35.0-k3s1", + "name": "1.35.0-k3s1", + "state": "available", + "version": "1.35.0-k3s1" + }, + { + "created_at": "2026-01-13T19:07:34Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "7617e6db-0746-4765-88fd-68d9b870f916", + "initial_user": "root", + "label": "1.34.2-k3s1", + "name": "1.34.2-k3s1", + "state": "available", + "version": "1.34.2-k3s1" + }, + { + "created_at": "2025-12-09T07:38:31Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "49603b06-7e3c-460b-9f74-68f9b4620d57", + "initial_user": "root", + "label": "1.34.1-k3s1", + "name": "1.34.1-k3s1", + "state": "available", + "version": "1.34.1-k3s1" + }, + { + "created_at": "2026-01-13T19:07:34Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "1135e5db-fee9-4bd0-953c-42cca32c4501", + "initial_user": "root", + "label": "1.33.6-k3s1", + "name": "1.33.6-k3s1", + "state": "available", + "version": "1.33.6-k3s1" + }, + { + "created_at": "2025-11-11T08:02:43Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3fcf95fd-852c-4205-a18d-e60dd38f3899", + "initial_user": "root", + "label": "1.33.5-k3s1", + "name": "1.33.5-k3s1", + "state": "available", + "version": "1.33.5-k3s1" + }, + { + "created_at": "2025-09-22T03:12:45Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "308d0cac-afdb-4551-b30f-8ccf53439b79", + "initial_user": "root", + "label": "1.32.5-k3s1", + "name": "1.32.5-k3s1", + "state": "available", + "version": "1.32.5-k3s1" + }, + { + "created_at": "2025-05-26T16:40:14Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3a8b3616-635a-467c-9890-77932c706982", + "initial_user": "root", + "label": "1.31.6-k3s1", + "name": "1.31.6-k3s1", + "state": "available", + "version": "1.31.6-k3s1" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3a12c8dc-d36e-43b2-97d2-e4e26579a776", + "initial_user": "root", + "label": "1.30.5-k3s1", + "name": "1.30.5-k3s1", + "state": "available", + "version": "1.30.5-k3s1" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "bed09b42-e286-495e-842a-3c73be307973", + "initial_user": "root", + "label": "1.29.8-k3s1", + "name": "1.29.8-k3s1", + "state": "available", + "version": "1.29.8-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "929df383-695e-4a18-a69e-d98fd819814c", + "initial_user": "root", + "label": "1.29.2-k3s1", + "name": "1.29.2-k3s1", + "state": "available", + "version": "1.29.2-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "898a54bf-4201-4e6f-9d86-d1f86d80b9b6", + "initial_user": "root", + "label": "1.28.7-k3s1", + "name": "1.28.7-k3s1", + "state": "available", + "version": "1.28.7-k3s1" + }, + { + "created_at": "2023-10-03T14:14:38Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "4009800d-d5fe-48bf-b916-19b389e9e72f", + "initial_user": "root", + "label": "1.28.2-k3s1", + "name": "1.28.2-k3s1", + "state": "available", + "version": "1.28.2-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "20fe1539-2ecd-4b48-8158-ab795aac638b", + "initial_user": "root", + "label": "1.27.11-k3s1", + "name": "1.27.11-k3s1", + "state": "available", + "version": "1.27.11-k3s1" + }, + { + "created_at": "2023-05-30T16:45:42Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "267033f4-5ca2-4a59-8d33-6e4d2f890157", + "initial_user": "root", + "label": "1.27.1-k3s1", + "name": "1.27.1-k3s1", + "state": "available", + "version": "1.27.1-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "62357d07-8777-4193-9607-d20ec8fed56c", + "initial_user": "root", + "label": "1.26.14-k3s1", + "name": "1.26.14-k3s1", + "state": "available", + "version": "1.26.14-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "1fc7ae3f-f73d-4812-8088-2935a7be974c", + "initial_user": "root", + "label": "1.26.4-k3s1", + "name": "1.26.4-k3s1", + "state": "available", + "version": "1.26.4-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "5a3566f9-e06d-4c15-961b-ced7f3525b75", + "initial_user": "root", + "label": "1.25.9-k3s1", + "name": "1.25.9-k3s1", + "state": "available", + "version": "1.25.9-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "bb142004-b730-4049-bc37-29b97285da61", + "initial_user": "root", + "label": "1.25.0-k3s1", + "name": "1.25.0-k3s1", + "state": "available", + "version": "1.25.0-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "6674b2d3-f10e-4ce3-8d94-0933bc4286d7", + "initial_user": "root", + "label": "1.24.13-k3s1", + "name": "1.24.13-k3s1", + "state": "available", + "version": "1.24.13-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "662c91fe-8c0a-4a35-b24c-64bd50b3864e", + "initial_user": "root", + "label": "1.24.4-k3s1", + "name": "1.24.4-k3s1", + "state": "available", + "version": "1.24.4-k3s1" + }, + { + "created_at": "2022-10-27T11:51:30Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "3770ea3c-81b3-484e-8bce-9f7a01abbf0b", + "initial_user": "root", + "label": "1.23.10-k3s1", + "name": "1.23.10-k3s1", + "state": "available", + "version": "1.23.10-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "84962d39-a07c-4216-b502-4570270dfa2f", + "initial_user": "root", + "label": "1.23.6-k3s1", + "name": "1.23.6-k3s1", + "state": "available", + "version": "1.23.6-k3s1" + }, + { + "created_at": "2022-07-11T10:33:17Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "da4037c7-a1e1-48e0-a74a-e7adec30f97f", + "initial_user": "root", + "label": "1.22.11-k3s1", + "name": "1.22.11-k3s1", + "state": "available", + "version": "1.22.11-k3s1" + }, + { + "created_at": "2022-01-27T11:06:34Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "6b35b340-6562-4f2f-a7be-a40966694453", + "initial_user": "root", + "label": "1.22.2-k3s1", + "name": "1.22.2-k3s1", + "state": "available", + "version": "1.22.2-k3s1" + }, + { + "created_at": "2022-01-06T16:01:33Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "a66ca672-6d87-4e78-95ca-796c054145e9", + "initial_user": "root", + "label": "1.21.2-k3s1", + "name": "1.21.2-k3s1", + "state": "available", + "version": "1.21.2-k3s1" + }, + { + "created_at": "2025-12-10T12:51:06Z", + "description": "debian", + "distribution": "debian", + "distribution_default": true, + "id": "ea377a00-f0a7-4d1f-8ee4-f79cf124d299", + "initial_user": "admin", + "label": "trixie", + "name": "debian-13", + "state": "available", + "version": "13" + }, + { + "created_at": "2025-03-05T16:05:34Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "727b9409-866c-460e-90c6-31cbf105c64a", + "initial_user": "admin", + "label": "bookworm", + "name": "debian-12", + "state": "available", + "version": "12" + }, + { + "created_at": "2022-07-21T14:12:03Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "f398bcd6-c27e-4d56-a6df-d5aa63bb4083", + "initial_user": "admin", + "label": "bullseye", + "name": "debian-11", + "state": "available", + "version": "11" + }, + { + "created_at": "2021-03-17T16:52:12Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "4204229c-510c-4ba4-ab07-522e2aaa2cf8", + "initial_user": "admin", + "label": "buster", + "name": "debian-10", + "state": "available", + "version": "10" + }, + { + "created_at": "2021-03-17T16:52:14Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "cddce6c9-f84e-4e4f-ab8d-7a33cab85158", + "initial_user": "admin", + "label": "stretch", + "name": "debian-9", + "state": "available", + "version": "9" + }, + { + "created_at": "2025-07-30T07:53:34Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": true, + "id": "34b21eb5-80ee-4c5f-86e8-70f12a2485fc", + "initial_user": "rocky", + "label": "10", + "name": "rocky-10", + "state": "available", + "version": "10" + }, + { + "created_at": "2023-02-17T09:34:38Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": false, + "id": "4a7b536b-c893-41cf-b5e1-19f47933267d", + "initial_user": "rocky", + "label": "9.1", + "name": "rocky-9-1", + "state": "available", + "version": "9.1" + }, + { + "created_at": "2025-07-30T07:53:34Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": false, + "id": "54480094-4607-4185-95cc-d30253fce9eb", + "initial_user": "rocky", + "label": "9", + "name": "rocky-9", + "state": "available", + "version": "9" + }, + { + "created_at": "2023-09-15T08:56:58Z", + "description": "talos", + "distribution": "talos", + "distribution_default": true, + "id": "64053a48-b20d-4345-860c-cfa0a688816d", + "label": "v1.5.0", + "name": "talos-v1.5.0", + "state": "available", + "version": "1.27.0" + }, + { + "created_at": "2023-02-02T06:30:42Z", + "description": "talos", + "distribution": "talos", + "distribution_default": false, + "id": "7bb21130-840a-484f-a049-dcbbb4fd87cf", + "label": "v1.2.8", + "name": "talos-v1.2.8", + "state": "available", + "version": "1.25.5" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": true, + "id": "9fb7b840-6cde-448a-b570-6418430c6ac8", + "initial_user": "ubuntu", + "label": "noble", + "name": "ubuntu-noble", + "state": "available", + "version": "24.04" + }, + { + "created_at": "2022-05-18T21:29:33Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "9f953761-8b20-4623-8dff-e0c845201966", + "initial_user": "ubuntu", + "label": "jammy", + "name": "ubuntu-jammy", + "state": "available", + "version": "22.04" + }, + { + "created_at": "2021-03-17T16:52:13Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "8eb48e20-e5db-49fe-9cdf-cc8f381c61c6", + "initial_user": "ubuntu", + "label": "focal", + "name": "ubuntu-focal", + "state": "available", + "version": "20.04" + }, + { + "created_at": "2025-07-30T07:53:56Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "14f97f88-3963-462a-b517-e3697e263147", + "initial_user": "ubuntu", + "label": "cuda12-9", + "name": "ubuntu-cuda12-9", + "state": "available", + "version": "24.04-cuda12-9" + }, + { + "created_at": "2025-07-30T07:53:56Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "a6dd00aa-96fe-4b00-b6bd-f5c42dab33a9", + "initial_user": "ubuntu", + "label": "cuda12-8", + "name": "ubuntu-cuda12-8", + "state": "available", + "version": "24.04-cuda12-8" + }, + { + "created_at": "2025-05-26T16:40:14Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "c7ce39db-2054-4276-8b95-3b1d35e4ef01", + "initial_user": "ubuntu", + "label": "cuda12-6", + "name": "ubuntu-cuda12-6", + "state": "available", + "version": "24.04-cuda12-6" + }, + { + "created_at": "2024-11-21T09:15:26Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "a9cf6632-d127-4516-9ce4-e2998e1bb2eb", + "initial_user": "ubuntu", + "label": "cuda12-2", + "name": "ubuntu-cuda12-2", + "state": "available", + "version": "22.04-cuda12-2" + }, + { + "created_at": "2024-11-21T09:15:26Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "ac2aa741-b547-4b77-b3e8-fb40b73c6266", + "initial_user": "ubuntu", + "label": "cuda11-8", + "name": "ubuntu-cuda11-8", + "state": "available", + "version": "22.04-cuda11-8" + } +] diff --git a/test/fixtures/civo/networks.json b/test/fixtures/civo/networks.json new file mode 100644 index 000000000..81685b43d --- /dev/null +++ b/test/fixtures/civo/networks.json @@ -0,0 +1,17 @@ +[ + { + "cidr": "192.168.1.0/24", + "default": true, + "free_ip_count": 254, + "gateway_ipv4": "192.168.1.1", + "id": "aee434ed-e2b8-4e5d-8eb2-97ef3d3062b3", + "ipv4_enabled": true, + "label": "default", + "name": "cust-default-d2449447-75059009d0bd", + "nameservers_v4": [ + "8.8.8.8", + "1.1.1.1" + ], + "status": "Active" + } +] diff --git a/test/fixtures/civo/sshkeys.json b/test/fixtures/civo/sshkeys.json new file mode 100644 index 000000000..95332b180 --- /dev/null +++ b/test/fixtures/civo/sshkeys.json @@ -0,0 +1,9 @@ +[ + { + "created_at": "2026-02-12T00:26:45Z", + "fingerprint": "SHA256:Ko5JS3tB2xRw5C+jWzdDsRoscXqt1fWV1w0VPThbjrA", + "id": "feb0a45d-7c21-44fa-adc2-c24a88b2f5d5", + "name": "spawn-MacBookPro-1770856003", + "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHmcVdzydp72a/B69nmENZvCvjuk7xGpKdi5CvhkmNsv ahmed" + } +] diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index f6bbc7728..9b546f315 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-11T03:07:38Z", + "recorded_at": "2026-02-10T21:55:29Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-11T03:07:30Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-11T03:07:30Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-11T03:07:31Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-11T03:07:31Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T03:07:32Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-11T03:07:34Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-11T03:07:38Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} } } diff --git a/test/fixtures/digitalocean/create_server.json b/test/fixtures/digitalocean/create_server.json index ede0fc5b1..5fd324cf4 100644 --- a/test/fixtures/digitalocean/create_server.json +++ b/test/fixtures/digitalocean/create_server.json @@ -1,7 +1,7 @@ { "droplet": { "backup_ids": [], - "created_at": "2026-02-11T03:07:33Z", + "created_at": "2026-02-13T06:37:13Z", "disk": 10, "disk_info": [ { @@ -15,7 +15,7 @@ "features": [ "droplet_agent" ], - "id": 550842125, + "id": 551384349, "image": { "created_at": "2025-08-08T15:11:27Z", "description": "Ubuntu 24.04 (LTS) x64", @@ -51,7 +51,7 @@ "kernel": null, "locked": false, "memory": 512, - "name": "spawn-record-1770779252", + "name": "spawn-record-1770964632", "networks": { "v4": [], "v6": [] @@ -276,8 +276,8 @@ "links": { "actions": [ { - "href": "/v2/actions/3038067722", - "id": 3038067722, + "href": "/v2/actions/3041720329", + "id": 3041720329, "rel": "create" } ] diff --git a/test/fixtures/digitalocean/delete_server.json b/test/fixtures/digitalocean/delete_server.json index 0967ef424..3a7eeca98 100644 --- a/test/fixtures/digitalocean/delete_server.json +++ b/test/fixtures/digitalocean/delete_server.json @@ -1 +1,4 @@ -{} +{ + "http_code": 204, + "status": "deleted" +} diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 3ef65964d..2ec9f844f 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -289,11 +289,9 @@ "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", - "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", - "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", @@ -317,9 +315,7 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -329,7 +325,6 @@ "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -381,10 +376,12 @@ "s-2vcpu-4gb-amd", "s-2vcpu-4gb-intel", "s-2vcpu-4gb-120gb-intel", + "s-2vcpu-8gb-amd", "c-2", "c2-2vcpu-4gb", "s-2vcpu-8gb-160gb-intel", "s-4vcpu-8gb", + "s-4vcpu-8gb-amd", "s-4vcpu-8gb-intel", "c5-2vcpu-4gb", "g-2vcpu-8gb", @@ -392,6 +389,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -403,6 +401,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -417,6 +416,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1158,17 +1158,13 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1176,7 +1172,6 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -1191,27 +1186,29 @@ "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", - "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", - "so1_5-32vcpu-256gb", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "fra1" diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 2d8280f88..c5883a23a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-11T03:07:29Z", + "recorded_at": "2026-02-10T21:55:26Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-11T03:07:22Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-11T03:07:23Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-11T03:07:24Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-11T03:07:25Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-11T03:07:26Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-11T03:07:29Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index d731f2840..74637d827 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 610873205655089, + "id": 610821665885201, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-11T03:07:26Z", + "started": "2026-02-10T21:55:23Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 610873205655090, - "parent_id": 610873205655089, + "id": 610821665885202, + "parent_id": 610821665885201, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" } ], - "started": "2026-02-11T03:07:26Z", + "started": "2026-02-10T21:55:23Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-11T03:07:26Z", + "created": "2026-02-10T21:55:23Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -166,7 +166,7 @@ ] } }, - "id": 120704776, + "id": 120682780, "image": { "architecture": "x86", "bound_to": null, @@ -205,7 +205,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770779245", + "name": "spawn-record-1770760522", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -219,15 +219,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.236.114.98.91.clients.your-server.de", - "id": 117647239, - "ip": "91.98.114.236" + "dns_ptr": "static.197.210.245.188.clients.your-server.de", + "id": 117603379, + "ip": "188.245.210.197" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117647240, - "ip": "2a01:4f8:1c1e:e2da::/64" + "id": 117603380, + "ip": "2a01:4f8:1c1e:bc55::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index d65164ead..bbf825949 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 610873205655095, + "id": 610821665885215, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" } ], - "started": "2026-02-11T03:07:29Z", + "started": "2026-02-10T21:55:26Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index b30c403b7..dd6315039 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,7 +6,7 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 3 + "total_entries": 1 } }, "servers": [ @@ -163,7 +163,7 @@ "type": "system" }, "included_traffic": 21990232555520, - "ingoing_traffic": 1481770000, + "ingoing_traffic": 0, "iso": null, "labels": {}, "load_balancers": [], @@ -179,7 +179,7 @@ }, "locked": false, "name": "claude", - "outgoing_traffic": 14700000, + "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, "private_net": [], @@ -287,562 +287,6 @@ }, "status": "running", "volumes": [] - }, - { - "backup_window": null, - "created": "2026-02-11T01:39:38Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120699126, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "spawn-record-1770773977", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.197.210.245.188.clients.your-server.de", - "id": 117636450, - "ip": "188.245.210.197" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117636451, - "ip": "2a01:4f8:1c1e:bc55::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] - }, - { - "backup_window": null, - "created": "2026-02-11T01:40:34Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120699168, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "spawn-record-1770774032", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.36.112.98.91.clients.your-server.de", - "id": 117636536, - "ip": "91.98.112.36" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117636537, - "ip": "2a01:4f8:1c1c:cff1::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] } ] } diff --git a/test/fixtures/lambda/_metadata.json b/test/fixtures/lambda/_metadata.json index 6e08753ae..1858a8b2b 100644 --- a/test/fixtures/lambda/_metadata.json +++ b/test/fixtures/lambda/_metadata.json @@ -1,10 +1,9 @@ { "cloud": "lambda", - "recorded_at": "2026-02-11T00:54:13Z", + "recorded_at": "2026-02-13T06:37:11Z", "fixtures": { - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T00:54:12Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-11T00:54:12Z"}, - "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-11T00:54:12Z"}, - "create_server": {"endpoint": "POST /instance-operations/launch", "type": "live", "recorded_at": "2026-02-11T00:54:13Z"} + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-13T06:37:10Z"}, + "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-13T06:37:11Z"} } } diff --git a/test/fixtures/lambda/instance_types.json b/test/fixtures/lambda/instance_types.json index f448e670e..55b7c795d 100644 --- a/test/fixtures/lambda/instance_types.json +++ b/test/fixtures/lambda/instance_types.json @@ -67,7 +67,20 @@ "vcpus": 30 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Virginia, USA", + "name": "us-east-1" + }, + { + "description": "Arizona, USA", + "name": "us-west-2" + }, + { + "description": "India", + "name": "asia-south-1" + } + ] }, "gpu_1x_a6000": { "instance_type": { @@ -97,7 +110,12 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Sydney, Australia", + "name": "australia-east-1" + } + ] }, "gpu_1x_gh200": { "instance_type": { @@ -112,7 +130,12 @@ "vcpus": 64 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Washington DC, USA", + "name": "us-east-3" + } + ] }, "gpu_1x_h100_pcie": { "instance_type": { @@ -142,7 +165,12 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Central Texas, USA", + "name": "us-south-3" + } + ] }, "gpu_1x_rtx6000": { "instance_type": { @@ -277,7 +305,12 @@ "vcpus": 104 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "North Texas, USA", + "name": "us-south-2" + } + ] }, "gpu_8x_a100": { "instance_type": { @@ -304,10 +337,6 @@ { "description": "Arizona, USA", "name": "us-west-2" - }, - { - "description": "Germany", - "name": "europe-central-1" } ] }, @@ -324,7 +353,16 @@ "vcpus": 240 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Illinois, USA", + "name": "us-midwest-1" + }, + { + "description": "Sydney, Australia", + "name": "australia-east-1" + } + ] }, "gpu_8x_b200_sxm6": { "instance_type": { @@ -354,7 +392,12 @@ "vcpus": 208 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "North Texas, USA", + "name": "us-south-2" + } + ] }, "gpu_8x_v100": { "instance_type": { diff --git a/test/fixtures/latitude/_metadata.json b/test/fixtures/latitude/_metadata.json new file mode 100644 index 000000000..51134fe55 --- /dev/null +++ b/test/fixtures/latitude/_metadata.json @@ -0,0 +1,9 @@ +{ + "cloud": "latitude", + "recorded_at": "2026-02-13T06:37:12Z", + "fixtures": { + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:10Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:11Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} + } +} diff --git a/test/fixtures/latitude/plans.json b/test/fixtures/latitude/plans.json new file mode 100644 index 000000000..e04f77dde --- /dev/null +++ b/test/fixtures/latitude/plans.json @@ -0,0 +1,4899 @@ +{ + "data": [ + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c2.small.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 0.9, + "month": 460.0, + "year": 3864.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-small-x86", + "specs": { + "cpu": { + "clock": 3.6, + "cores": 4, + "count": 1, + "type": "E-2234" + }, + "drives": [ + { + "count": 1, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_2X6KG5mA5yPBM", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c2.medium.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.73, + "month": 1200.0, + "year": 10608.0 + }, + "USD": { + "hour": 0.35, + "month": 243.0, + "year": 2146.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-medium-x86", + "specs": { + "cpu": { + "clock": 3.4, + "cores": 8, + "count": 1, + "type": "E-2278G" + }, + "drives": [ + { + "count": 2, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 64 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_GdmkONWoNjL1b", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c2.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-large-x86", + "specs": { + "cpu": { + "clock": 2.2, + "cores": 20, + "count": 2, + "type": "Silver 4210" + }, + "drives": [ + { + "count": 2, + "size": "1 TB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 128 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_zdexA0q8alQVv", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c1.tiny.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "LAX", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c1-tiny-x86", + "specs": { + "cpu": { + "clock": 3.5, + "cores": 4, + "count": 1, + "type": "E-2134" + }, + "drives": [ + { + "count": 1, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 16 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_D6B9VaLRN7vrk", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "s3.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 956.0, + "year": 8030.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "s3-large-x86", + "specs": { + "cpu": { + "clock": 2.85, + "cores": 24, + "count": 1, + "type": "AMD 7443P" + }, + "drives": [ + { + "count": 2, + "size": "3.8 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 512 + }, + "nics": [ + { + "count": 1, + "type": "10Gbps" + } + ] + } + }, + "id": "plan_y3ZXaDKW5p2ml", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c3.small.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.3, + "month": 664.0, + "year": 5578.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c3-small-x86", + "specs": { + "cpu": { + "clock": 3.5, + "cores": 6, + "count": 1, + "type": "E-2386G" + }, + "drives": [ + { + "count": 1, + "size": "800 GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_gXQvNe93NzpbP", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "m3.large.x86", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "NYC", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 11.0, + "month": 5621.0, + "year": 47216.0 + }, + "USD": { + "hour": 2.2, + "month": 1124.0, + "year": 9442.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m3-large-x86", + "specs": { + "cpu": { + "clock": 2.8, + "cores": 32, + "count": 1, + "type": "AMD 7543P" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 1024 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10 Gbps" + } + ] + } + }, + "id": "plan_jv6m5JADNLPeE", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c3.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 6.72, + "month": 3434.0, + "year": 28846.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "low" + } + ], + "slug": "c3-large-x86", + "specs": { + "cpu": { + "clock": 2.85, + "cores": 24, + "count": 1, + "type": "AMD 7443P" + }, + "drives": [ + { + "count": 2, + "size": "1.9 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 256 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10 Gbps" + } + ] + } + }, + "id": "plan_M3Beabem5LnbE", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "s2.small.x86", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + } + ], + "slug": "s2-small-x86", + "specs": { + "cpu": { + "clock": 3.7, + "cores": 6, + "count": 1, + "type": "E-2176G" + }, + "drives": [ + { + "count": 2, + "size": "500 GB", + "type": "SSD" + }, + { + "count": 2, + "size": "1 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_dexA0qn8NlQVv", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g3.h100.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 45.03, + "month": 23010.0, + "year": 193284.0 + }, + "USD": { + "hour": 7.97, + "month": 4073.0, + "year": 34213.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "g3-h100-medium", + "specs": { + "cpu": { + "clock": 3.25, + "cores": 64, + "count": 2, + "type": "AMD 9354" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 4, + "interconnect": "Paired-NVLink, PCIe based", + "type": "NVIDIA H100 80GB NVLink", + "vram_per_gpu": 80 + }, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 2, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_3xMV0xWKamQp7", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g3.h100.small", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu24_ml_in_a_box" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 13.62, + "month": 6960.0, + "year": 58464.0 + }, + "USD": { + "hour": 2.41, + "month": 1232.0, + "year": 10349.0 + } + }, + "stock_level": "high" + } + ], + "slug": "g3-h100-small", + "specs": { + "cpu": { + "clock": 3.0, + "cores": 16, + "count": 1, + "type": "AMD 9124" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 1, + "interconnect": "Paired-NVLink, PCIe based", + "type": "NVIDIA H100 80GB", + "vram_per_gpu": 80 + }, + "memory": { + "total": 192 + }, + "nics": [ + { + "count": 2, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_lnow0Ovwa9x1q", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "m4.metal.small", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "MIA2", + "LAX2", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 2.49, + "month": 1272.0, + "year": 10685.0 + }, + "USD": { + "hour": 0.44, + "month": 225.0, + "year": 1890.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 2.55, + "month": 1303.0, + "year": 10945.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [ + "BGT" + ] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 2.2, + "month": 1124.0, + "year": 9442.0 + }, + "USD": { + "hour": 0.44, + "month": 225.0, + "year": 1890.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "high" + } + ], + "slug": "m4-metal-small", + "specs": { + "cpu": { + "clock": 3.8, + "cores": 6, + "count": 1, + "type": "AMD 4244P" + }, + "drives": [ + { + "count": 2, + "size": "960GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 64 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_8mop5gXvajxvn", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 5.1, + "month": 2606.0, + "year": 21890.0 + }, + "USD": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON", + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [ + "BGT" + ] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 5.1, + "month": 2606.0, + "year": 21890.0 + }, + "USD": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m4-metal-medium", + "specs": { + "cpu": { + "clock": 3.0, + "cores": 16, + "count": 1, + "type": "AMD 9124" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "1.9TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 128 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_zZOo0l3eaKQYb", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.large", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON", + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "low" + } + ], + "slug": "m4-metal-large", + "specs": { + "cpu": { + "clock": 2.9, + "cores": 24, + "count": 1, + "type": "AMD 9254" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 384 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_g3PYaRB3ayozM", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "rs4.metal.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [ + "SAO2" + ] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 15.93, + "month": 8140.0, + "year": 68376.0 + }, + "USD": { + "hour": 2.82, + "month": 1441.0, + "year": 12104.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 15.93, + "month": 8140.0, + "year": 68376.0 + }, + "USD": { + "hour": 2.82, + "month": 1441.0, + "year": 12104.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 12.71, + "month": 6495.0, + "year": 54558.0 + }, + "USD": { + "hour": 2.25, + "month": 1150.0, + "year": 9660.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 12.71, + "month": 6495.0, + "year": 54558.0 + }, + "USD": { + "hour": 2.25, + "month": 1150.0, + "year": 9660.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + } + ], + "slug": "rs4-metal-large", + "specs": { + "cpu": { + "clock": 3.25, + "cores": 32, + "count": 1, + "type": "AMD 9354P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_mJyo0vmLaDkw9", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "rs4.metal.xlarge", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 21.64, + "month": 11058.0, + "year": 92887.0 + }, + "USD": { + "hour": 3.83, + "month": 1957.0, + "year": 16439.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 21.64, + "month": 11058.0, + "year": 92887.0 + }, + "USD": { + "hour": 3.83, + "month": 1957.0, + "year": 16439.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "low" + } + ], + "slug": "rs4-metal-xlarge", + "specs": { + "cpu": { + "clock": 3.1, + "cores": 64, + "count": 1, + "type": "AMD 9554P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 4, + "size": "8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 1536 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_ewOM5GAb0p8ER", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "f4.metal.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 8.81, + "month": 4502.0, + "year": 37817.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts", + "ipxe" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 8.81, + "month": 4502.0, + "year": 37817.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 7.0, + "month": 3577.0, + "year": 30047.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [ + "BUE" + ] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 7.8, + "month": 3986.0, + "year": 33482.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 7.0, + "month": 3577.0, + "year": 30047.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 6.16, + "month": 3148.0, + "year": 26443.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + } + ], + "slug": "f4-metal-medium", + "specs": { + "cpu": { + "clock": 4.5, + "cores": 16, + "count": 1, + "type": "AMD 4564P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "1.9TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 192 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_w8MVaone523gj", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "f4.metal.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [ + "SAO2" + ] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 17.52, + "month": 8953.0, + "year": 75205.0 + }, + "USD": { + "hour": 3.1, + "month": 1584.0, + "year": 13306.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "LAX2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 12.5, + "month": 6388.0, + "year": 53659.0 + }, + "USD": { + "hour": 2.5, + "month": 1277.0, + "year": 10727.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 12.5, + "month": 6388.0, + "year": 53659.0 + }, + "USD": { + "hour": 2.5, + "month": 1277.0, + "year": 10727.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 12.26, + "month": 6265.0, + "year": 52626.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "low" + } + ], + "slug": "f4-metal-large", + "specs": { + "cpu": { + "clock": 4.1, + "cores": 24, + "count": 1, + "type": "AMD 9275F" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_7vRENk3gNdPyk", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "f4.metal.small", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 4.41, + "month": 2254.0, + "year": 18934.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 4.41, + "month": 2254.0, + "year": 18934.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 3.35, + "month": 1712.0, + "year": 14381.0 + }, + "USD": { + "hour": 0.67, + "month": 342.0, + "year": 2873.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 3.9, + "month": 1993.0, + "year": 16741.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 3.35, + "month": 1712.0, + "year": 14381.0 + }, + "USD": { + "hour": 0.67, + "month": 342.0, + "year": 2873.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 3.22, + "month": 1645.0, + "year": 13818.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + } + ], + "slug": "f4-metal-small", + "specs": { + "cpu": { + "clock": 4.4, + "cores": 12, + "count": 1, + "type": "AMD 4484PX" + }, + "drives": [ + { + "count": 2, + "size": "960GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 96 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_PVwea4vvNB9OQ", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g4.rtx6kpro.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu24_ml_in_a_box", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 110.18, + "month": 56302.0, + "year": 472937.0 + }, + "USD": { + "hour": 19.5, + "month": 9964.0, + "year": 83698.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 0.0, + "month": 0.0, + "year": 0.0 + }, + "USD": { + "hour": null, + "month": null, + "year": null + } + }, + "stock_level": "unavailable" + } + ], + "slug": "g4-rtx6kpro-large", + "specs": { + "cpu": { + "clock": 3.55, + "cores": 64, + "count": 2, + "type": "AMD 9355" + }, + "drives": [ + { + "count": 4, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 8, + "interconnect": "No Interconnect", + "type": "NVIDIA RTX PRO 6000 Server Edition", + "vram_per_gpu": 96 + }, + "memory": { + "total": 1536 + }, + "nics": [ + { + "count": 2, + "type": "100 Gbps" + } + ] + } + }, + "id": "plan_YGwn0Vnq063JD", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.xlarge", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 17.01, + "month": 8692.0, + "year": 73013.0 + }, + "USD": { + "hour": 3.01, + "month": 1538.0, + "year": 12919.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 17.01, + "month": 8692.0, + "year": 73013.0 + }, + "USD": { + "hour": 3.01, + "month": 1538.0, + "year": 12919.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m4-metal-xlarge", + "specs": { + "cpu": { + "clock": 3.15, + "cores": 48, + "count": 1, + "type": "AMD 9455P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_y9815Xnx0vEkd", + "type": "plans" + } + ], + "meta": {} +} diff --git a/test/fixtures/latitude/regions.json b/test/fixtures/latitude/regions.json new file mode 100644 index 000000000..2a19e2488 --- /dev/null +++ b/test/fixtures/latitude/regions.json @@ -0,0 +1,285 @@ +{ + "data": [ + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Digital Realty Silicon Valley - SJC15", + "name": "Silicon Valley", + "slug": "SJC2", + "type": "Core" + }, + "id": "loc_kW9EKa3v5RoBV", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Japan", + "slug": "japan" + }, + "facility": "Digital Realty Tokyo - NRT10", + "name": "Tokyo 4", + "slug": "TYO4", + "type": "Core" + }, + "id": "loc_9vAPXaMp5epzk", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "CenterSquare LAX5", + "name": "Los Angeles 2", + "slug": "LAX2", + "type": "Core" + }, + "id": "loc_bEvjLaBg0oqyx", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Netherlands", + "slug": "netherlands" + }, + "facility": "LeaseWeb Amsterdam AMS-01", + "name": "Amsterdam", + "slug": "AMS", + "type": "Core" + }, + "id": "loc_E7pWRawp0rD6y", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Germany", + "slug": "germany" + }, + "facility": "Leaseweb FRA1 ( Iron Mountain FRA-2)", + "name": "Frankfurt 2", + "slug": "FRA2", + "type": "Core" + }, + "id": "loc_qewOM5GbNp8ER", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Australia", + "slug": "australia" + }, + "facility": "Equinix SY5", + "name": "Sydney 2", + "slug": "SYD2", + "type": "Core" + }, + "id": "loc_OmJyo0vL5Dkw9", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Singapore", + "slug": "singapore" + }, + "facility": "Equinix SG2", + "name": "Singapore", + "slug": "SGP", + "type": "Core" + }, + "id": "loc_4g3PYaR30yozM", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Japan", + "slug": "japan" + }, + "facility": "Equinix TY10", + "name": "Tokyo", + "slug": "TYO3", + "type": "Core" + }, + "id": "loc_nzZOo0leNKQYb", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United Kingdom", + "slug": "united-kingdom" + }, + "facility": "Telehouse Docklands South", + "name": "London 2", + "slug": "LON2", + "type": "Core" + }, + "id": "loc_28mop5gvNjxvn", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Colohouse - Miami", + "name": "Miami", + "slug": "MIA2", + "type": "Core" + }, + "id": "loc_JLqG158d5BOgv", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Chile", + "slug": "chile" + }, + "facility": "Ascenty Chile 2", + "name": "Santiago 3", + "slug": "SAN3", + "type": "Core" + }, + "id": "loc_D6B9VaLRN7vrk", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Germany", + "slug": "germany" + }, + "facility": "Leaseweb Germany GmbH", + "name": "Frankfurt", + "slug": "FRA", + "type": "Core" + }, + "id": "loc_PogXka9d5JdBA", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Mexico", + "slug": "mexico" + }, + "facility": "Ascenty MEX1", + "name": "Mexico City", + "slug": "MEX2", + "type": "Core" + }, + "id": "loc_Ylnow0Ow09x1q", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Colombia", + "slug": "colombia" + }, + "facility": "Odata", + "name": "Bogota", + "slug": "BGT", + "type": "Core" + }, + "id": "loc_l3xMV0xKamQp7", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "DEFT IAD", + "name": "Ashburn", + "slug": "ASH", + "type": "Core" + }, + "id": "loc_zdexA0q8alQVv", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Deft CH1", + "name": "Chicago", + "slug": "CHI", + "type": "Core" + }, + "id": "loc_wmgWeN6O5Yd7k", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Argentina", + "slug": "argentina" + }, + "facility": "SYT", + "name": "Buenos Aires", + "slug": "BUE", + "type": "Core" + }, + "id": "loc_rx3egaQx546Q8", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Brazil", + "slug": "brazil" + }, + "facility": "Scala SP2", + "name": "S\u00e3o Paulo 2", + "slug": "SAO2", + "type": "Core" + }, + "id": "loc_ZnPRbajK0koM1", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United Kingdom", + "slug": "united-kingdom" + }, + "facility": "LDEX1", + "name": "London", + "slug": "LON", + "type": "Core" + }, + "id": "loc_ndYjv5rwaqp7l", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Australia", + "slug": "australia" + }, + "facility": "Global Switch SYD", + "name": "Sydney", + "slug": "SYD", + "type": "Core" + }, + "id": "loc_Dy9815Xx5vEkd", + "type": "regions" + } + ], + "meta": {} +} diff --git a/test/fixtures/latitude/ssh_keys.json b/test/fixtures/latitude/ssh_keys.json new file mode 100644 index 000000000..a73cfd655 --- /dev/null +++ b/test/fixtures/latitude/ssh_keys.json @@ -0,0 +1,4 @@ +{ + "data": [], + "meta": {} +} diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index b050bf247..029fa863d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "linode", - "recorded_at": "2026-02-11T03:01:14Z", + "recorded_at": "2026-02-10T21:55:36Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-11T03:01:06Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-11T03:01:07Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-11T03:01:07Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-11T03:01:08Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T03:01:08Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-11T03:01:10Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-11T03:01:14Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} } } diff --git a/test/fixtures/linode/create_server.json b/test/fixtures/linode/create_server.json index ac9a6578f..f0405c827 100644 --- a/test/fixtures/linode/create_server.json +++ b/test/fixtures/linode/create_server.json @@ -19,20 +19,20 @@ "Block Storage Encryption", "Maintenance Policy" ], - "created": "2026-02-11T03:01:09", + "created": "2026-02-13T06:37:12", "disk_encryption": "enabled", "group": "", "has_user_data": false, - "host_uuid": "fc1687288efff1e09ccc267d044e3002025130de", + "host_uuid": "e36b971087c98564b67156145d654ec2cb99733a", "hypervisor": "kvm", - "id": 91482496, + "id": 91660266, "image": "linode/ubuntu24.04", "interface_generation": "legacy_config", "ipv4": [ - "23.92.20.47" + "45.56.108.222" ], - "ipv6": "2600:3c03::2000:caff:fea5:da9f/128", - "label": "spawn-record-1770778868", + "ipv6": "2600:3c03::2000:2fff:fe0f:c385/128", + "label": "spawn-record-1770964631", "lke_cluster_id": null, "maintenance_policy": "linode/migrate", "placement_group": null, @@ -49,6 +49,6 @@ "status": "provisioning", "tags": [], "type": "g6-nanode-1", - "updated": "2026-02-11T03:01:09", + "updated": "2026-02-13T06:37:12", "watchdog_enabled": true } diff --git a/test/fixtures/scaleway/_metadata.json b/test/fixtures/scaleway/_metadata.json index c78a3354d..991bbab65 100644 --- a/test/fixtures/scaleway/_metadata.json +++ b/test/fixtures/scaleway/_metadata.json @@ -1,8 +1,8 @@ { "cloud": "scaleway", - "recorded_at": "2026-02-11T01:51:27Z", + "recorded_at": "2026-02-13T06:37:11Z", "fixtures": { - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-11T01:51:26Z"}, - "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-11T01:51:27Z"} + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:10Z"}, + "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-13T06:37:11Z"} } } diff --git a/test/fixtures/scaleway/images.json b/test/fixtures/scaleway/images.json index 6f6505628..26d83f5a3 100644 --- a/test/fixtures/scaleway/images.json +++ b/test/fixtures/scaleway/images.json @@ -2,40 +2,40 @@ "images": [ { "arch": "x86_64", - "creation_date": "2026-02-10T16:31:15.771530+00:00", + "creation_date": "2026-02-11T15:09:32.597221+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "374b4139-51c8-4497-80da-bde105394f09", - "modification_date": "2026-02-10T16:31:15.771530+00:00", - "name": "Windows Server 2025", + "id": "5c248ca4-5058-489b-8481-be7d2799fb0a", + "modification_date": "2026-02-11T15:09:32.597221+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "6a1029ca-329a-4202-9b6d-3445bdbf0916", - "name": "", - "size": 0, - "volume_type": "sbs_snapshot" + "id": "4159bb92-5a75-44ee-9ce5-8294de74cc2d", + "name": "k8s_base_node_instance", + "size": 10000000000, + "volume_type": "l_ssd" }, "state": "available", "tags": [], "zone": "fr-par-1" }, { - "arch": "x86_64", - "creation_date": "2026-02-10T11:30:39.697503+00:00", + "arch": "arm64", + "creation_date": "2026-02-11T15:08:54.481101+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "ca5e7780-a7c3-451f-a528-0eabc745343c", - "modification_date": "2026-02-10T11:30:39.697503+00:00", - "name": "Windows Server 2025 Core", + "id": "28ee64af-8f6e-43d3-b9ea-d3d4a123b687", + "modification_date": "2026-02-11T15:08:54.481101+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "955c1e88-d75b-4ca1-b53e-b5c9c07a7bb1", + "id": "89b1b122-8470-4c54-8919-366266f658c8", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -46,18 +46,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-05T15:28:32.401289+00:00", + "creation_date": "2026-02-11T15:08:08.602556+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "ca115f77-d927-4483-b1f8-354107143e8c", - "modification_date": "2026-02-05T15:28:32.401289+00:00", - "name": "Windows Server 2025", + "id": "0a25b795-9e9b-4351-a445-8865d3d41410", + "modification_date": "2026-02-11T15:08:08.602556+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "e7d1c335-e70a-419b-8a71-07f61e21b729", + "id": "7b182ad3-7b1e-4b7a-8c74-cc88302d3036", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -68,18 +68,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-05T14:56:51.105141+00:00", + "creation_date": "2026-02-10T16:31:15.771530+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "3fea98bc-5e72-4f0c-a4bd-28d38010ff34", - "modification_date": "2026-02-05T14:56:51.105141+00:00", - "name": "Windows Server 2025 Core", + "id": "374b4139-51c8-4497-80da-bde105394f09", + "modification_date": "2026-02-10T16:31:15.771530+00:00", + "name": "Windows Server 2025", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "93775898-0256-4eac-a773-cb154aab73ba", + "id": "6a1029ca-329a-4202-9b6d-3445bdbf0916", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -89,19 +89,19 @@ "zone": "fr-par-1" }, { - "arch": "arm64", - "creation_date": "2026-02-04T12:02:05.696710+00:00", + "arch": "x86_64", + "creation_date": "2026-02-10T11:30:39.697503+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "2a3ad407-ad47-4b6a-89b3-7d6d820d24bb", - "modification_date": "2026-02-04T12:02:05.696710+00:00", - "name": "debian-trixie", + "id": "ca5e7780-a7c3-451f-a528-0eabc745343c", + "modification_date": "2026-02-10T11:30:39.697503+00:00", + "name": "Windows Server 2025 Core", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "77cd231f-aa0c-4bff-a180-7d5cde4f048e", + "id": "955c1e88-d75b-4ca1-b53e-b5c9c07a7bb1", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -112,21 +112,21 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-04T12:00:27.604925+00:00", + "creation_date": "2026-02-05T15:28:32.401289+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "024676ab-6b3c-4473-b538-181a688d62e7", - "modification_date": "2026-02-04T12:00:27.604925+00:00", - "name": "debian-trixie", + "id": "ca115f77-d927-4483-b1f8-354107143e8c", + "modification_date": "2026-02-05T15:28:32.401289+00:00", + "name": "Windows Server 2025", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "c5af243c-1e63-4799-8f01-d3302d6fea72", - "name": "debian-trixie", - "size": 10000000000, - "volume_type": "l_ssd" + "id": "e7d1c335-e70a-419b-8a71-07f61e21b729", + "name": "", + "size": 0, + "volume_type": "sbs_snapshot" }, "state": "available", "tags": [], @@ -134,18 +134,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-04T12:00:22.720696+00:00", + "creation_date": "2026-02-05T14:56:51.105141+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "71d4a59b-5c9a-4084-b62b-f32bc066b1c3", - "modification_date": "2026-02-04T12:00:22.720696+00:00", - "name": "debian-trixie", + "id": "3fea98bc-5e72-4f0c-a4bd-28d38010ff34", + "modification_date": "2026-02-05T14:56:51.105141+00:00", + "name": "Windows Server 2025 Core", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "8f813bfe-0ad8-4ff1-aeb3-663d9e1670af", + "id": "93775898-0256-4eac-a773-cb154aab73ba", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -156,18 +156,18 @@ }, { "arch": "arm64", - "creation_date": "2026-01-28T16:28:07.893202+00:00", + "creation_date": "2026-02-04T12:02:05.696710+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "2dcbfb4f-1b77-4733-b734-954b09c2b17b", - "modification_date": "2026-01-28T16:28:07.893202+00:00", + "id": "2a3ad407-ad47-4b6a-89b3-7d6d820d24bb", + "modification_date": "2026-02-04T12:02:05.696710+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "a33ea2ec-9531-4ce2-b2c1-6e4979c371f6", + "id": "77cd231f-aa0c-4bff-a180-7d5cde4f048e", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -178,18 +178,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-01-28T16:27:26.694891+00:00", + "creation_date": "2026-02-04T12:00:27.604925+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "69982b79-5db1-487a-8fd4-cdb3547f4bb2", - "modification_date": "2026-01-28T16:27:26.694891+00:00", + "id": "024676ab-6b3c-4473-b538-181a688d62e7", + "modification_date": "2026-02-04T12:00:27.604925+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "a6ed67b9-9789-4b18-875b-4e747eda3dc9", + "id": "c5af243c-1e63-4799-8f01-d3302d6fea72", "name": "debian-trixie", "size": 10000000000, "volume_type": "l_ssd" @@ -200,18 +200,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-01-28T16:27:25.789355+00:00", + "creation_date": "2026-02-04T12:00:22.720696+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "6d173ed8-ae70-4671-a8f1-584921c4e706", - "modification_date": "2026-01-28T16:27:25.789355+00:00", + "id": "71d4a59b-5c9a-4084-b62b-f32bc066b1c3", + "modification_date": "2026-02-04T12:00:22.720696+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "5f780810-ef25-4def-987b-b03409862d08", + "id": "8f813bfe-0ad8-4ff1-aeb3-663d9e1670af", "name": "", "size": 0, "volume_type": "sbs_snapshot" diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 523379dac..96a70de8c 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "vultr", - "recorded_at": "2026-02-11T03:01:06Z", + "recorded_at": "2026-02-10T21:55:33Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-11T02:59:20Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-11T02:59:21Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T02:59:21Z"}, - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-11T02:59:24Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T02:59:25Z"}, - "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-11T02:59:27Z"}, - "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-11T03:01:06Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} } } diff --git a/test/fixtures/vultr/account.json b/test/fixtures/vultr/account.json index d580fc601..f22b4a562 100644 --- a/test/fixtures/vultr/account.json +++ b/test/fixtures/vultr/account.json @@ -23,17 +23,17 @@ "address2": "", "balance": 0, "city": "San Jose", - "company_industry_id": "34", - "company_info": "tests please let me run it", - "company_name": "test", - "company_size_id": "3", + "company_industry_id": "0", + "company_info": "", + "company_name": "", + "company_size_id": 0, "company_url": "", "country": "US", "email": "ahmed@abushagur.com", - "interest": "DevOps", + "interest": "", "name": "Ahmed Abushagur", "org_name": "", - "pending_charges": 0.1, + "pending_charges": 0, "prepayment_remaining": "0.00", "state": "CA" } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 1e72f225f..d4382630c 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -495,14 +495,7 @@ "hourly_cost_preemptible": 0.438, "id": "vc2-16c-64gb", "invoice_type": "monthly", - "location_cost": { - "sao": { - "hourly_cost": 0.658, - "hourly_cost_preemptible": 0.658, - "monthly_cost": 480, - "monthly_cost_preemptible": 480 - } - }, + "location_cost": {}, "locations": [ "ewr", "ord", @@ -525,7 +518,6 @@ "sgp", "sto", "mex", - "sao", "mel", "bom", "jnb", @@ -5370,7 +5362,8 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "ewr" + "ewr", + "nrt" ], "monthly_cost": 315, "monthly_cost_preemptible": 315, diff --git a/test/qa-dry-cycle.sh b/test/qa-dry-cycle.sh new file mode 100644 index 000000000..51b31c0a8 --- /dev/null +++ b/test/qa-dry-cycle.sh @@ -0,0 +1,595 @@ +#!/bin/bash +set -eo pipefail + +# QA Dry Run — Same phases as qa-cycle.sh but NO GitHub interaction. +# Runs locally: records fixtures, mock tests, spawns fix agents, re-tests. +# Skips: git push, PRs, issues, remote branch cleanup, git reset --hard origin/main. +# Artifacts saved to .docs/qa-dry-run-latest/ for inspection. + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +if [[ -z "${REPO_ROOT}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +fi +cd "${REPO_ROOT}" + +SPAWN_REASON="${SPAWN_REASON:-manual}" +WORKTREE_BASE="/tmp/spawn-worktrees/qa-dry" +CYCLE_TIMEOUT=2700 # 45 min total + +# Output directory for dry-run artifacts +DRY_OUTPUT_DIR="${REPO_ROOT}/.docs/qa-dry-run-latest" +rm -rf "${DRY_OUTPUT_DIR}" +mkdir -p "${DRY_OUTPUT_DIR}" + +LOG_FILE="${DRY_OUTPUT_DIR}/qa-dry-run.log" + +# Results files +RESULTS_PHASE2="/tmp/spawn-qa-dry-results.txt" +RESULTS_PHASE4="/tmp/spawn-qa-dry-results-retry.txt" + +# Ensure directories +mkdir -p "$(dirname "${LOG_FILE}")" "${WORKTREE_BASE}" + +log() { + printf '[%s] [qa-dry] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*" | tee -a "${LOG_FILE}" +} + +# Log what would happen in a real cycle +dry_log() { + log "DRY_RUN: Would run: $*" + printf '[would-run] %s\n' "$*" >> "${DRY_OUTPUT_DIR}/would-commit.txt" +} + +cleanup() { + local exit_code=$? + log "Running cleanup (exit_code=${exit_code})..." + cd "${REPO_ROOT}" 2>/dev/null || true + git worktree prune 2>/dev/null || true + rm -rf "${WORKTREE_BASE}" 2>/dev/null || true + rm -f "${RESULTS_PHASE2}" "${RESULTS_PHASE4}" "/tmp/spawn-qa-dry-record-output.txt" 2>/dev/null || true + # Save results files to output dir before deleting + [[ -f "${RESULTS_PHASE2}" ]] && cp "${RESULTS_PHASE2}" "${DRY_OUTPUT_DIR}/results-phase2.txt" 2>/dev/null || true + [[ -f "${RESULTS_PHASE4}" ]] && cp "${RESULTS_PHASE4}" "${DRY_OUTPUT_DIR}/results-phase4.txt" 2>/dev/null || true + log "=== QA Dry Run Done (exit_code=${exit_code}) ===" + exit $exit_code +} + +trap cleanup EXIT SIGTERM SIGINT + +# macOS-compatible timeout: run command with a time limit +# Usage: run_with_timeout SECONDS COMMAND [ARGS...] +run_with_timeout() { + local secs="$1"; shift + "$@" & + local pid=$! + local elapsed=0 + while kill -0 "$pid" 2>/dev/null; do + if [[ "$elapsed" -ge "$secs" ]]; then + kill "$pid" 2>/dev/null + sleep 1 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + wait "$pid" 2>/dev/null +} + +log "=== Starting QA Dry Run ===" +log "Repo root: ${REPO_ROOT}" +log "Output dir: ${DRY_OUTPUT_DIR}" +log "Timeout: ${CYCLE_TIMEOUT}s" + +# Track start time for total cycle timeout +CYCLE_START=$(date +%s) + +check_timeout() { + local now elapsed + now=$(date +%s) + elapsed=$((now - CYCLE_START)) + if [[ "$elapsed" -ge "$CYCLE_TIMEOUT" ]]; then + log "TIMEOUT: Cycle exceeded ${CYCLE_TIMEOUT}s, stopping" + return 1 + fi + return 0 +} + +# ============================================================ +# Pre-cycle cleanup (local only — no remote branch/PR operations) +# ============================================================ +log "Pre-cycle cleanup (local only)..." + +# Clean stale worktrees +git worktree prune 2>&1 | tee -a "${LOG_FILE}" || true +if [[ -d "${WORKTREE_BASE}" ]]; then + rm -rf "${WORKTREE_BASE}" 2>&1 | tee -a "${LOG_FILE}" || true + log "Removed stale ${WORKTREE_BASE} directory" +fi +mkdir -p "${WORKTREE_BASE}" + +# Delete stale local qa-dry/* branches only +LOCAL_QA_BRANCHES=$(git branch --list 'qa-dry/*' | tr -d ' *') || true +for branch in $LOCAL_QA_BRANCHES; do + if [[ -n "$branch" ]]; then + git branch -D "$branch" 2>&1 | tee -a "${LOG_FILE}" || true + fi +done + +log "Pre-cycle cleanup complete" + +# ============================================================ +# Phase 0: Key Preflight +# ============================================================ +log "=== Phase 0: Key Preflight ===" + +if [[ -f "${REPO_ROOT}/shared/key-request.sh" ]]; then + source "${REPO_ROOT}/shared/key-request.sh" + load_cloud_keys_from_config + if [[ -n "${MISSING_KEY_PROVIDERS:-}" ]]; then + log "Key preflight: Missing keys for: ${MISSING_KEY_PROVIDERS}" + log "Phase 0: Missing keys for: ${MISSING_KEY_PROVIDERS}" + dry_log "request_missing_cloud_keys for: ${MISSING_KEY_PROVIDERS}" + else + log "Phase 0: All cloud keys available" + fi +else + log "Phase 0: shared/key-request.sh not found, skipping key preflight" +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 1: Record fixtures +# ============================================================ +log "=== Phase 1: Record fixtures ===" + +RECORD_OUTPUT="/tmp/spawn-qa-dry-record-output.txt" +rm -f "${RECORD_OUTPUT}" + +RECORD_EXIT=0 +bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$? + +if [[ "${RECORD_EXIT}" -eq 0 ]]; then + log "Phase 1: All fixtures recorded successfully" +else + log "Phase 1: Some fixture recordings failed, identifying failed clouds..." + + # Parse which clouds had failures from record.sh output + RECORD_FAILED_CLOUDS="" + current_cloud="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + ;; + *"fail "*) + if [[ -n "${current_cloud}" ]]; then + case " ${RECORD_FAILED_CLOUDS} " in + *" ${current_cloud} "*) ;; # already tracked + *) RECORD_FAILED_CLOUDS="${RECORD_FAILED_CLOUDS} ${current_cloud}" ;; + esac + fi + ;; + esac + done < "${RECORD_OUTPUT}" + RECORD_FAILED_CLOUDS=$(printf '%s' "${RECORD_FAILED_CLOUDS}" | sed 's/^ //') + + if [[ -n "${RECORD_FAILED_CLOUDS}" ]]; then + log "Phase 1: Failed clouds: ${RECORD_FAILED_CLOUDS}" + + # Separate auth failures from code failures + NON_AUTH_FAILED_CLOUDS="" + STALE_KEY_PROVIDERS="" + AUTH_PATTERN="401|403|[Uu]nauthorized|[Ff]orbidden|[Ii]nvalid.*(token|key|api)|[Aa]ccess.denied|[Aa]uthentication.failed" + + for cloud in ${RECORD_FAILED_CLOUDS}; do + error_output=$(sed -n "/Recording ${cloud}/,/Recording \|━━━ \|Results:/p" "${RECORD_OUTPUT}" | head -50 || true) + + if printf '%s' "${error_output}" | grep -iqE "${AUTH_PATTERN}"; then + log "Phase 1: Auth failure for ${cloud} — key is stale, skipping fix agent" + if type invalidate_cloud_key &>/dev/null; then + invalidate_cloud_key "${cloud}" + while IFS= read -r var_name; do + [[ -n "${var_name}" ]] && unset "${var_name}" 2>/dev/null || true + done <<< "$(get_cloud_env_vars "${cloud}")" + fi + STALE_KEY_PROVIDERS="${STALE_KEY_PROVIDERS} ${cloud}" + else + NON_AUTH_FAILED_CLOUDS="${NON_AUTH_FAILED_CLOUDS} ${cloud}" + fi + done + NON_AUTH_FAILED_CLOUDS=$(printf '%s' "${NON_AUTH_FAILED_CLOUDS}" | sed 's/^ //') + STALE_KEY_PROVIDERS=$(printf '%s' "${STALE_KEY_PROVIDERS}" | sed 's/^ //') + + if [[ -n "${STALE_KEY_PROVIDERS}" ]]; then + log "Phase 1: Stale keys detected: ${STALE_KEY_PROVIDERS}" + fi + + # Spawn ONE agent per non-auth failed cloud (10 min each, one attempt only) + RECORD_FIX_PIDS="" + for cloud in ${NON_AUTH_FAILED_CLOUDS}; do + check_timeout || break + + error_lines=$(sed -n "/Recording ${cloud}/,/Recording \|━━━ \|Results:/p" "${RECORD_OUTPUT}" | head -30 || true) + + log "Phase 1: Spawning agent to debug ${cloud} recording failure" + worktree="${WORKTREE_BASE}/record-fix-${cloud}" + branch_name="qa-dry/record-fix-${cloud}" + + git worktree add "${worktree}" -b "${branch_name}" HEAD 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 1: Could not create worktree for ${cloud}, skipping" + continue + } + + ( + cd "${worktree}" + run_with_timeout 600 \ + claude -p "The API fixture recording for cloud '${cloud}' is failing in test/record.sh. + +Error output: +${error_lines} + +This likely means the cloud provider's API has changed. Investigate thoroughly and fix. + +## Investigation Steps (follow in order): + +1. **Check the provider's documentation:** + - Search for \"${cloud} API documentation\" or visit the provider's developer docs + - Look for recent API changelog or breaking changes announcements + - Note any version changes, deprecated endpoints, or new required headers + +2. **Understand the current implementation:** + - Read ${cloud}/lib/common.sh to see how API calls are currently made + - Read test/record.sh to understand the recording flow (get_endpoints, call_api) + - Identify which specific API endpoint is failing from the error output + +3. **Test the API manually (if possible):** + - Try calling the failing endpoint with curl to see the actual response + - Check if authentication headers have changed (API key format, Bearer tokens, etc.) + - Verify the endpoint URL is still correct (base URL changes, versioning) + - Compare the actual API response format with what the code expects + +4. **Check for common issues:** + - Has the base URL changed? (e.g., api.provider.com → api.v2.provider.com) + - Are there new required headers? (Content-Type, User-Agent, API version) + - Has the response schema changed? (different field names, nested structures) + - Are there new rate limits or authentication requirements? + +5. **Fix the lib/common.sh API functions:** + - Update endpoint URLs, headers, or request format to match current API + - Preserve backward compatibility where possible + - Add comments explaining what changed and why + +6. **Test your fix:** + - Run: bash test/record.sh ${cloud} + - Verify all endpoints record successfully + - Check that response formats are correct + +7. **Syntax check and commit:** + - Run: bash -n ${cloud}/lib/common.sh + - Commit with a clear message explaining what API change you fixed + +Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastructure needs updating." \ + 2>&1 | tee -a "${LOG_FILE}" || true + + # Check for changes (uncommitted OR committed by the agent) + has_uncommitted=$(git status --porcelain 2>/dev/null) + has_commits=$(git log HEAD@{1}..HEAD --oneline 2>/dev/null || true) + + if [[ -n "$has_uncommitted" ]] && bash -n "${cloud}/lib/common.sh" 2>/dev/null; then + git add "${cloud}/lib/common.sh" "test/record.sh" 2>/dev/null || true + git commit -m "$(printf 'fix: Update %s API integration for recording\n\nAgent: qa-record-fixer (dry run)\nCo-Authored-By: Claude Opus 4.6 ' "${cloud}")" || true + fi + + # Save diff instead of pushing + git diff HEAD~1..HEAD > "${DRY_OUTPUT_DIR}/diff-record-fix-${cloud}.patch" 2>/dev/null || true + dry_log "git push -u origin ${branch_name} && gh pr create for ${cloud} record fix" + ) & + RECORD_FIX_PIDS="${RECORD_FIX_PIDS} $!" + done + + # Wait for record-fix agents + for pid in ${RECORD_FIX_PIDS}; do + wait "$pid" 2>/dev/null || true + done + + # Clean up worktrees + for cloud in ${NON_AUTH_FAILED_CLOUDS}; do + git worktree remove "${WORKTREE_BASE}/record-fix-${cloud}" 2>/dev/null || true + git branch -D "qa-dry/record-fix-${cloud}" 2>/dev/null || true + done + git worktree prune 2>/dev/null || true + + # Re-record (ONE retry, no agents on second failure) — no git reset to origin + log "Phase 1: Re-recording after fixes (no agents on second failure)..." + bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 1: Re-record still has failures — continuing with existing fixtures" + } + fi + + # Log stale key request (but don't actually send) + if [[ -n "${STALE_KEY_PROVIDERS:-}" ]]; then + dry_log "request_missing_cloud_keys for stale providers: ${STALE_KEY_PROVIDERS}" + fi +fi + +# --- Track consecutive Phase 1 failures per cloud --- +FINAL_RECORD_FAILED="" +FINAL_RECORD_SUCCEEDED="" +if [[ -f "${RECORD_OUTPUT}" ]]; then + _current_cloud="" + _cloud_had_error="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi + _current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + _cloud_had_error="" + ;; + *"fail "*) + _cloud_had_error="true" + ;; + *"done "*) + ;; + esac + done < "${RECORD_OUTPUT}" + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi +fi +FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //') +FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //') + +# Log escalation info but don't create GitHub issues +if [[ -n "${FINAL_RECORD_FAILED}" ]]; then + log "Phase 1: Failed clouds (would track/escalate): ${FINAL_RECORD_FAILED}" + for cloud in ${FINAL_RECORD_FAILED}; do + dry_log "gh issue create for ${cloud} persistent recording failure" + done +fi + +rm -f "${RECORD_OUTPUT}" +check_timeout || exit 0 + +# ============================================================ +# Phase 2: Run mock tests +# ============================================================ +log "=== Phase 2: Run mock tests ===" + +rm -f "${RESULTS_PHASE2}" +MOCK_EXIT=0 +RESULTS_FILE="${RESULTS_PHASE2}" bash test/mock.sh 2>&1 | tee -a "${LOG_FILE}" || MOCK_EXIT=$? + +if [[ -f "${RESULTS_PHASE2}" ]]; then + TOTAL_TESTS=$(wc -l < "${RESULTS_PHASE2}" | tr -d ' ') + PASS_COUNT=$(grep -c ':pass$' "${RESULTS_PHASE2}" || true) + FAIL_COUNT=$(grep -c ':fail$' "${RESULTS_PHASE2}" || true) + log "Phase 2: ${PASS_COUNT} passed, ${FAIL_COUNT} failed, ${TOTAL_TESTS} total" + cp "${RESULTS_PHASE2}" "${DRY_OUTPUT_DIR}/results-phase2.txt" 2>/dev/null || true +else + log "Phase 2: No results file generated" + FAIL_COUNT=0 +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 3: Fix mock failures +# ============================================================ +log "=== Phase 3: Fix failures ===" + +if [[ "${FAIL_COUNT:-0}" -eq 0 ]]; then + log "Phase 3: No failures to fix" +else + FAILURES="" + FAILED_CLOUDS="" + if [[ -f "${RESULTS_PHASE2}" ]]; then + FAILURES=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' || true) + FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true) + fi + + # Capture full mock test output per-cloud for richer agent context + MOCK_OUTPUT_DIR="/tmp/spawn-qa-dry-mock-output" + rm -rf "${MOCK_OUTPUT_DIR}" + mkdir -p "${MOCK_OUTPUT_DIR}" + for cloud in $FAILED_CLOUDS; do + log "Phase 3: Capturing full mock test output for ${cloud}..." + bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true + done + + AGENT_PIDS="" + for cloud in $FAILED_CLOUDS; do + check_timeout || break + + cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true) + failing_scripts="" + failing_agents="" + for combo in $cloud_failures; do + agent=$(printf '%s' "$combo" | cut -d/ -f2) + script_path="${cloud}/${agent}.sh" + failing_scripts="${failing_scripts} ${script_path}" + failing_agents="${failing_agents} ${agent}" + done + failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //') + failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //') + + error_context="" + if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then + error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log") + # Also save to output dir for inspection + cp "${MOCK_OUTPUT_DIR}/${cloud}.log" "${DRY_OUTPUT_DIR}/agent-fix-${cloud}.log" 2>/dev/null || true + fi + + fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ') + log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}" + + worktree="${WORKTREE_BASE}/fix-${cloud}" + branch_name="qa-dry/fix-${cloud}" + + git worktree add "${worktree}" -b "${branch_name}" HEAD 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 3: Could not create worktree for ${cloud}, skipping" + continue + } + + dry_log "git worktree add ... -b qa/fix-${cloud} origin/main" + + # Spawn ONE Claude agent per cloud to fix all its failing scripts (15 min timeout) + ( + cd "${worktree}" + run_with_timeout 900 \ + claude -p "Fix the failing mock tests for cloud '${cloud}' in the spawn codebase. + +Failing scripts: ${failing_scripts} + +Error context from test run: +${error_context} + +## Investigation & Fix Process (be thorough): + +1. **Understand the test infrastructure:** + - Read test/mock.sh to see how mocking works (curl interception, fixture matching) + - Read ${cloud}/lib/common.sh to understand the cloud's API primitives + - Check test/fixtures/${cloud}/ to see what API responses are mocked + +2. **For EACH failing script, investigate the root cause:** + - Read the failing script (${cloud}/.sh) + - Identify which API calls are being made + - Check if the script is making API calls that aren't mocked in test/fixtures/${cloud}/ + - Look for missing fixtures, incorrect API endpoint URLs, or changed function signatures + +3. **Check the cloud provider's current API (if needed):** + - If the script seems correct but fixtures seem outdated, check the provider's API docs + - Compare fixture responses with current API documentation + - Look for API changes: new required parameters, different response formats, endpoint deprecations + +4. **Common failure patterns to check:** + - Missing test fixtures (script calls an API that has no mock response) + - Wrong API endpoint format (e.g., /v2/servers vs /servers) + - Missing authentication setup (API token not set in mock environment) + - Incorrect assumptions about SSH connectivity in mock mode + - Scripts calling commands that don't work in mock mode (ssh, scp without proper mocking) + +5. **Fix the issues:** + - Update scripts to work properly with the mock infrastructure + - Add missing fixture files if needed (test/fixtures/${cloud}/.json) + - Fix API calls to match current provider API + - Ensure proper error handling for mock environment + +6. **Test each fix incrementally:** + - After fixing each script, run: RESULTS_FILE=/tmp/fix-test.txt bash test/mock.sh ${cloud} + - Verify the specific script now passes + - Check for regressions in other scripts + +7. **Syntax check and commit:** + - Run: bash -n on each modified script + - Test final state: RESULTS_FILE=/tmp/fix-test.txt bash test/mock.sh ${cloud} + - Commit all fixes with a message listing what was fixed and why + +You can modify: scripts in ${cloud}/, test/fixtures/${cloud}/, and test/mock.sh if infrastructure updates are needed." \ + 2>&1 | tee -a "${LOG_FILE}" || true + + # Always check for changes + syntax_ok=true + for script in ${failing_scripts}; do + if [[ -f "${script}" ]] && ! bash -n "${script}" 2>/dev/null; then + log "Phase 3: Syntax check failed for ${script}" + syntax_ok=false + fi + done + + # Stage any uncommitted changes the agent left behind + if [[ "$syntax_ok" == "true" ]] && [[ -n "$(git status --porcelain)" ]]; then + git add ${failing_scripts} "${cloud}/lib/common.sh" "test/fixtures/${cloud}/" "test/mock.sh" 2>/dev/null || true + git commit -m "$(cat < +FIXEOF + )" || true + fi + + # Save diff instead of pushing + git diff HEAD~1..HEAD > "${DRY_OUTPUT_DIR}/diff-fix-${cloud}.patch" 2>/dev/null || true + dry_log "git add ${cloud}/ test/fixtures/${cloud}/ test/mock.sh && git commit && git push && gh pr create for ${cloud}" + ) & + AGENT_PIDS="${AGENT_PIDS} $!" + done + + # Wait for all fix agents to complete + for pid in $AGENT_PIDS; do + wait "$pid" 2>/dev/null || true + done + + # Clean up worktrees (one per cloud) + for cloud in $FAILED_CLOUDS; do + git worktree remove "${WORKTREE_BASE}/fix-${cloud}" 2>/dev/null || true + git branch -D "qa-dry/fix-${cloud}" 2>/dev/null || true + done + git worktree prune 2>/dev/null || true + + # Clean up per-cloud mock output + rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true + + log "Phase 3: Fix agents complete" +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 4: Re-run mock tests + update README (local only) +# ============================================================ +log "=== Phase 4: Re-run tests and update README ===" + +# No git fetch/reset — work with current local state + +rm -f "${RESULTS_PHASE4}" +RESULTS_FILE="${RESULTS_PHASE4}" bash test/mock.sh 2>&1 | tee -a "${LOG_FILE}" || true + +if [[ -f "${RESULTS_PHASE4}" ]]; then + RETRY_PASS=$(grep -c ':pass$' "${RESULTS_PHASE4}" || true) + RETRY_FAIL=$(grep -c ':fail$' "${RESULTS_PHASE4}" || true) + log "Phase 4: ${RETRY_PASS} passed, ${RETRY_FAIL} failed" + cp "${RESULTS_PHASE4}" "${DRY_OUTPUT_DIR}/results-phase4.txt" 2>/dev/null || true + + python3 test/update-readme.py "${RESULTS_PHASE4}" 2>&1 | tee -a "${LOG_FILE}" + + # Save README diff instead of pushing + if [[ -n "$(git diff --name-only README.md 2>/dev/null)" ]]; then + git diff README.md > "${DRY_OUTPUT_DIR}/diff-readme.patch" 2>/dev/null || true + log "Phase 4: README changes saved to ${DRY_OUTPUT_DIR}/diff-readme.patch" + dry_log "git checkout -b qa/readme-update-\$(date +%s) && git add README.md && git commit && git push && gh pr create" + # Revert the local README change so we don't leave dirty state + git checkout README.md 2>/dev/null || true + else + log "Phase 4: No README changes needed" + fi +else + log "Phase 4: No results file generated" +fi + +# Final summary +log "=== QA Dry Run Summary ===" +log "Phase 2: ${PASS_COUNT:-0} pass / ${FAIL_COUNT:-0} fail" +log "Phase 4: ${RETRY_PASS:-0} pass / ${RETRY_FAIL:-0} fail" +if [[ "${FAIL_COUNT:-0}" -gt 0 ]] && [[ "${RETRY_FAIL:-0}" -lt "${FAIL_COUNT:-0}" ]]; then + FIXED=$(( ${FAIL_COUNT:-0} - ${RETRY_FAIL:-0} )) + log "Fixed ${FIXED} failure(s) this cycle" +fi +log "Artifacts saved to: ${DRY_OUTPUT_DIR}/" +log "=== QA Dry Run Complete ===" diff --git a/test/record.sh b/test/record.sh index b60bec949..9f52bc90f 100644 --- a/test/record.sh +++ b/test/record.sh @@ -79,9 +79,7 @@ get_endpoints() { printf '%s\n' \ "regions:/regions" \ "instances:/instances" \ - "sshkeys:/sshkeys" \ - "networks:/networks" \ - "disk_images:/disk_images" + "ssh_keys:/sshkeys" ;; upcloud) printf '%s\n' \ @@ -167,23 +165,16 @@ try_load_config() { # OVH uses separate config with multiple fields if [[ "$cloud" == "ovh" ]]; then if [[ -f "$config_file" ]]; then - local ovh_vals - ovh_vals=$(python3 -c " + eval "$(python3 -c " import json, sys try: d = json.load(open(sys.argv[1])) - # Output tab-separated values in fixed order - print('\t'.join(d.get(k, '') for k in ['application_key', 'application_secret', 'consumer_key', 'project_id'])) -except: print('\t\t\t') -" "$config_file" 2>/dev/null) || true - if [[ -n "${ovh_vals:-}" ]]; then - local IFS=$'\t' - read -r ak as ck pid <<< "$ovh_vals" - [[ -n "${ak:-}" ]] && export OVH_APPLICATION_KEY="$ak" - [[ -n "${as:-}" ]] && export OVH_APPLICATION_SECRET="$as" - [[ -n "${ck:-}" ]] && export OVH_CONSUMER_KEY="$ck" - [[ -n "${pid:-}" ]] && export OVH_PROJECT_ID="$pid" - fi + for k, e in [('application_key','OVH_APPLICATION_KEY'), ('application_secret','OVH_APPLICATION_SECRET'), + ('consumer_key','OVH_CONSUMER_KEY'), ('project_id','OVH_PROJECT_ID')]: + v = d.get(k, '') + if v: print(f'export {e}=\"{v}\"') +except: pass +" "$config_file" 2>/dev/null)" || true fi return 0 fi @@ -233,29 +224,29 @@ save_config() { case "$cloud" in ovh) python3 -c " -import json, sys -d = {'application_key': sys.argv[1], 'application_secret': sys.argv[2], - 'consumer_key': sys.argv[3], 'project_id': sys.argv[4]} +import json +d = {'application_key': '${OVH_APPLICATION_KEY:-}', 'application_secret': '${OVH_APPLICATION_SECRET:-}', + 'consumer_key': '${OVH_CONSUMER_KEY:-}', 'project_id': '${OVH_PROJECT_ID:-}'} print(json.dumps(d, indent=2)) -" "${OVH_APPLICATION_KEY:-}" "${OVH_APPLICATION_SECRET:-}" "${OVH_CONSUMER_KEY:-}" "${OVH_PROJECT_ID:-}" > "$config_file" +" > "$config_file" ;; upcloud) python3 -c " -import json, sys -print(json.dumps({'username': sys.argv[1], 'password': sys.argv[2]}, indent=2)) -" "${UPCLOUD_USERNAME:-}" "${UPCLOUD_PASSWORD:-}" > "$config_file" +import json +print(json.dumps({'username': '${UPCLOUD_USERNAME:-}', 'password': '${UPCLOUD_PASSWORD:-}'}, indent=2)) +" > "$config_file" ;; kamatera) python3 -c " -import json, sys -print(json.dumps({'client_id': sys.argv[1], 'secret': sys.argv[2]}, indent=2)) -" "${KAMATERA_API_CLIENT_ID:-}" "${KAMATERA_API_SECRET:-}" > "$config_file" +import json +print(json.dumps({'client_id': '${KAMATERA_API_CLIENT_ID:-}', 'secret': '${KAMATERA_API_SECRET:-}'}, indent=2)) +" > "$config_file" ;; *) local env_var env_var=$(get_auth_env_var "$cloud") eval "local val=\"\${${env_var}:-}\"" - python3 -c "import json, sys; print(json.dumps({'api_key': sys.argv[1]}, indent=2))" "$val" > "$config_file" + python3 -c "import json; print(json.dumps({'api_key': '${val}'}, indent=2))" > "$config_file" ;; esac printf '%b\n' " ${GREEN}saved${NC} → ${config_file}" @@ -335,7 +326,7 @@ has_api_error() { echo "$response" | python3 -c " import json, sys d = json.loads(sys.stdin.read()) -cloud = sys.argv[1] +cloud = '$cloud' if cloud == 'hetzner': err = d.get('error') @@ -360,7 +351,7 @@ elif cloud == 'latitude': sys.exit(0 if 'error' in d or ('errors' in d and d['errors']) else 1) else: sys.exit(1) -" "$cloud" 2>/dev/null +" 2>/dev/null } # --- Pretty print JSON --- @@ -376,16 +367,13 @@ _record_live_cycle() { local cloud="$1" local fixture_dir="$2" - # Source cloud lib so API wrappers are available (dynamic scoping - # lets _live_* functions update caller's counters/metadata) - source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null || true - case "$cloud" in - hetzner) _live_hetzner "$fixture_dir" ;; - digitalocean) _live_digitalocean "$fixture_dir" ;; - vultr) _live_vultr "$fixture_dir" ;; - linode) _live_linode "$fixture_dir" ;; - civo) _live_civo "$fixture_dir" ;; + hetzner) + # Source cloud lib so API wrappers are available (dynamic scoping + # lets _live_hetzner update caller's counters/metadata) + source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null || true + _live_hetzner "$fixture_dir" + ;; *) return 0 ;; # No live cycle for this cloud yet esac } @@ -409,13 +397,6 @@ _save_live_fixture() { return 1 fi - # Check for API error responses (cloud var is available via dynamic scoping) - if has_api_error "$cloud" "$response"; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" - cloud_errors=$((cloud_errors + 1)) - return 1 - fi - echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} (live)" @@ -660,79 +641,98 @@ _live_civo() { } # --- Record one cloud --- -# Check credentials and prompt if needed; returns 1 to skip this cloud -_record_ensure_credentials() { +record_cloud() { local cloud="$1" - if has_credentials "$cloud"; then - return 0 - fi - local env_var - env_var=$(get_auth_env_var "$cloud") - if [[ "$PROMPT_FOR_CREDS" == "true" ]]; then - printf '%b\n' "${CYAN}━━━ ${cloud} ━━━${NC}" - printf '%b\n' " ${YELLOW}missing${NC} ${env_var}" - if prompt_credentials "$cloud"; then + if ! has_credentials "$cloud"; then + local env_var + env_var=$(get_auth_env_var "$cloud") + if [[ "$PROMPT_FOR_CREDS" == "true" ]]; then + printf '%b\n' "${CYAN}━━━ ${cloud} ━━━${NC}" + printf '%b\n' " ${YELLOW}missing${NC} ${env_var}" + if ! prompt_credentials "$cloud"; then + printf '%b\n' " ${YELLOW}skip${NC} ${cloud}" + SKIPPED=$((SKIPPED + 1)) + return 0 + fi + else + printf '%b\n' " ${YELLOW}skip${NC} ${cloud} — ${env_var} not set" + SKIPPED=$((SKIPPED + 1)) return 0 fi - printf '%b\n' " ${YELLOW}skip${NC} ${cloud}" - else - printf '%b\n' " ${YELLOW}skip${NC} ${cloud} — ${env_var} not set" fi - SKIPPED=$((SKIPPED + 1)) - return 1 -} -# Record a single endpoint fixture; increments cloud_recorded/cloud_errors -# Usage: _record_endpoint CLOUD FIXTURE_DIR FIXTURE_NAME ENDPOINT -_record_endpoint() { - local cloud="$1" fixture_dir="$2" fixture_name="$3" endpoint="$4" + printf '%b\n' "${CYAN}━━━ Recording ${cloud} ━━━${NC}" - # Call API in a subshell that sources the cloud lib - local tmp_response - tmp_response=$(mktemp /tmp/spawn-record-XXXXXX) + # Create fixture directory + local fixture_dir="${FIXTURES_DIR}/${cloud}" + mkdir -p "$fixture_dir" - ( - source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null - call_api "$cloud" "$endpoint" 2>/dev/null - ) > "$tmp_response" 2>/dev/null || true + # Source the cloud's lib in a subshell to avoid namespace collisions + # Capture results via temp files + local endpoints + endpoints=$(get_endpoints "$cloud") - local response - response=$(cat "$tmp_response") - rm -f "$tmp_response" + local cloud_recorded=0 + local cloud_errors=0 + local metadata_entries="" - if [[ -z "$response" ]]; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — empty response" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + while IFS=: read -r fixture_name endpoint; do + [[ -z "$fixture_name" ]] && continue - if ! echo "$response" | is_valid_json; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — invalid JSON" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + local response="" + local record_ok=false - if has_api_error "$cloud" "$response"; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + # Call API in a subshell that sources the cloud lib + local tmp_response + tmp_response=$(mktemp /tmp/spawn-record-XXXXXX) - echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" - printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} → fixtures/${cloud}/${fixture_name}.json" - cloud_recorded=$((cloud_recorded + 1)) + ( + # Source cloud lib (this also sources shared/common.sh) + source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null + + # Suppress any interactive prompts by ensuring tokens are loaded + # The API call itself uses env vars directly + call_api "$cloud" "$endpoint" 2>/dev/null + ) > "$tmp_response" 2>/dev/null || true - local timestamp - timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - metadata_entries="${metadata_entries} \"${fixture_name}\": {\"endpoint\": \"${endpoint}\", \"recorded_at\": \"${timestamp}\"}, + response=$(cat "$tmp_response") + rm -f "$tmp_response" + + if [[ -z "$response" ]]; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — empty response" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + if ! echo "$response" | is_valid_json; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — invalid JSON" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + if has_api_error "$cloud" "$response"; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + # Save pretty-printed fixture + echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" + printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} → fixtures/${cloud}/${fixture_name}.json" + cloud_recorded=$((cloud_recorded + 1)) + + # Build metadata entry + local timestamp + timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + metadata_entries="${metadata_entries} \"${fixture_name}\": {\"endpoint\": \"${endpoint}\", \"recorded_at\": \"${timestamp}\"}, " -} + done <<< "$endpoints" -# Write the _metadata.json file for a cloud's fixtures -_record_write_metadata() { - local cloud="$1" fixture_dir="$2" + # --- Live create+delete cycle for write endpoint fixtures --- + _record_live_cycle "$cloud" "$fixture_dir" cloud_recorded cloud_errors metadata_entries + # Write metadata local meta_timestamp meta_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") @@ -748,34 +748,6 @@ ${metadata_entries} } } METADATA_EOF -} - -record_cloud() { - local cloud="$1" - - _record_ensure_credentials "$cloud" || return 0 - - printf '%b\n' "${CYAN}━━━ Recording ${cloud} ━━━${NC}" - - local fixture_dir="${FIXTURES_DIR}/${cloud}" - mkdir -p "$fixture_dir" - - local endpoints - endpoints=$(get_endpoints "$cloud") - - local cloud_recorded=0 - local cloud_errors=0 - local metadata_entries="" - - while IFS=: read -r fixture_name endpoint; do - [[ -z "$fixture_name" ]] && continue - _record_endpoint "$cloud" "$fixture_dir" "$fixture_name" "$endpoint" - done <<< "$endpoints" - - # Live create+delete cycle for write endpoint fixtures - _record_live_cycle "$cloud" "$fixture_dir" cloud_recorded cloud_errors metadata_entries || true - - _record_write_metadata "$cloud" "$fixture_dir" RECORDED=$((RECORDED + cloud_recorded)) ERRORS=$((ERRORS + cloud_errors)) diff --git a/test/run.sh b/test/run.sh index 45443e739..2a475813a 100644 --- a/test/run.sh +++ b/test/run.sh @@ -38,6 +38,8 @@ NC='\033[0m' cleanup() { rm -rf "${TEST_DIR}" + rm -f /tmp/sprite_mock_created 2>/dev/null || true + find /tmp -maxdepth 1 -name 'sprite_mock_created_*' -delete 2>/dev/null || true } trap 'cleanup' EXIT @@ -136,81 +138,43 @@ assert_exit_code() { fi } -# Assert that a value equals an expected string -# Usage: assert_equals ACTUAL EXPECTED MSG -assert_equals() { - local actual="$1" expected="$2" msg="$3" - if [[ "${actual}" == "${expected}" ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg} (got '${actual}')" - FAILED=$((FAILED + 1)) - fi -} +# --- Test runner for a single script --- +run_script_test() { + local script_name="$1" + local script_path="${REPO_ROOT}/sprite/${script_name}.sh" + local output_file="${TEST_DIR}/${script_name}_output.log" -# Assert that a value contains a substring pattern (glob match) -# Usage: assert_match ACTUAL PATTERN MSG -# PATTERN uses glob syntax: *substring* for contains, prefix* for starts-with, etc. -assert_match() { - local actual="$1" pattern="$2" msg="$3" - # Use a case statement for glob matching (compatible with bash 3.x) - case "${actual}" in - ${pattern}) - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - ;; - *) - printf '%b\n' " ${RED}✗${NC} ${msg} (got '${actual}')" - FAILED=$((FAILED + 1)) - ;; - esac -} + echo "" + printf '%b\n' "${YELLOW}━━━ Testing ${script_name}.sh ━━━${NC}" -# Run a shared/common.sh function and assert it succeeds (exit 0) -assert_common_succeeds() { - local msg="$1" cmd="$2" - local result - result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && '"${cmd}" 2>/dev/null) - if [[ "${result}" == "valid" ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg}" - FAILED=$((FAILED + 1)) - fi -} + # Reset mock log and state + : > "${MOCK_LOG}" + rm -f /tmp/sprite_mock_created 2>/dev/null || true + find /tmp -maxdepth 1 -name 'sprite_mock_created_*' -delete 2>/dev/null || true -# Run a shared/common.sh function and assert it fails (exit non-zero) -assert_common_fails() { - local msg="$1" cmd="$2" - local rc=0 - bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && '"${cmd}" /dev/null 2>&1 || rc=$? - if [[ "${rc}" -ne 0 ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg}" - FAILED=$((FAILED + 1)) - fi -} + # Run the script with mocked PATH and env vars (timeout 30s) + local exit_code=0 + MOCK_LOG="${MOCK_LOG}" \ + SPRITE_NAME="test-sprite-${script_name}" \ + OPENROUTER_API_KEY="sk-or-v1-0000000000000000000000000000000000000000000000000000000000000000" \ + PATH="${TEST_DIR}:${PATH}" \ + timeout 30 bash "${script_path}" > "${output_file}" 2>&1 || exit_code=$? -# --- Sprite command assertions --- -# Assert that a sprite script follows the standard command lifecycle: -# auth check -> list -> create -> exec -> env upload -> interactive launch -_assert_sprite_common_commands() { - local script_name="$1" + assert_exit_code "${exit_code}" 0 "Script exits successfully" + + # Common assertions for all scripts assert_contains "${MOCK_LOG}" "sprite org list" "Checks sprite authentication" assert_contains "${MOCK_LOG}" "sprite list" "Checks if sprite exists" assert_contains "${MOCK_LOG}" "sprite create.*test-sprite-${script_name}" "Creates sprite with correct name" assert_contains "${MOCK_LOG}" "sprite exec.*test-sprite-${script_name}" "Runs commands on sprite" + + # Check env var injection (temp file upload) assert_contains "${MOCK_LOG}" "sprite exec.*-file.*/tmp/env_config" "Uploads env config to sprite" + + # Check final interactive launch (flag order varies: -s NAME -tty or -tty -s NAME) assert_contains "${MOCK_LOG}" "sprite exec.*-tty.*" "Launches interactive session" -} -# Assert that a sprite script installs agent-specific components -_assert_agent_specific() { - local script_name="$1" + # Script-specific assertions case "${script_name}" in claude) assert_contains "${MOCK_LOG}" "sprite exec.*claude.*install" "Installs Claude Code" @@ -225,11 +189,12 @@ _assert_agent_specific() { assert_contains "${MOCK_LOG}" "sprite exec.*git.*nanoclaw" "Clones nanoclaw repo" assert_contains "${MOCK_LOG}" "sprite exec.*-file.*/tmp/nanoclaw_env" "Uploads nanoclaw .env" ;; + *) + # No agent-specific assertions for other agents + ;; esac -} -# Assert no temp files were leaked during script execution -_assert_no_temp_leaks() { + # Check no temp files leaked local leaked_temps leaked_temps=$(find /tmp -maxdepth 1 -name "tmp.*" -newer "${MOCK_LOG}" 2>/dev/null | wc -l) if [[ "${leaked_temps}" -eq 0 ]]; then @@ -238,36 +203,12 @@ _assert_no_temp_leaks() { fi } -# --- Test runner for a single script --- -run_script_test() { - local script_name="$1" - local script_path="${REPO_ROOT}/sprite/${script_name}.sh" - local output_file="${TEST_DIR}/${script_name}_output.log" - +# --- Test common.sh sourcing --- +test_common_source() { echo "" - printf '%b\n' "${YELLOW}━━━ Testing ${script_name}.sh ━━━${NC}" - - # Reset mock state - : > "${MOCK_LOG}" - rm -f /tmp/sprite_mock_created_* /tmp/sprite_mock_created 2>/dev/null || true - - # Run the script with mocked PATH and env vars (timeout 30s) - local exit_code=0 - MOCK_LOG="${MOCK_LOG}" \ - SPRITE_NAME="test-sprite-${script_name}" \ - OPENROUTER_API_KEY="sk-or-v1-0000000000000000000000000000000000000000000000000000000000000000" \ - PATH="${TEST_DIR}:${PATH}" \ - timeout 30 bash "${script_path}" > "${output_file}" 2>&1 || exit_code=$? - - assert_exit_code "${exit_code}" 0 "Script exits successfully" - _assert_sprite_common_commands "${script_name}" - _assert_agent_specific "${script_name}" - _assert_no_temp_leaks -} + printf '%b\n' "${YELLOW}━━━ Testing common.sh ━━━${NC}" -# --- Test common.sh sourcing --- -_test_sprite_functions_and_syntax() { - # Source locally and check all functions exist + # Test 1: Source locally and check all functions exist local output output=$(bash -c ' source "'"${REPO_ROOT}"'/sprite/lib/common.sh" @@ -283,126 +224,167 @@ _test_sprite_functions_and_syntax() { local missing missing=$(echo "${output}" | grep "^MISSING:" || true) - assert_equals "${missing}" "" "All functions defined" - - # Syntax check - local rc=0 - bash -n "${REPO_ROOT}/sprite/lib/common.sh" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "Syntax valid" -} + if [[ -z "${missing}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} All functions defined" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} Missing functions: ${missing}" + FAILED=$((FAILED + 1)) + fi -_test_sprite_log_and_name() { - # log functions write to stderr, not stdout + # Test 2: log functions write to stderr, not stdout local stdout stderr stdout=$(timeout 5 bash -c 'source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && log_info "test"' /dev/null) stderr=$(timeout 5 bash -c 'source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && log_info "test"' &1 >/dev/null) - assert_equals "${stdout}" "" "Log functions write to stderr (no stdout)" - assert_match "${stderr}" "?*" "Log functions produce stderr output" + if [[ -z "${stdout}" && -n "${stderr}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} Log functions write to stderr" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} Log functions should write to stderr only" + FAILED=$((FAILED + 1)) + fi - # get_sprite_name uses SPRITE_NAME env var + # Test 3: get_sprite_name uses SPRITE_NAME env var local name name=$(timeout 5 bash -c 'SPRITE_NAME=from-env; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' 2>/dev/null) - assert_equals "${name}" "from-env" "get_sprite_name reads SPRITE_NAME env var" + if [[ "${name}" == "from-env" ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_sprite_name reads SPRITE_NAME env var" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} get_sprite_name should return 'from-env', got '${name}'" + FAILED=$((FAILED + 1)) + fi - # get_sprite_name fails gracefully without TTY or env var + # Test 4: get_sprite_name fails gracefully without TTY or env var local rc=0 timeout 5 bash -c 'SPRITE_NAME=""; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' /dev/null 2>&1 || rc=$? - assert_match "${rc}" "[1-9]*" "get_sprite_name fails without TTY or env var" -} - -_test_sprite_remote_source() { - if [[ "${REMOTE}" != true ]]; then - return 0 + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_sprite_name fails without TTY or env var" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} get_sprite_name should fail without input" + FAILED=$((FAILED + 1)) fi - local remote_fns - remote_fns=$(bash -c ' - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/lib/common.sh)" - type log_info &>/dev/null && echo "OK" || echo "FAIL" - ' 2>/dev/null) - assert_equals "${remote_fns}" "OK" "Remote source from GitHub works" -} -test_common_source() { - echo "" - printf '%b\n' "${YELLOW}━━━ Testing common.sh ━━━${NC}" + # Test 5: Syntax check + if bash -n "${REPO_ROOT}/sprite/lib/common.sh" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} Syntax valid" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} Syntax errors" + FAILED=$((FAILED + 1)) + fi - _test_sprite_functions_and_syntax - _test_sprite_log_and_name - _test_sprite_remote_source + # Test 6: Remote source (if --remote) + if [[ "${REMOTE}" == true ]]; then + local remote_fns + remote_fns=$(bash -c ' + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/lib/common.sh) + type log_info &>/dev/null && echo "OK" || echo "FAIL" + ' 2>/dev/null) + if [[ "${remote_fns}" == "OK" ]]; then + printf '%b\n' " ${GREEN}✓${NC} Remote source from GitHub works" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} Remote source from GitHub failed" + FAILED=$((FAILED + 1)) + fi + fi } # --- Test shared/common.sh functions --- -# --- shared/common.sh sub-tests (grouped by feature) --- - -_test_model_validation() { - assert_common_succeeds "validate_model_id accepts valid model IDs" \ - 'validate_model_id "anthropic/claude-3.5-sonnet" && echo "valid"' - assert_common_fails "validate_model_id rejects invalid characters" \ - 'validate_model_id "bad;model"' - assert_common_succeeds "validate_model_id accepts empty string" \ - 'validate_model_id "" && echo "valid"' - assert_common_succeeds "validate_model_id accepts openrouter/auto" \ - 'validate_model_id "openrouter/auto" && echo "valid"' - assert_common_succeeds "validate_model_id accepts model IDs with colons" \ - 'validate_model_id "provider/model:version" && echo "valid"' - - # Bulk test: all shell metacharacters must be rejected - # Note: backtick excluded due to shell escaping complexity - local dangerous_chars=('$' '&' '|' '>' '<' '(' ')' '{' '}' ';' '*' '?' '[' ']') - local rejected_count=0 - local rc - for char in "${dangerous_chars[@]}"; do - rc=0 - local test_str - test_str=$(printf 'bad%smodel' "${char}") - bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id '"$(printf '%q' "${test_str}")" /dev/null 2>&1 || rc=$? - [[ "${rc}" -ne 0 ]] && rejected_count=$((rejected_count + 1)) - done - assert_equals "${rejected_count}" "${#dangerous_chars[@]}" \ - "validate_model_id rejects shell metacharacters (${rejected_count}/${#dangerous_chars[@]})" -} +test_shared_common() { + echo "" + printf '%b\n' "${YELLOW}━━━ Testing shared/common.sh ━━━${NC}" -_test_json_escape() { + # Test 1: validate_model_id accepts valid model IDs local result + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "anthropic/claude-3.5-sonnet" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts valid model IDs" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'anthropic/claude-3.5-sonnet'" + FAILED=$((FAILED + 1)) + fi + + # Test 2: validate_model_id rejects invalid characters + local rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "bad;model"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects invalid characters" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should reject 'bad;model'" + FAILED=$((FAILED + 1)) + fi + + # Test 3: validate_model_id accepts empty string + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts empty string" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept empty string" + FAILED=$((FAILED + 1)) + fi + + # Test 4: json_escape handles special characters result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && json_escape "test\"quote"' 2>/dev/null) - # json_escape should produce escaped quotes (\\") in the output - assert_match "${result}" '*\\"*' "json_escape handles special characters" -} + if [[ "${result}" == *'\\"'* ]] || [[ "${result}" == *'\"'* ]]; then + printf '%b\n' " ${GREEN}✓${NC} json_escape handles special characters" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} json_escape should escape quotes" + FAILED=$((FAILED + 1)) + fi -_test_ssh_key_utils() { - # generate_ssh_key_if_missing - creates key + # Test 5: generate_ssh_key_if_missing creates key if missing local test_key="${TEST_DIR}/test_id_ed25519" bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && generate_ssh_key_if_missing "'"${test_key}"'"' >/dev/null 2>&1 - local key_exists="no" - [[ -f "${test_key}" && -f "${test_key}.pub" ]] && key_exists="yes" - assert_equals "${key_exists}" "yes" "generate_ssh_key_if_missing creates key" + if [[ -f "${test_key}" && -f "${test_key}.pub" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing creates key" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should create key at ${test_key}" + FAILED=$((FAILED + 1)) + fi - # generate_ssh_key_if_missing - skips existing + # Test 6: generate_ssh_key_if_missing skips if key exists local mtime_before mtime_before=$(stat -c %Y "${test_key}" 2>/dev/null || stat -f %m "${test_key}" 2>/dev/null) sleep 1 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && generate_ssh_key_if_missing "'"${test_key}"'"' >/dev/null 2>&1 local mtime_after mtime_after=$(stat -c %Y "${test_key}" 2>/dev/null || stat -f %m "${test_key}" 2>/dev/null) - assert_equals "${mtime_before}" "${mtime_after}" "generate_ssh_key_if_missing skips existing key" + if [[ "${mtime_before}" == "${mtime_after}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing skips existing key" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should not recreate existing key" + FAILED=$((FAILED + 1)) + fi - # get_ssh_fingerprint - local result + # Test 7: get_ssh_fingerprint returns fingerprint result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_ssh_fingerprint "'"${test_key}.pub"'"' 2>/dev/null) - assert_match "${result}" "*:*" "get_ssh_fingerprint returns valid fingerprint" - - # extract_ssh_key_ids - local mock_json='{"ssh_keys":[{"id":123},{"id":456}]}' - result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && echo '"'${mock_json}'"' | extract_ssh_key_ids "$(cat)" "ssh_keys"' 2>/dev/null) - assert_match "${result}" "*123*456*" "extract_ssh_key_ids parses JSON correctly" -} + if [[ -n "${result}" && "${result}" =~ ^[a-f0-9:]+$ ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_ssh_fingerprint returns valid fingerprint" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} get_ssh_fingerprint should return hex fingerprint, got '${result}'" + FAILED=$((FAILED + 1)) + fi -_test_syntax_and_logging() { - local rc=0 - bash -n "${REPO_ROOT}/shared/common.sh" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "shared/common.sh syntax valid" + # Test 8: Syntax check for shared/common.sh + if bash -n "${REPO_ROOT}/shared/common.sh" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} shared/common.sh syntax valid" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} shared/common.sh has syntax errors" + FAILED=$((FAILED + 1)) + fi - local output missing + # Test 9: All logging functions exist in shared/common.sh output=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" for fn in log_info log_warn log_error; do @@ -410,34 +392,62 @@ _test_syntax_and_logging() { done ' 2>/dev/null) missing=$(echo "${output}" | grep "^MISSING:" || true) - assert_equals "${missing}" "" "All logging functions exist in shared/common.sh" -} + if [[ -z "${missing}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} All logging functions exist in shared/common.sh" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} Missing logging functions: ${missing}" + FAILED=$((FAILED + 1)) + fi -_test_open_browser() { - # open_browser: termux - local result + # Test 10: extract_ssh_key_ids parses JSON correctly + local mock_json='{"ssh_keys":[{"id":123},{"id":456}]}' + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && echo '"'${mock_json}'"' | extract_ssh_key_ids "$(cat)" "ssh_keys"' 2>/dev/null) + if [[ "${result}" == "[123, 456]" ]] || [[ "${result}" == "[123,456]" ]]; then + printf '%b\n' " ${GREEN}✓${NC} extract_ssh_key_ids parses JSON correctly" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} extract_ssh_key_ids should return [123, 456], got '${result}'" + FAILED=$((FAILED + 1)) + fi + + # Test 11: open_browser detects termux-open-url result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" termux-open-url() { echo "termux: $*"; } export -f termux-open-url open_browser "https://example.com" ' 2>/dev/null) - assert_equals "${result}" "termux: https://example.com" "open_browser detects termux-open-url" + if [[ "${result}" == "termux: https://example.com" ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser detects termux-open-url" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should use termux-open-url, got '${result}'" + FAILED=$((FAILED + 1)) + fi - # open_browser: macOS open + # Test 14: open_browser detects macOS open result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" open() { echo "macOS: $*"; } export -f open open_browser "https://example.com" ' 2>/dev/null) - assert_equals "${result}" "macOS: https://example.com" "open_browser detects macOS open" + if [[ "${result}" == "macOS: https://example.com" ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser detects macOS open" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should use macOS open, got '${result}'" + FAILED=$((FAILED + 1)) + fi - # open_browser: fallback message + # Test 15: open_browser handles missing browsers gracefully local stderr_output stderr_output=$(bash -c ' + # Use minimal PATH to force fallback behavior PATH="/usr/bin:/bin" source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock all browser detection to fail command() { if [[ "$2" == "termux-open-url" || "$2" == "open" || "$2" == "xdg-open" ]]; then return 1 @@ -447,118 +457,256 @@ _test_open_browser() { export -f command open_browser "https://example.com" ' 2>&1 >/dev/null) - assert_match "${stderr_output}" "*Please open: https://example.com*" \ - "open_browser shows fallback message when browsers unavailable" -} + if [[ "${stderr_output}" == *"Please open: https://example.com"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser shows fallback message when browsers unavailable" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should show fallback message, got '${stderr_output}'" + FAILED=$((FAILED + 1)) + fi -_test_cloud_init() { - # get_cloud_init_userdata - local result + # Test 16: get_cloud_init_userdata returns valid YAML result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_cloud_init_userdata' 2>/dev/null) - assert_match "${result}" "*#cloud-config*" "get_cloud_init_userdata returns valid YAML" - assert_match "${result}" "*curl*" "get_cloud_init_userdata includes curl" - assert_match "${result}" "*git*" "get_cloud_init_userdata includes git" - assert_match "${result}" "*zsh*" "get_cloud_init_userdata includes zsh" - assert_match "${result}" "*bun.sh/install*" "get_cloud_init_userdata includes Bun installation" - assert_match "${result}" "*claude.ai/install*" "get_cloud_init_userdata includes Claude installation" - - # check_openrouter_connectivity -- accepts success or graceful failure - if command -v curl &> /dev/null; then - local connectivity_result - connectivity_result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && check_openrouter_connectivity && echo "reachable"' 2>/dev/null) - # Accept both "reachable" and empty (network unavailable) -- just shouldn't crash - assert_match "${connectivity_result:-ok}" "*" "check_openrouter_connectivity handles connectivity check" + if [[ "${result}" == *"#cloud-config"* ]] && [[ "${result}" == *"package_update"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata returns valid YAML" + PASSED=$((PASSED + 1)) else - printf '%b\n' " ${YELLOW}⚠${NC} check_openrouter_connectivity test skipped (curl not available)" + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should return cloud-init YAML" + FAILED=$((FAILED + 1)) fi -} -_test_oauth_functions() { - local rc + # Test 17: get_cloud_init_userdata includes required packages + if [[ "${result}" == *"curl"* ]] && [[ "${result}" == *"git"* ]] && [[ "${result}" == *"zsh"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes required packages" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include curl, git, zsh" + FAILED=$((FAILED + 1)) + fi + + # Test 18: get_cloud_init_userdata includes Bun and Claude installation + if [[ "${result}" == *"bun.sh/install"* ]] && [[ "${result}" == *"claude.ai/install"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes Bun and Claude installation" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include Bun and Claude install" + FAILED=$((FAILED + 1)) + fi - # wait_for_oauth_code - success + # Test 19: wait_for_oauth_code returns success when file exists local code_test_file="${TEST_DIR}/oauth_code_test" echo "test_code" > "${code_test_file}" rc=0 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${code_test_file}"'" 1' >/dev/null 2>&1 || rc=$? - assert_exit_code "${rc}" 0 "wait_for_oauth_code returns success when file exists" + if [[ "${rc}" -eq 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns success when file exists" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return 0 when file exists" + FAILED=$((FAILED + 1)) + fi - # wait_for_oauth_code - timeout + # Test 21: wait_for_oauth_code returns failure on timeout local missing_file="${TEST_DIR}/missing_oauth_code" rc=0 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${missing_file}"'" 1' >/dev/null 2>&1 || rc=$? - assert_match "${rc}" "[1-9]*" "wait_for_oauth_code returns failure on timeout" + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns failure on timeout" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return non-zero on timeout" + FAILED=$((FAILED + 1)) + fi - # cleanup_oauth_session + # Test 22: cleanup_oauth_session removes directory local cleanup_test_dir="${TEST_DIR}/oauth_cleanup_test" mkdir -p "${cleanup_test_dir}" bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && cleanup_oauth_session "" "'"${cleanup_test_dir}"'"' >/dev/null 2>&1 - local dir_removed="yes" - [[ -d "${cleanup_test_dir}" ]] && dir_removed="no" - assert_equals "${dir_removed}" "yes" "cleanup_oauth_session removes directory" -} + if [[ ! -d "${cleanup_test_dir}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} cleanup_oauth_session removes directory" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} cleanup_oauth_session should remove directory" + FAILED=$((FAILED + 1)) + fi -_test_ssh_wait() { - # generic_ssh_wait - success - local result + # Test 23: generic_ssh_wait succeeds when command passes result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock ssh that always succeeds ssh() { return 0; } export -f ssh generic_ssh_wait "root" "1.2.3.4" "-o Test" "true" "test" 2 1 2>&1 echo $? ' 2>/dev/null | tail -1) - assert_equals "${result}" "0" "generic_ssh_wait succeeds when command passes" + if [[ "${result}" == "0" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait succeeds when command passes" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 0 on success, got '${result}'" + FAILED=$((FAILED + 1)) + fi - # generic_ssh_wait - failure + # Test 24: generic_ssh_wait fails after max attempts result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock ssh that always fails ssh() { return 1; } export -f ssh generic_ssh_wait "root" "1.2.3.4" "-o Test" "false" "test" 2 1 2>&1 echo $? ' 2>/dev/null | tail -1) - assert_equals "${result}" "1" "generic_ssh_wait fails after max attempts" -} + if [[ "${result}" == "1" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait fails after max attempts" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 1 after max attempts, got '${result}'" + FAILED=$((FAILED + 1)) + fi + + # Test 25: safe_read fails when no TTY available + rc=0 + timeout 5 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && safe_read "test: "' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} safe_read fails when no TTY available" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} safe_read should fail without TTY" + FAILED=$((FAILED + 1)) + fi + + # Test 26: validate_model_id accepts openrouter/auto + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "openrouter/auto" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts openrouter/auto" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'openrouter/auto'" + FAILED=$((FAILED + 1)) + fi + + # Test 27: validate_model_id accepts model IDs with colons + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "provider/model:version" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts model IDs with colons" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept colons in model IDs" + FAILED=$((FAILED + 1)) + fi + + # Test 28: validate_model_id rejects shell metacharacters + # Note: backtick excluded due to shell escaping complexity + local dangerous_chars=('$' '&' '|' '>' '<' '(' ')' '{' '}' ';' '*' '?' '[' ']') + local rejected_count=0 + for char in "${dangerous_chars[@]}"; do + rc=0 + # Use printf %q to properly escape the character + local test_str + test_str=$(printf 'bad%smodel' "${char}") + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id '"$(printf '%q' "${test_str}")" /dev/null 2>&1 || rc=$? + [[ "${rc}" -ne 0 ]] && rejected_count=$((rejected_count + 1)) + done + if [[ "${rejected_count}" -eq "${#dangerous_chars[@]}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects shell metacharacters" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should reject all shell metacharacters (${rejected_count}/${#dangerous_chars[@]})" + FAILED=$((FAILED + 1)) + fi -_test_input_and_server_validation() { - # safe_read without TTY - assert_common_fails "safe_read fails when no TTY available" \ - 'safe_read "test: " /dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name accepts valid names" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should accept 'dev-server-01'" + FAILED=$((FAILED + 1)) + fi - # validate_server_name - assert_common_succeeds "validate_server_name accepts valid names" \ - 'validate_server_name "dev-server-01" && echo "valid"' - assert_common_fails "validate_server_name rejects names too short" \ - 'validate_server_name "ab"' + # Test 30: validate_server_name rejects names too short + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "ab"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too short" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names < 3 characters" + FAILED=$((FAILED + 1)) + fi + # Test 31: validate_server_name rejects names too long + rc=0 local long_name long_name=$(printf 'a%.0s' {1..64}) - assert_common_fails "validate_server_name rejects names too long" \ - 'validate_server_name "'"${long_name}"'"' - assert_common_fails "validate_server_name rejects leading dash" \ - 'validate_server_name "-server"' - assert_common_fails "validate_server_name rejects trailing dash" \ - 'validate_server_name "server-"' - assert_common_fails "validate_server_name rejects invalid characters" \ - 'validate_server_name "server_01"' - assert_common_fails "validate_server_name rejects empty string" \ - 'validate_server_name ""' -} + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "'"${long_name}"'"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too long" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names > 63 characters" + FAILED=$((FAILED + 1)) + fi -test_shared_common() { - echo "" - printf '%b\n' "${YELLOW}━━━ Testing shared/common.sh ━━━${NC}" + # Test 32: validate_server_name rejects leading dash + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "-server"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects leading dash" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names starting with dash" + FAILED=$((FAILED + 1)) + fi + + # Test 33: validate_server_name rejects trailing dash + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server-"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects trailing dash" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names ending with dash" + FAILED=$((FAILED + 1)) + fi + + # Test 34: validate_server_name rejects invalid characters + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server_01"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects invalid characters" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject underscore and special characters" + FAILED=$((FAILED + 1)) + fi + + # Test 35: validate_server_name rejects empty string + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name ""' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects empty string" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject empty string" + FAILED=$((FAILED + 1)) + fi - _test_model_validation - _test_json_escape - _test_ssh_key_utils - _test_syntax_and_logging - _test_open_browser - _test_cloud_init - _test_oauth_functions - _test_ssh_wait - _test_input_and_server_validation + # Test 36: check_openrouter_connectivity succeeds with real network + # Only run if curl is available and we have network access + if command -v curl &> /dev/null; then + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && check_openrouter_connectivity && echo "reachable"' 2>/dev/null) + if [[ "${result}" == "reachable" ]] || [[ -z "${result}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} check_openrouter_connectivity handles connectivity check" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} check_openrouter_connectivity should return success or failure gracefully" + FAILED=$((FAILED + 1)) + fi + else + printf '%b\n' " ${YELLOW}⚠${NC} check_openrouter_connectivity test skipped (curl not available)" + fi } # --- Test source detection in each script --- @@ -571,47 +719,61 @@ test_source_detection() { [[ -f "${script_path}" ]] || continue # Verify the source block checks for local file existence - assert_contains "${script_path}" 'if \[\[ -f "\$\{SCRIPT_DIR\}/lib/common.sh" \]\]' \ - "${script}.sh uses file-existence check for sourcing" + if grep -q 'if \[\[ -f "${SCRIPT_DIR}/lib/common.sh" \]\]' "${script_path}"; then + printf '%b\n' " ${GREEN}✓${NC} ${script}.sh uses file-existence check for sourcing" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} ${script}.sh missing file-existence source check" + FAILED=$((FAILED + 1)) + fi # Verify syntax - local rc=0 - bash -n "${script_path}" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "${script}.sh syntax valid" + if bash -n "${script_path}" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} ${script}.sh syntax valid" + PASSED=$((PASSED + 1)) + else + printf '%b\n' " ${RED}✗${NC} ${script}.sh syntax error" + FAILED=$((FAILED + 1)) + fi done } # --- Static analysis with shellcheck --- +run_shellcheck() { + echo "" + printf '%b\n' "${YELLOW}━━━ Running shellcheck (static analysis) ━━━${NC}" -# Discover all shell scripts in the repo: agent scripts, lib files, shared, and test harness. -# Populates the DISCOVERED_SCRIPTS array. -_discover_shell_scripts() { - DISCOVERED_SCRIPTS=() - local dir - for dir in "${REPO_ROOT}"/*/; do - local cloud - cloud=$(basename "${dir}") - case "${cloud}" in - cli|shared|test|node_modules|.git|.github|.claude|.docs) continue ;; - esac - local f - for f in "${dir}"*.sh; do - [[ -f "${f}" ]] && DISCOVERED_SCRIPTS+=("${f}") - done - [[ -f "${dir}lib/common.sh" ]] && DISCOVERED_SCRIPTS+=("${dir}lib/common.sh") - done - DISCOVERED_SCRIPTS+=("${REPO_ROOT}/shared/common.sh" "${REPO_ROOT}/test/run.sh") -} + # Check if shellcheck is available + if ! command -v shellcheck &> /dev/null; then + printf '%b\n' " ${YELLOW}⚠${NC} shellcheck not found (install with: apt install shellcheck / brew install shellcheck)" + printf '%b\n' " ${YELLOW}⚠${NC} Skipping static analysis" + return 0 + fi + + # Find all shell scripts + local all_scripts=( + "${REPO_ROOT}"/sprite/*.sh + "${REPO_ROOT}"/sprite/lib/common.sh + "${REPO_ROOT}"/shared/common.sh + "${REPO_ROOT}"/digitalocean/*.sh + "${REPO_ROOT}"/digitalocean/lib/common.sh + "${REPO_ROOT}"/hetzner/*.sh + "${REPO_ROOT}"/hetzner/lib/common.sh + "${REPO_ROOT}"/linode/*.sh + "${REPO_ROOT}"/linode/lib/common.sh + "${REPO_ROOT}"/vultr/*.sh + "${REPO_ROOT}"/vultr/lib/common.sh + "${REPO_ROOT}"/test/run.sh + ) -# Run shellcheck on each discovered script and report results. -_run_shellcheck_on_scripts() { local issue_count=0 local checked_count=0 - for script in "${DISCOVERED_SCRIPTS[@]}"; do + for script in "${all_scripts[@]}"; do [[ -f "${script}" ]] || continue checked_count=$((checked_count + 1)) + # Run shellcheck with warning severity, exclude some noisy checks # SC1090: Can't follow non-constant source # SC2312: Consider invoking this command separately to avoid masking its return value local output @@ -629,23 +791,10 @@ _run_shellcheck_on_scripts() { PASSED=$((PASSED + 1)) else printf '%b\n' " ${YELLOW}⚠${NC} Found issues in ${issue_count}/${checked_count} scripts (advisory only)" + # Don't fail the build, just warn fi } -run_shellcheck() { - echo "" - printf '%b\n' "${YELLOW}━━━ Running shellcheck (static analysis) ━━━${NC}" - - if ! command -v shellcheck &> /dev/null; then - printf '%b\n' " ${YELLOW}⚠${NC} shellcheck not found (install with: apt install shellcheck / brew install shellcheck)" - printf '%b\n' " ${YELLOW}⚠${NC} Skipping static analysis" - return 0 - fi - - _discover_shell_scripts - _run_shellcheck_on_scripts -} - # --- Main --- echo "===============================" echo " Spawn Script Test Suite" diff --git a/vastai/claude.sh b/vastai/claude.sh new file mode 100644 index 000000000..ad9d11d0f --- /dev/null +++ b/vastai/claude.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=vastai/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/vastai/lib/common.sh)" +fi + +log_info "Claude Code on Vast.ai" +echo "" + +# 1. Ensure vastai CLI and API key are configured +ensure_vastai_cli +ensure_vastai_token + +# 2. Get instance name and create instance +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 3. Wait for SSH connectivity and install base tools +verify_server_connectivity +install_base_tools + +# 4. Verify Claude Code is installed (fallback to manual install) +log_step "Verifying Claude Code installation..." +if ! run_server "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then + log_step "Claude Code not found, installing manually..." + run_server "${VASTAI_INSTANCE_ID}" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +# 5. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 6. Inject environment variables +log_step "Setting up environment variables..." +inject_env_vars_ssh "${VASTAI_INSTANCE_ID}" 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" + +# 7. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${VASTAI_INSTANCE_ID}" \ + "run_server ${VASTAI_INSTANCE_ID}" + +echo "" +log_info "Vast.ai instance setup completed successfully!" +log_info "Instance: ${SERVER_NAME} (ID: ${VASTAI_INSTANCE_ID})" +echo "" + +# 8. Start Claude Code interactively +log_step "Starting Claude Code..." +sleep 1 +clear +interactive_session "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude"