refactor: Extract helpers from civo and kamatera create_server functions (#124)

Civo: Extract build_create_instance_body() for JSON body construction
and wait_for_civo_instance() for the status polling loop, reducing
create_server() from 113 to 53 lines.

Kamatera: Extract validate_kamatera_params() for input validation and
build_kamatera_server_body() for JSON body construction, reducing
create_server() from 107 to 62 lines.

Agent: complexity-hunter

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-09 20:18:15 -08:00 committed by GitHub
parent 05fa33ead3
commit 8a780e933b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 117 additions and 90 deletions

View file

@ -183,6 +183,67 @@ if data:
echo "$ssh_key_id"
}
# Build the JSON request body for instance creation
# Usage: build_create_instance_body NAME SIZE REGION NETWORK_ID TEMPLATE_ID SSH_KEY_ID INIT_SCRIPT
build_create_instance_body() {
local name="$1" size="$2" region="$3"
local network_id="$4" template_id="$5" ssh_key_id="$6"
local init_script="$7"
local json_script
json_script=$(json_escape "$init_script")
python3 -c "
import json, sys
script = json.loads(sys.stdin.read())
body = {
'hostname': '$name',
'size': '$size',
'region': '$region',
'network_id': '$network_id',
'template_id': '$template_id',
'ssh_key_id': '$ssh_key_id',
'initial_user': 'root',
'script': script,
'public_ip': 'create'
}
print(json.dumps(body))
" <<< "$json_script"
}
# Wait for a Civo instance to become ACTIVE and retrieve its public IP
# Sets: CIVO_SERVER_IP
# Usage: wait_for_civo_instance SERVER_ID [MAX_ATTEMPTS]
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
status_response=$(civo_api GET "/instances/$server_id")
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
}
create_server() {
local name="$1"
local size="${CIVO_SIZE:-g3.medium}"
@ -194,16 +255,10 @@ create_server() {
log_warn "Creating Civo instance '$name' (size: $size, region: $region)..."
# Get network ID
local network_id
# Gather required resource IDs
local network_id template_id ssh_key_id
network_id=$(get_default_network_id "$region") || return 1
# Get Ubuntu template ID
local template_id
template_id=$(get_ubuntu_template_id "$region") || return 1
# Get SSH key ID
local ssh_key_id
ssh_key_id=$(get_ssh_key_id) || return 1
# Build init script for cloud-init equivalent
@ -225,27 +280,9 @@ touch /root/.cloud-init-complete
INIT_EOF
)
# Pass init script safely via stdin to avoid triple-quote injection
local json_script
json_script=$(json_escape "$init_script")
# Build request body and create instance
local body
body=$(python3 -c "
import json, sys
script = json.loads(sys.stdin.read())
body = {
'hostname': '$name',
'size': '$size',
'region': '$region',
'network_id': '$network_id',
'template_id': '$template_id',
'ssh_key_id': '$ssh_key_id',
'initial_user': 'root',
'script': script,
'public_ip': 'create'
}
print(json.dumps(body))
" <<< "$json_script")
body=$(build_create_instance_body "$name" "$size" "$region" "$network_id" "$template_id" "$ssh_key_id" "$init_script")
local response
response=$(civo_api POST "/instances" "$body")
@ -269,32 +306,7 @@ print(json.dumps(body))
return 1
fi
# Wait for instance to become active and get an IP
log_warn "Waiting for instance to become active..."
local max_attempts=60
local attempt=1
while [[ "$attempt" -le "$max_attempts" ]]; do
local status_response
status_response=$(civo_api GET "/instances/$CIVO_SERVER_ID")
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
wait_for_civo_instance "$CIVO_SERVER_ID"
}
verify_server_connectivity() {

View file

@ -272,6 +272,54 @@ if power == 'on':
return 1
}
# Validate Kamatera server creation parameters
# Usage: validate_kamatera_params DATACENTER CPU RAM DISK IMAGE BILLING
validate_kamatera_params() {
local datacenter="$1" cpu="$2" ram="$3" disk="$4" image="$5" billing="$6"
validate_region_name "$datacenter" || { log_error "Invalid KAMATERA_DATACENTER"; return 1; }
validate_resource_name "$cpu" || { log_error "Invalid KAMATERA_CPU"; return 1; }
if [[ ! "$ram" =~ ^[0-9]+$ ]]; then log_error "Invalid KAMATERA_RAM: must be numeric"; return 1; fi
if [[ ! "$disk" =~ ^[a-zA-Z0-9_=,-]+$ ]]; then log_error "Invalid KAMATERA_DISK"; return 1; fi
if [[ ! "$image" =~ ^[a-zA-Z0-9_.:-]+$ ]]; then log_error "Invalid KAMATERA_IMAGE"; return 1; fi
validate_resource_name "$billing" || { log_error "Invalid KAMATERA_BILLING"; return 1; }
}
# Build the JSON request body for Kamatera server creation
# Usage: build_kamatera_server_body NAME PASSWORD DATACENTER IMAGE CPU RAM DISK BILLING SSH_KEY SCRIPT_CONTENT
build_kamatera_server_body() {
local name="$1" password="$2" datacenter="$3" image="$4"
local cpu="$5" ram="$6" disk="$7" billing="$8"
local ssh_key="$9" script_content="${10}"
local json_ssh_key json_script
json_ssh_key=$(json_escape "$ssh_key")
json_script=$(json_escape "$script_content")
python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
body = {
'name': '$name',
'password': '$password',
'passwordValidate': '$password',
'ssh-key': data['ssh_key'],
'datacenter': '$datacenter',
'image': '$image',
'cpu': '$cpu',
'ram': $ram,
'disk': '$disk',
'dailybackup': 'no',
'managed': 'no',
'network': 'name=wan,ip=auto',
'quantity': 1,
'billingcycle': '$billing',
'poweronaftercreate': 'yes',
'script-file': data['script']
}
print(json.dumps(body))
" <<< "{\"ssh_key\": $json_ssh_key, \"script\": $json_script}"
}
create_server() {
local name="$1"
local datacenter="${KAMATERA_DATACENTER:-EU}"
@ -281,13 +329,7 @@ create_server() {
local image="${KAMATERA_IMAGE:-ubuntu_server_24.04_64-bit}"
local billing="${KAMATERA_BILLING:-hourly}"
# Validate env var inputs to prevent injection into Python code
validate_region_name "$datacenter" || { log_error "Invalid KAMATERA_DATACENTER"; return 1; }
validate_resource_name "$cpu" || { log_error "Invalid KAMATERA_CPU"; return 1; }
if [[ ! "$ram" =~ ^[0-9]+$ ]]; then log_error "Invalid KAMATERA_RAM: must be numeric"; return 1; fi
if [[ ! "$disk" =~ ^[a-zA-Z0-9_=,-]+$ ]]; then log_error "Invalid KAMATERA_DISK"; return 1; fi
if [[ ! "$image" =~ ^[a-zA-Z0-9_.:-]+$ ]]; then log_error "Invalid KAMATERA_IMAGE"; return 1; fi
validate_resource_name "$billing" || { log_error "Invalid KAMATERA_BILLING"; return 1; }
validate_kamatera_params "$datacenter" "$cpu" "$ram" "$disk" "$image" "$billing" || return 1
log_warn "Creating Kamatera server '$name' (datacenter: $datacenter, cpu: $cpu, ram: ${ram}MB)..."
@ -321,36 +363,9 @@ touch /root/.cloud-init-complete
INIT_EOF
)
# Pass SSH key and script content safely via stdin as JSON
local json_ssh_key
json_ssh_key=$(json_escape "$ssh_key")
local json_script
json_script=$(json_escape "$script_content")
# Build request body and submit server creation
local body
body=$(python3 -c "
import json, sys
data = json.loads(sys.stdin.read())
body = {
'name': '$name',
'password': '$password',
'passwordValidate': '$password',
'ssh-key': data['ssh_key'],
'datacenter': '$datacenter',
'image': '$image',
'cpu': '$cpu',
'ram': $ram,
'disk': '$disk',
'dailybackup': 'no',
'managed': 'no',
'network': 'name=wan,ip=auto',
'quantity': 1,
'billingcycle': '$billing',
'poweronaftercreate': 'yes',
'script-file': data['script']
}
print(json.dumps(body))
" <<< "{\"ssh_key\": $json_ssh_key, \"script\": $json_script}")
body=$(build_kamatera_server_body "$name" "$password" "$datacenter" "$image" "$cpu" "$ram" "$disk" "$billing" "$ssh_key" "$script_content")
local response
response=$(kamatera_api POST "/service/server" "$body")