diff --git a/civo/lib/common.sh b/civo/lib/common.sh index 11ee7827..81536b53 100644 --- a/civo/lib/common.sh +++ b/civo/lib/common.sh @@ -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() { diff --git a/kamatera/lib/common.sh b/kamatera/lib/common.sh index c6b7f024..bfb729aa 100644 --- a/kamatera/lib/common.sh +++ b/kamatera/lib/common.sh @@ -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")