From 81bb668ee092ed0166dbeb5cb450144a446d9b47 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:07:17 -0800 Subject: [PATCH] refactor: replace hand-rolled loops/helpers with shared utilities in cherry and ionos (#916) Agent: complexity-hunter Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Haiku 4.5 --- cherry/lib/common.sh | 84 +++++------------------------------- ionos/lib/common.sh | 100 ++++++++++++++----------------------------- 2 files changed, 42 insertions(+), 142 deletions(-) diff --git a/cherry/lib/common.sh b/cherry/lib/common.sh index 1927252a..8bfdf008 100755 --- a/cherry/lib/common.sh +++ b/cherry/lib/common.sh @@ -18,40 +18,6 @@ CHERRY_DEFAULT_PLAN="${CHERRY_DEFAULT_PLAN:-cloud_vps_1}" CHERRY_DEFAULT_REGION="${CHERRY_DEFAULT_REGION:-eu_nord_1}" CHERRY_DEFAULT_IMAGE="${CHERRY_DEFAULT_IMAGE:-Ubuntu 24.04 64bit}" -# ============================================================ -# JSON Helpers -# ============================================================ - -# Extract a field from a JSON object via stdin -# Usage: echo '{"id": 123}' | _cherry_json_field "id" -_cherry_json_field() { - local field="$1" - python3 -c " -import sys, json -try: - data = json.load(sys.stdin) - print(data.get(sys.argv[1], '')) -except: - pass -" "$field" 2>&1 -} - -# Extract the primary IP address from a Cherry server info response -# Usage: echo '{"ip_addresses":[...]}' | _cherry_extract_primary_ip -_cherry_extract_primary_ip() { - python3 -c " -import sys, json -try: - data = json.load(sys.stdin) - for addr in data.get('ip_addresses', []): - if addr.get('type') == 'primary-ip': - print(addr.get('address', '')) - break -except: - pass -" 2>&1 -} - # ============================================================ # API Wrapper # ============================================================ @@ -132,14 +98,7 @@ get_cherry_project_id() { projects=$(cherry_api GET "/projects") local project_id - project_id=$(printf '%s' "$projects" | python3 -c " -import sys, json -try: - data = json.load(sys.stdin) - if isinstance(data, list) and len(data) > 0: - print(data[0].get('id', '')) -except: pass -" 2>&1) + project_id=$(_extract_json_field "$projects" "d[0]['id'] if isinstance(d,list) and d else ''") if [[ -z "$project_id" ]]; then log_error "No project found in Cherry Servers account" @@ -161,39 +120,18 @@ get_server_name() { printf '%s' "$server_name" } -# Create server -# Sets CHERRY_SERVER_ID and CHERRY_SERVER_IP as exports -# Poll the Cherry Servers API until the server has an IP address +# Python expression to extract primary IPv4 from Cherry Servers response. +# Finds the first ip_addresses entry with type == 'primary-ip'. +readonly _CHERRY_IP_PY="next((a.get('address','') for a in d.get('ip_addresses',[]) if a.get('type')=='primary-ip'), '')" + +# Wait for Cherry server to be deployed and get IP # Sets CHERRY_SERVER_IP on success _cherry_wait_for_ip() { local server_id="$1" - log_step "Waiting for IP address assignment..." - local ip_address="" - local attempts=0 - local max_attempts=60 - - while [[ -z "$ip_address" ]] && [[ $attempts -lt $max_attempts ]]; do - sleep "${POLL_INTERVAL}" - - local server_info - server_info=$(cherry_api GET "/servers/${server_id}") - - ip_address=$(printf '%s' "$server_info" | _cherry_extract_primary_ip) - - attempts=$((attempts + 1)) - if [[ -z "$ip_address" ]] && [[ $((attempts % 5)) -eq 0 ]]; then - log_step "Still waiting for IP address... (attempt ${attempts}/${max_attempts})" - fi - done - - if [[ -z "$ip_address" ]]; then - log_error "Failed to get server IP address after ${max_attempts} attempts" - return 1 - fi - - log_info "Server IP: $ip_address" - CHERRY_SERVER_IP="$ip_address" - export CHERRY_SERVER_IP + generic_wait_for_instance cherry_api "/servers/${server_id}" \ + "deployed" "d.get('status','')" \ + "${_CHERRY_IP_PY}" \ + CHERRY_SERVER_IP "Cherry server" 60 } create_server() { @@ -232,7 +170,7 @@ print(json.dumps(data)) response=$(cherry_api POST "/projects/${project_id}/servers" "$payload") local server_id - server_id=$(printf '%s' "$response" | _cherry_json_field "id") + server_id=$(_extract_json_field "$response" "d.get('id','')") if [[ -z "$server_id" ]]; then log_error "Failed to create server" diff --git a/ionos/lib/common.sh b/ionos/lib/common.sh index 6241c7b3..9eac11c2 100644 --- a/ionos/lib/common.sh +++ b/ionos/lib/common.sh @@ -39,7 +39,9 @@ ionos_api() { # Usage: error_msg=$(ionos_parse_api_error "$response") ionos_parse_api_error() { local response="$1" - echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); msgs=d.get('messages',[]); print(msgs[0].get('message','Unknown error') if msgs else 'Unknown error')" 2>/dev/null || echo "$response" + _extract_json_field "$response" \ + "(lambda m: m[0].get('message','Unknown error') if m else 'Unknown error')(d.get('messages',[]))" \ + "$response" } # Check if an IONOS API response is an error, log it, and return 1 if so @@ -146,21 +148,16 @@ get_server_name() { _ionos_find_existing_datacenter() { local response="$1" - # Extract ID and name from first datacenter in a single python3 call - local dc_info - dc_info=$(printf '%s' "$response" | python3 -c " -import json, sys -items = json.loads(sys.stdin.read()).get('items', []) -if not items: - sys.exit(1) -dc = items[0] -print(dc['id']) -print(dc.get('properties', {}).get('name', 'N/A')) -" 2>/dev/null) || return 1 + IONOS_DATACENTER_ID=$(_extract_json_field "$response" \ + "d.get('items',[])[0]['id'] if d.get('items') else ''") + + if [[ -z "$IONOS_DATACENTER_ID" ]]; then + return 1 + fi - IONOS_DATACENTER_ID=$(printf '%s' "$dc_info" | head -1) local dc_name - dc_name=$(printf '%s' "$dc_info" | tail -1) + dc_name=$(_extract_json_field "$response" \ + "d.get('items',[])[0].get('properties',{}).get('name','N/A')") log_info "Using existing datacenter: $dc_name (ID: $IONOS_DATACENTER_ID)" } @@ -221,17 +218,8 @@ _ionos_find_ubuntu_image() { local images_response images_response=$(ionos_api GET "/images?depth=2") - echo "$images_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -for img in data.get('items', []): - props = img.get('properties', {}) - name = props.get('name', '').lower() - image_type = props.get('imageType', '') - if 'ubuntu' in name and '24' in name and image_type == 'HDD': - print(img['id']) - break -" 2>/dev/null + _extract_json_field "$images_response" \ + "next((i['id'] for i in d.get('items',[]) if 'ubuntu' in i.get('properties',{}).get('name','').lower() and '24' in i.get('properties',{}).get('name','') and i.get('properties',{}).get('imageType','')=='HDD'), '')" } # Build JSON request body for IONOS volume creation @@ -260,29 +248,16 @@ print(json.dumps(body)) " "$name" "$disk_size" "$image_id" "$userdata" } -# Poll until an IONOS volume reaches AVAILABLE state -# Usage: _ionos_wait_for_volume VOLUME_ID [MAX_WAIT] +# Wait for an IONOS volume to reach AVAILABLE state +# Usage: _ionos_wait_for_volume VOLUME_ID [MAX_ATTEMPTS] _ionos_wait_for_volume() { local volume_id="$1" - local max_wait="${2:-120}" - - log_step "Waiting for volume provisioning..." - local waited=0 - while [[ $waited -lt $max_wait ]]; do - local vol_status - vol_status=$(ionos_api GET "/datacenters/${IONOS_DATACENTER_ID}/volumes/${volume_id}") - local state - state=$(_extract_json_field "$vol_status" "d.get('metadata',{}).get('state','')") - - if [[ "$state" == "AVAILABLE" ]]; then - log_info "Volume ready" - return 0 - fi - - sleep 5 - waited=$((waited + 5)) - done - return 1 + local max_attempts="${2:-24}" + generic_wait_for_instance ionos_api \ + "/datacenters/${IONOS_DATACENTER_ID}/volumes/${volume_id}" \ + "AVAILABLE" "d.get('metadata',{}).get('state','')" \ + "str(d.get('id',''))" \ + _IONOS_VOLUME_READY "IONOS volume" "${max_attempts}" } # Create a boot volume in the datacenter and wait for it to become AVAILABLE @@ -312,31 +287,18 @@ _ionos_create_boot_volume() { echo "$volume_id" } -# Poll the IONOS API until the server has an IP address +# Python expression to extract IPv4 from IONOS server response (depth=3). +# Walks entities->nics->items[] to find the first assigned IP. +readonly _IONOS_IP_PY="next((ip for n in d.get('entities',{}).get('nics',{}).get('items',[]) for ip in n.get('properties',{}).get('ips',[])), '')" + +# Wait for IONOS server to become AVAILABLE and get its IP # Sets IONOS_SERVER_IP on success _ionos_wait_for_server_ip() { - log_step "Waiting for server to get IP address..." - local max_wait=180 - local waited=0 - while [[ $waited -lt $max_wait ]]; do - local server_status - server_status=$(ionos_api GET "/datacenters/${IONOS_DATACENTER_ID}/servers/${IONOS_SERVER_ID}?depth=3") - - IONOS_SERVER_IP=$(_extract_json_field "$server_status" \ - "next((ip for n in d.get('entities',{}).get('nics',{}).get('items',[]) for ip in n.get('properties',{}).get('ips',[])), '')") - - if [[ -n "$IONOS_SERVER_IP" ]]; then - log_info "Server IP: $IONOS_SERVER_IP" - export IONOS_SERVER_IP - return 0 - fi - - sleep 5 - waited=$((waited + 5)) - done - - log_error "Failed to get server IP address" - return 1 + generic_wait_for_instance ionos_api \ + "/datacenters/${IONOS_DATACENTER_ID}/servers/${IONOS_SERVER_ID}?depth=3" \ + "AVAILABLE" "d.get('metadata',{}).get('state','')" \ + "${_IONOS_IP_PY}" \ + IONOS_SERVER_IP "IONOS server" 36 } # Build JSON request body for IONOS server creation