diff --git a/genesiscloud/lib/common.sh b/genesiscloud/lib/common.sh index 3df7b487..2f362e84 100644 --- a/genesiscloud/lib/common.sh +++ b/genesiscloud/lib/common.sh @@ -111,6 +111,70 @@ get_server_name() { echo "$server_name" } +# Fetch all SSH key IDs from Genesis Cloud account +_genesis_fetch_ssh_key_ids() { + local ssh_keys_response + ssh_keys_response=$(genesis_api GET "/ssh-keys") + echo "$ssh_keys_response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +keys = data.get('ssh_keys', []) +ids = [k['id'] for k in keys] +print(json.dumps(ids)) +" +} + +# Build the JSON request body for Genesis Cloud instance creation +_genesis_build_create_payload() { + local name="$1" instance_type="$2" region="$3" image="$4" ssh_key_ids="$5" + + local userdata + userdata=$(get_cloud_init_userdata) + local userdata_json + userdata_json=$(echo "$userdata" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))") + + python3 -c " +import json +body = { + 'name': '$name', + 'type': '$instance_type', + 'region': '$region', + 'image': '$image', + 'ssh_key_ids': $ssh_key_ids, + 'startup_script': json.loads($userdata_json) +} +print(json.dumps(body)) +" +} + +# Poll Genesis Cloud API until instance is active, sets GENESIS_SERVER_IP +_genesis_wait_for_active() { + local server_id="$1" + 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=$(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 +} + create_server() { local name="$1" local instance_type="${GENESIS_INSTANCE_TYPE:-vcpu-4_memory-12g_nvidia-rtx-3080-1}" @@ -125,36 +189,11 @@ create_server() { log_warn "Creating Genesis Cloud instance '$name' (type: $instance_type, region: $region)..." - # Get all SSH key IDs - local ssh_keys_response - ssh_keys_response=$(genesis_api GET "/ssh-keys") local ssh_key_ids - ssh_key_ids=$(echo "$ssh_keys_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -keys = data.get('ssh_keys', []) -ids = [k['id'] for k in keys] -print(json.dumps(ids)) -") - - local userdata - userdata=$(get_cloud_init_userdata) - local userdata_json - userdata_json=$(echo "$userdata" | python3 -c "import json,sys; print(json.dumps(sys.stdin.read()))") + ssh_key_ids=$(_genesis_fetch_ssh_key_ids) local body - body=$(python3 -c " -import json -body = { - 'name': '$name', - 'type': '$instance_type', - 'region': '$region', - 'image': '$image', - 'ssh_key_ids': $ssh_key_ids, - 'startup_script': json.loads($userdata_json) -} -print(json.dumps(body)) -") + body=$(_genesis_build_create_payload "$name" "$instance_type" "$region" "$image" "$ssh_key_ids") local response response=$(genesis_api POST "/instances" "$body") @@ -178,30 +217,7 @@ print(json.dumps(body)) return 1 fi - # Wait for instance to get an IP and become active - 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=$(genesis_api GET "/instances/$GENESIS_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 + _genesis_wait_for_active "$GENESIS_SERVER_ID" } verify_server_connectivity() { diff --git a/linode/lib/common.sh b/linode/lib/common.sh index 1c338f26..be4ad593 100644 --- a/linode/lib/common.sh +++ b/linode/lib/common.sh @@ -113,40 +113,31 @@ get_server_name() { # get_cloud_init_userdata is now defined in shared/common.sh -create_server() { - local name="$1" - local type="${LINODE_TYPE:-g6-standard-1}" - local region="${LINODE_REGION:-us-east}" - local image="linode/ubuntu24.04" - - # Validate env var inputs to prevent injection into Python code - validate_resource_name "$type" || { log_error "Invalid LINODE_TYPE"; return 1; } - validate_region_name "$region" || { log_error "Invalid LINODE_REGION"; return 1; } - - log_warn "Creating Linode '$name' (type: $type, region: $region)..." - - # Get all SSH key IDs +# Fetch all authorized SSH public keys from Linode profile +_linode_fetch_ssh_keys() { local ssh_keys_response ssh_keys_response=$(linode_api GET "/profile/sshkeys") - local authorized_keys - authorized_keys=$(python3 -c " + python3 -c " import json, sys data = json.loads(sys.stdin.read()) keys = [k['ssh_key'] for k in data.get('data', [])] print(json.dumps(keys)) -" <<< "$ssh_keys_response") +" <<< "$ssh_keys_response" +} + +# Build the JSON request body for Linode instance creation +_linode_build_create_payload() { + local name="$1" region="$2" type="$3" image="$4" authorized_keys="$5" local userdata userdata=$(get_cloud_init_userdata) local userdata_b64 userdata_b64=$(echo "$userdata" | base64 -w0 2>/dev/null || echo "$userdata" | base64) - # Generate a root password (required by Linode API) local root_pass root_pass=$(python3 -c "import secrets,string; print(''.join(secrets.choice(string.ascii_letters+string.digits+'!@#$') for _ in range(32)))") - local body - body=$(python3 -c " + python3 -c " import json body = { 'label': '$name', @@ -161,7 +152,49 @@ body = { 'booted': True } 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 +} + +create_server() { + local name="$1" + local type="${LINODE_TYPE:-g6-standard-1}" + local region="${LINODE_REGION:-us-east}" + local image="linode/ubuntu24.04" + + # Validate env var inputs to prevent injection into Python code + validate_resource_name "$type" || { log_error "Invalid LINODE_TYPE"; return 1; } + validate_region_name "$region" || { log_error "Invalid LINODE_REGION"; return 1; } + + log_warn "Creating Linode '$name' (type: $type, region: $region)..." + + local authorized_keys + authorized_keys=$(_linode_fetch_ssh_keys) + + local body + body=$(_linode_build_create_payload "$name" "$region" "$type" "$image" "$authorized_keys") local response response=$(linode_api POST "/linode/instances" "$body") @@ -173,7 +206,6 @@ print(json.dumps(body)) else log_error "Failed to create Linode instance" - # Parse error details local error_msg error_msg=$(echo "$response" | python3 -c " import json,sys @@ -192,25 +224,7 @@ print('; '.join(e.get('reason','Unknown') for e in errs) if errs else 'Unknown e return 1 fi - # Wait for Linode to become running and get IP - 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/$LINODE_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++)) - done - log_error "Linode did not become active in time"; return 1 + _linode_wait_for_active "$LINODE_SERVER_ID" } verify_server_connectivity() {