|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +usage() { |
| 5 | + cat <<'EOF' |
| 6 | +Usage: firecracker.sh <action> <payload.json> [timeout] |
| 7 | +
|
| 8 | +Actions: create, start, stop, reboot, delete, status, recover, console |
| 9 | +EOF |
| 10 | +} |
| 11 | + |
| 12 | +fail() { |
| 13 | + echo "{\"status\":\"error\",\"error\":\"$1\"}" |
| 14 | + exit 1 |
| 15 | +} |
| 16 | + |
| 17 | +[[ $# -lt 2 ]] && { usage; fail "Missing action or payload file"; } |
| 18 | + |
| 19 | +ACTION="$1" |
| 20 | +PAYLOAD_FILE="$2" |
| 21 | +TIMEOUT="${3:-${FC_AGENT_TIMEOUT:-30}}" |
| 22 | + |
| 23 | +[[ -r "$PAYLOAD_FILE" ]] || fail "Payload file not found or unreadable" |
| 24 | +if ! [[ "$TIMEOUT" =~ ^[0-9]+$ ]]; then |
| 25 | + fail "Timeout must be an integer" |
| 26 | +fi |
| 27 | + |
| 28 | +RAW_PAYLOAD="$(<"$PAYLOAD_FILE")" |
| 29 | + |
| 30 | +mapfile -t FIELDS < <( |
| 31 | + jq -r '[ |
| 32 | + (.vm_name // ."cloudstack.vm.details".name // ."cloudstack.vm.details".uuid // "vm"), |
| 33 | + (.host_url // .externaldetails.host.url // ""), |
| 34 | + (.host_port // .externaldetails.host.port // .externaldetails.host.agent_port // 8000), |
| 35 | + (.host_username // .externaldetails.host.username // .externaldetails.host.user // .externaldetails.host.login // .username // ""), |
| 36 | + (.host_password // .externaldetails.host.password // .externaldetails.host.pass // .password // ""), |
| 37 | + (.host_token // .externaldetails.host.token // .externaldetails.host.agent_token // ""), |
| 38 | + ((.skip_ssl_verification // .host_skip_ssl_verification // .externaldetails.host.skip_ssl_verification // "false") | ascii_downcase), |
| 39 | + (.ca_bundle // .externaldetails.host.ca_bundle // .externaldetails.host.ca_cert // ""), |
| 40 | + (.client_cert // .externaldetails.host.client_cert // ""), |
| 41 | + (.client_key // .externaldetails.host.client_key // "") |
| 42 | + ] | @tsv' <<<"$RAW_PAYLOAD" |
| 43 | +) |
| 44 | + |
| 45 | +IFS=$'\t' read -r VM_NAME HOST_URL HOST_PORT HOST_USERNAME HOST_PASSWORD HOST_TOKEN SKIP_VERIFY CA_BUNDLE CLIENT_CERT CLIENT_KEY <<<"${FIELDS[0]}" |
| 46 | + |
| 47 | +[[ -z "$HOST_URL" ]] && fail "host_url or externaldetails.host.url is required" |
| 48 | +if [[ "$VM_NAME" =~ [^A-Za-z0-9-] ]]; then |
| 49 | + fail "Invalid VM name '$VM_NAME'. Only alphanumeric characters and dashes are allowed." |
| 50 | +fi |
| 51 | + |
| 52 | +if [[ "$HOST_URL" != *"://"* ]]; then |
| 53 | + HOST_URL="http://$HOST_URL" |
| 54 | + HAS_PORT=false |
| 55 | +else |
| 56 | + authority="${HOST_URL#*://}" |
| 57 | + authority="${authority%%/*}" |
| 58 | + if [[ "$authority" == *:* ]]; then |
| 59 | + HAS_PORT=true |
| 60 | + else |
| 61 | + HAS_PORT=false |
| 62 | + fi |
| 63 | +fi |
| 64 | + |
| 65 | +BASE="${HOST_URL%/}" |
| 66 | +if [[ $HAS_PORT == false ]]; then |
| 67 | + BASE="${BASE}:${HOST_PORT}" |
| 68 | +fi |
| 69 | +API_BASE="${BASE%/}/v1" |
| 70 | + |
| 71 | +declare -a CURL_OPTS |
| 72 | +CURL_OPTS=(-sS -w '\n%{http_code}') |
| 73 | + |
| 74 | +if [[ "$SKIP_VERIFY" == "true" || "$SKIP_VERIFY" == "1" ]]; then |
| 75 | + CURL_OPTS+=(-k) |
| 76 | +fi |
| 77 | +if [[ -n "$CA_BUNDLE" ]]; then |
| 78 | + CURL_OPTS+=(--cacert "$CA_BUNDLE") |
| 79 | +fi |
| 80 | +if [[ -n "$CLIENT_CERT" ]]; then |
| 81 | + if [[ -n "$CLIENT_KEY" ]]; then |
| 82 | + CURL_OPTS+=(--cert "$CLIENT_CERT" --key "$CLIENT_KEY") |
| 83 | + else |
| 84 | + CURL_OPTS+=(--cert "$CLIENT_CERT") |
| 85 | + fi |
| 86 | +fi |
| 87 | + |
| 88 | +declare -a CURL_HEADERS |
| 89 | +CURL_HEADERS=(-H "Accept: application/json") |
| 90 | +if [[ -n "$HOST_TOKEN" ]]; then |
| 91 | + CURL_HEADERS+=(-H "Authorization: Bearer $HOST_TOKEN") |
| 92 | +fi |
| 93 | +if [[ -n "$HOST_USERNAME" ]]; then |
| 94 | + CURL_OPTS+=(-u "${HOST_USERNAME}:${HOST_PASSWORD}") |
| 95 | +fi |
| 96 | + |
| 97 | +call_api() { |
| 98 | + local method="$1" |
| 99 | + local path="$2" |
| 100 | + local body="${3:-}" |
| 101 | + local args=("${CURL_OPTS[@]}" -X "$method" "${CURL_HEADERS[@]}") |
| 102 | + if [[ -n "$body" ]]; then |
| 103 | + args+=(-H "Content-Type: application/json" -d "$body") |
| 104 | + fi |
| 105 | + |
| 106 | + local response |
| 107 | + if ! response=$(curl "${args[@]}" "$API_BASE$path"); then |
| 108 | + fail "HTTP request failed" |
| 109 | + fi |
| 110 | + local http_code="${response##*$'\n'}" |
| 111 | + local body_content="${response%$'\n'$http_code}" |
| 112 | + |
| 113 | + if [[ "$http_code" =~ ^[45] ]]; then |
| 114 | + local err |
| 115 | + err=$(jq -r '.error // .message // @json' <<<"$body_content" 2>/dev/null || echo "$body_content") |
| 116 | + fail "Agent error ($http_code): $err" |
| 117 | + fi |
| 118 | + |
| 119 | + echo "$body_content" |
| 120 | +} |
| 121 | + |
| 122 | +payload_with_spec() { |
| 123 | + jq -c --argjson timeout "$TIMEOUT" '{spec: ., timeout: $timeout}' "$PAYLOAD_FILE" |
| 124 | +} |
| 125 | + |
| 126 | +timeout_payload() { |
| 127 | + jq -n --argjson timeout "$TIMEOUT" '{timeout: $timeout}' |
| 128 | +} |
| 129 | + |
| 130 | +case "$ACTION" in |
| 131 | + create) |
| 132 | + call_api POST "/vms" "$(payload_with_spec)" |
| 133 | + ;; |
| 134 | + start) |
| 135 | + call_api POST "/vms/${VM_NAME}/start" "$(payload_with_spec)" |
| 136 | + ;; |
| 137 | + stop) |
| 138 | + call_api POST "/vms/${VM_NAME}/stop" "$(timeout_payload)" |
| 139 | + ;; |
| 140 | + reboot) |
| 141 | + call_api POST "/vms/${VM_NAME}/reboot" "$(timeout_payload)" |
| 142 | + ;; |
| 143 | + delete) |
| 144 | + call_api DELETE "/vms/${VM_NAME}" |
| 145 | + ;; |
| 146 | + status) |
| 147 | + call_api GET "/vms/${VM_NAME}/status" |
| 148 | + ;; |
| 149 | + recover) |
| 150 | + call_api POST "/vms/${VM_NAME}/recover" "$(payload_with_spec)" |
| 151 | + ;; |
| 152 | + console) |
| 153 | + call_api POST "/vms/${VM_NAME}/console" |
| 154 | + ;; |
| 155 | + *) |
| 156 | + usage |
| 157 | + fail "Invalid action '$ACTION'" |
| 158 | + ;; |
| 159 | +esac |
0 commit comments