From 52ed7dcfbc9490a3bd8b02b21c18678e2b1625d1 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:28:18 -0800 Subject: [PATCH] refactor: extract generic_wait_for_instance to reduce duplication across 7 clouds (#415) Seven cloud providers had nearly identical instance status polling loops (20-36 lines each). Extract the shared pattern into generic_wait_for_instance() in shared/common.sh and replace the duplicated loops with one-liner calls. Clouds refactored: Civo, Contabo, DigitalOcean, GenesisCloud, Linode, UpCloud, Vultr Net reduction: ~99 lines (-185/+86) Agent: complexity-hunter Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 --- civo/lib/common.sh | 30 +++---------------- contabo/lib/common.sh | 33 +++------------------ digitalocean/lib/common.sh | 34 +++------------------- genesiscloud/lib/common.sh | 27 ++--------------- linode/lib/common.sh | 21 ++------------ shared/common.sh | 59 ++++++++++++++++++++++++++++++++++++++ upcloud/lib/common.sh | 37 +++--------------------- vultr/lib/common.sh | 30 ++++--------------- 8 files changed, 86 insertions(+), 185 deletions(-) diff --git a/civo/lib/common.sh b/civo/lib/common.sh index 7aee7090..1d9272bc 100644 --- a/civo/lib/common.sh +++ b/civo/lib/common.sh @@ -236,32 +236,10 @@ print(json.dumps(body)) wait_for_civo_instance() { local server_id="$1" local max_attempts=${2:-60} - - log_warn "Waiting for instance to become active..." - local attempt=1 - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - local region="${CIVO_REGION:-lon1}" - status_response=$(civo_api GET "/instances/$server_id?region=$region") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('status',''))") - - if [[ "$status" == "ACTIVE" ]]; then - CIVO_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('public_ip',''))") - export CIVO_SERVER_IP - if [[ -n "$CIVO_SERVER_IP" ]]; then - log_info "Instance active: IP=$CIVO_SERVER_IP" - return 0 - fi - fi - - log_warn "Instance status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Instance did not become active in time" - return 1 + local region="${CIVO_REGION:-lon1}" + generic_wait_for_instance civo_api "/instances/${server_id}?region=${region}" \ + "ACTIVE" "d.get('status','')" "d.get('public_ip','')" \ + CIVO_SERVER_IP "Instance" "${max_attempts}" } # Handle Civo instance creation API error response diff --git a/contabo/lib/common.sh b/contabo/lib/common.sh index fac0718c..33c84926 100644 --- a/contabo/lib/common.sh +++ b/contabo/lib/common.sh @@ -280,35 +280,10 @@ print(json.dumps(body)) # Sets CONTABO_SERVER_IP on success _contabo_wait_for_instance() { local instance_id="$1" - local max_attempts=60 - local attempt=1 - - while [[ $attempt -le $max_attempts ]]; do - sleep 5 - local instance_info - instance_info=$(contabo_api GET "/compute/instances/$instance_id") - - local status - status=$(echo "$instance_info" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('data',[{}])[0].get('status',''))") - - if [[ "$status" == "running" ]]; then - CONTABO_SERVER_IP=$(echo "$instance_info" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()).get('data',[{}])[0] -ip = data.get('ipConfig', {}).get('v4', {}).get('ip', '') -print(ip) -") - export CONTABO_SERVER_IP - log_info "Instance running with IP: $CONTABO_SERVER_IP" - return 0 - fi - - log_info "Instance status: $status (attempt $attempt/$max_attempts)" - attempt=$((attempt + 1)) - done - - log_error "Instance failed to reach running state within timeout" - return 1 + generic_wait_for_instance contabo_api "/compute/instances/${instance_id}" \ + "running" "d.get('data',[{}])[0].get('status','')" \ + "d.get('data',[{}])[0].get('ipConfig',{}).get('v4',{}).get('ip','')" \ + CONTABO_SERVER_IP "Instance" 60 } # Create a Contabo instance with cloud-init diff --git a/digitalocean/lib/common.sh b/digitalocean/lib/common.sh index d831796f..984c98f9 100755 --- a/digitalocean/lib/common.sh +++ b/digitalocean/lib/common.sh @@ -149,36 +149,10 @@ print(json.dumps(body)) _wait_for_droplet_active() { local droplet_id="$1" local max_attempts="${2:-60}" - local attempt=1 - - log_warn "Waiting for droplet to become active..." - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(do_api GET "/droplets/$droplet_id") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['droplet']['status'])") - - if [[ "$status" == "active" ]]; then - DO_SERVER_IP=$(echo "$status_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -for net in data['droplet']['networks']['v4']: - if net['type'] == 'public': - print(net['ip_address']) - break -") - export DO_SERVER_IP - log_info "Droplet active: IP=$DO_SERVER_IP" - return 0 - fi - - log_warn "Droplet status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Droplet did not become active in time" - return 1 + generic_wait_for_instance do_api "/droplets/${droplet_id}" \ + "active" "d['droplet']['status']" \ + "next(n['ip_address'] for n in d['droplet']['networks']['v4'] if n['type']=='public')" \ + DO_SERVER_IP "Droplet" "${max_attempts}" } # Create a DigitalOcean droplet with cloud-init diff --git a/genesiscloud/lib/common.sh b/genesiscloud/lib/common.sh index cf6bc048..05cb799e 100644 --- a/genesiscloud/lib/common.sh +++ b/genesiscloud/lib/common.sh @@ -151,30 +151,9 @@ print(json.dumps(body)) # Sets GENESIS_SERVER_IP on success _genesis_wait_for_instance() { local server_id="$1" - local max_attempts=60 - local attempt=1 - - log_warn "Waiting for instance to become active..." - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(genesis_api GET "/instances/$server_id") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['status'])") - - if [[ "$status" == "active" ]]; then - GENESIS_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['public_ip'])") - export GENESIS_SERVER_IP - log_info "Instance active: IP=$GENESIS_SERVER_IP" - return 0 - fi - - log_warn "Instance status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Instance did not become active in time" - return 1 + generic_wait_for_instance genesis_api "/instances/${server_id}" \ + "active" "d['instance']['status']" "d['instance']['public_ip']" \ + GENESIS_SERVER_IP "Instance" 60 } create_server() { diff --git a/linode/lib/common.sh b/linode/lib/common.sh index be4ad593..2c02352b 100644 --- a/linode/lib/common.sh +++ b/linode/lib/common.sh @@ -158,24 +158,9 @@ print(json.dumps(body)) # Poll Linode API until instance is running, sets LINODE_SERVER_IP _linode_wait_for_active() { local server_id="$1" - log_warn "Waiting for Linode to become active..." - local max_attempts=60 attempt=1 - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(linode_api GET "/linode/instances/$server_id") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['status'])") - - if [[ "$status" == "running" ]]; then - LINODE_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['ipv4'][0])") - export LINODE_SERVER_IP - log_info "Linode active: IP=$LINODE_SERVER_IP" - return 0 - fi - log_warn "Linode status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}"; attempt=$((attempt + 1)) - done - log_error "Linode did not become active in time"; return 1 + generic_wait_for_instance linode_api "/linode/instances/${server_id}" \ + "running" "d['status']" "d['ipv4'][0]" \ + LINODE_SERVER_IP "Linode" 60 } create_server() { diff --git a/shared/common.sh b/shared/common.sh index 5d53ca32..388efb83 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -1371,6 +1371,65 @@ wait_for_cloud_init() { generic_ssh_wait "root" "${ip}" "${SSH_OPTS}" "test -f /root/.cloud-init-complete" "cloud-init" "${max_attempts}" 5 } +# Generic instance status polling loop +# Polls an API endpoint until the instance reaches the target status, then extracts the IP. +# Usage: generic_wait_for_instance API_FUNC ENDPOINT TARGET_STATUS STATUS_PY IP_PY IP_VAR DESCRIPTION [MAX_ATTEMPTS] +# +# Arguments: +# API_FUNC - Cloud API function name (e.g., "vultr_api", "do_api") +# ENDPOINT - API endpoint path (e.g., "/instances/$id") +# TARGET_STATUS - Status value that means "ready" (e.g., "active", "running") +# STATUS_PY - Python expression to extract status from JSON (receives 'd' as parsed dict) +# IP_PY - Python expression to extract IP from JSON (receives 'd' as parsed dict) +# IP_VAR - Environment variable name to export with the IP (e.g., "VULTR_SERVER_IP") +# DESCRIPTION - Human-readable label for logging (e.g., "Vultr instance") +# MAX_ATTEMPTS - Optional, defaults to 60 +# +# Example: +# generic_wait_for_instance vultr_api "/instances/$id" "active" \ +# "d['instance']['status']" "d['instance']['main_ip']" \ +# VULTR_SERVER_IP "Instance" 60 +generic_wait_for_instance() { + local api_func="${1}" + local endpoint="${2}" + local target_status="${3}" + local status_py="${4}" + local ip_py="${5}" + local ip_var="${6}" + local description="${7}" + local max_attempts="${8:-60}" + local poll_delay="${INSTANCE_STATUS_POLL_DELAY:-5}" + + local attempt=1 + log_warn "Waiting for ${description} to become ${target_status}..." + + while [[ "${attempt}" -le "${max_attempts}" ]]; do + local response + response=$("${api_func}" GET "${endpoint}" 2>/dev/null) || true + + local status + status=$(printf '%s' "${response}" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(${status_py})" 2>/dev/null || echo "unknown") + + if [[ "${status}" == "${target_status}" ]]; then + local ip + ip=$(printf '%s' "${response}" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(${ip_py})" 2>/dev/null || echo "") + + if [[ -n "${ip}" ]]; then + export "${ip_var}=${ip}" + log_info "${description} ${target_status}: IP=${ip}" + return 0 + fi + fi + + log_warn "${description} status: ${status} (${attempt}/${max_attempts})" + sleep "${poll_delay}" + attempt=$((attempt + 1)) + done + + log_error "${description} did not become ${target_status} in time" + return 1 +} + # ============================================================ # API token management helpers # ============================================================ diff --git a/upcloud/lib/common.sh b/upcloud/lib/common.sh index 56ab11e7..df0961f9 100644 --- a/upcloud/lib/common.sh +++ b/upcloud/lib/common.sh @@ -183,39 +183,10 @@ else: _wait_for_upcloud_server_ip() { local server_uuid="$1" local max_attempts=${2:-60} - local attempt=1 - - log_warn "Waiting for server to become active..." - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(upcloud_api GET "/server/$server_uuid") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['server']['state'])" 2>/dev/null || echo "unknown") - - if [[ "$status" == "started" ]]; then - UPCLOUD_SERVER_IP=$(echo "$status_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -for iface in data['server'].get('ip_addresses', {}).get('ip_address', []): - if iface.get('access') == 'public' and iface.get('family') == 'IPv4': - print(iface['address']) - break -" 2>/dev/null) - export UPCLOUD_SERVER_IP - - if [[ -n "${UPCLOUD_SERVER_IP}" ]]; then - log_info "Server active: IP=$UPCLOUD_SERVER_IP" - return 0 - fi - fi - - log_warn "Server status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Server did not become active in time" - return 1 + generic_wait_for_instance upcloud_api "/server/${server_uuid}" \ + "started" "d['server']['state']" \ + "next((i['address'] for i in d['server'].get('ip_addresses',{}).get('ip_address',[]) if i.get('access')=='public' and i.get('family')=='IPv4'), '')" \ + UPCLOUD_SERVER_IP "Server" "${max_attempts}" } # Build JSON request body for UpCloud server creation diff --git a/vultr/lib/common.sh b/vultr/lib/common.sh index 0723d876..6d3c2a35 100755 --- a/vultr/lib/common.sh +++ b/vultr/lib/common.sh @@ -141,31 +141,11 @@ print(json.dumps(body)) _wait_for_vultr_instance() { local instance_id="$1" local max_attempts=${2:-60} - local attempt=1 - - log_warn "Waiting for instance to become active..." - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(vultr_api GET "/instances/$instance_id") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['status'])") - local power - power=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['power_status'])") - - if [[ "$status" == "active" && "$power" == "running" ]]; then - VULTR_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['main_ip'])") - export VULTR_SERVER_IP - log_info "Instance active: IP=$VULTR_SERVER_IP" - return 0 - fi - - log_warn "Instance status: $status/$power ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Instance did not become active in time" - return 1 + generic_wait_for_instance vultr_api "/instances/${instance_id}" \ + "active/running" \ + "d['instance']['status']+'/'+d['instance']['power_status']" \ + "d['instance']['main_ip']" \ + VULTR_SERVER_IP "Instance" "${max_attempts}" } create_server() {