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 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-13 05:07:17 -08:00 committed by GitHub
parent 6be6537f1b
commit 81bb668ee0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 42 additions and 142 deletions

View file

@ -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"

View file

@ -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