#!/usr/bin/env bash # PAM deployment main script (Shell entry). set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_CONFIG_PATH="${SCRIPT_DIR}/config.txt" ACTIVE_CONFIG_PATH="$DEFAULT_CONFIG_PATH" TOKEN="" HASH_CODE="" NODE_URL="" TOTAL_COUNT=0 SUCCESS_COUNT=0 FAIL_COUNT=0 RESULTS_FILE="" ONLINE_IPS=() API_TRACE_FILE="" API_TRACE_SEQ=0 TRACE_ANNOUNCED=0 CURRENT_TRACE_ID="" DOWNLOAD_PROGRESS_STATUS="" DOWNLOAD_PROGRESS_SUCCESS="" DOWNLOAD_PROGRESS_STEP="" DOWNLOAD_PROGRESS_MSG="" DOWNLOAD_PROGRESS_MESSAGE="" DOWNLOAD_PROGRESS_RATE="" DOWNLOAD_PROGRESS_RESPONSE="" UPGRADE_PROGRESS_STATUS="" UPGRADE_PROGRESS_SUCCESS="" UPGRADE_PROGRESS_STEP="" UPGRADE_PROGRESS_MSG="" UPGRADE_PROGRESS_MESSAGE="" UPGRADE_PROGRESS_RATE="" UPGRADE_PROGRESS_CODE="" UPGRADE_PROGRESS_FINISH="" UPGRADE_PROGRESS_LAST_MODIFY="" UPGRADE_PROGRESS_RESPONSE="" usage() { cat <<'EOF' 用法: ./deploy.sh [--config /path/to/config.txt] ./deploy.sh [--config /path/to/config.txt] --rollback-ip 192.168.1.10 [--rollback-stop-first] ./deploy.sh [--config /path/to/config.txt] --action [--ip ] [--hash-code ] [--stop-first] [--trace-file /path/to/api_trace.log] 配置项: HOME_BASE_URL CLIENT_ID CLIENT_SECRET AIRPORT_CODE APP_NAME MODULE_NAME VERSION_NUMBER ZIP_FILE_PATH ACTION_TYPE TIMEOUT LOG_NAME EOF } log_info() { printf '[INFO] %s\n' "$*" >&2; } log_warn() { printf '[WARN] %s\n' "$*" >&2; } log_error() { printf '[ERROR] %s\n' "$*" >&2; } result_line() { printf '%s=%s\n' "$1" "$2"; } log_flow_start() { local name="$1" shift || true if (($#)); then log_info "[FLOW][START] ${name} | $*" else log_info "[FLOW][START] ${name}" fi } log_flow_done() { local name="$1" shift || true if (($#)); then log_info "[FLOW][DONE] ${name} | $*" else log_info "[FLOW][DONE] ${name}" fi } log_flow_fail() { local name="$1" shift || true if (($#)); then log_error "[FLOW][FAIL] ${name} | $*" else log_error "[FLOW][FAIL] ${name}" fi } run_flow_step() { local flow_name="$1" shift log_flow_start "$flow_name" if "$@"; then log_flow_done "$flow_name" return 0 fi local exit_code=$? log_flow_fail "$flow_name" "exit=${exit_code}" return "$exit_code" } run_flow_capture() { local __var_name="$1" local flow_name="$2" shift 2 local output="" log_flow_start "$flow_name" if output="$("$@")"; then printf -v "$__var_name" '%s' "$output" log_flow_done "$flow_name" return 0 fi local exit_code=$? printf -v "$__var_name" '%s' "$output" local detail="exit=${exit_code}" if [[ -n "$output" ]]; then detail="${detail} output=${output//$'\n'/ }" fi log_flow_fail "$flow_name" "$detail" return "$exit_code" } manual_rollback_command() { local ip="$1" local stop_first="$2" local command="./deploy.sh --config \"$ACTIVE_CONFIG_PATH\" --rollback-ip \"$ip\"" if [[ "$stop_first" == "true" ]]; then command="${command} --rollback-stop-first" fi printf '%s' "$command" } pending_rollback_status() { local ip="$1" local stage="$2" local reason="$3" local stop_first="$4" local command command="$(manual_rollback_command "$ip" "$stop_first")" printf '[WARN] 检测到需要回滚: ip=%s, stage=%s, reason=%s, stopFirst=%s\n' "$ip" "$stage" "$reason" "$stop_first" >&2 printf '[WARN] 当前脚本不会自动执行回滚。请由 Agent 与用户确认后,再执行: %s\n' "$command" >&2 printf 'PENDING_AGENT_CONFIRMATION(stopFirst=%s)' "$stop_first" } timestamp_now() { date '+%Y-%m-%d %H:%M:%S' } ensure_trace_file() { if [[ -n "$API_TRACE_FILE" ]]; then local trace_dir trace_dir="$(dirname "$API_TRACE_FILE")" mkdir -p "$trace_dir" if [[ ! -f "$API_TRACE_FILE" ]]; then { printf 'PAM API TRACE LOG\n' printf 'Started: %s\n' "$(timestamp_now)" printf 'ScriptDir: %s\n' "$SCRIPT_DIR" printf '\n' } > "$API_TRACE_FILE" fi if (( TRACE_ANNOUNCED == 0 )); then log_info "接口跟踪日志: $API_TRACE_FILE" TRACE_ANNOUNCED=1 fi return 0 fi local trace_dir="${SCRIPT_DIR}/logs" local trace_name mkdir -p "$trace_dir" trace_name="api_trace_$(safe_trace_name_part "$AIRPORT_CODE")_$(safe_trace_name_part "$APP_NAME")_$(safe_trace_name_part "$MODULE_NAME")_$(safe_trace_name_part "$VERSION_NUMBER").log" API_TRACE_FILE="${trace_dir}/${trace_name}" if [[ ! -f "$API_TRACE_FILE" ]]; then { printf 'PAM API TRACE LOG\n' printf 'Started: %s\n' "$(timestamp_now)" printf 'ScriptDir: %s\n' "$SCRIPT_DIR" printf '\n' } > "$API_TRACE_FILE" fi if (( TRACE_ANNOUNCED == 0 )); then log_info "接口跟踪日志: $API_TRACE_FILE" TRACE_ANNOUNCED=1 fi } next_trace_id() { API_TRACE_SEQ=$((API_TRACE_SEQ + 1)) printf -v CURRENT_TRACE_ID 'REQ-%04d' "$API_TRACE_SEQ" } mask_sensitive_text() { local text="$1" text="$(printf '%s' "$text" | sed -E \ -e 's/(client_secret=)[^&[:space:]]+/\1***MASKED***/g' \ -e 's/(Authorization: (Basic|Bearer) )[^\r\n]+/\1***MASKED***/g' \ -e 's/("access_token"[[:space:]]*:[[:space:]]*")[^"]+/\1***MASKED***/g' \ -e 's/("client_secret"[[:space:]]*:[[:space:]]*")[^"]+/\1***MASKED***/g')" printf '%s' "$text" } indent_text() { local text="$1" if [[ -z "$text" ]]; then printf ' (empty)\n' else printf '%s\n' "$text" | sed 's/^/ /' fi } trace_request() { local request_id="$1" local method="$2" local url="$3" local headers="$4" local body="$5" ensure_trace_file { printf '[%s] [%s] REQUEST\n' "$(timestamp_now)" "$request_id" printf 'METHOD: %s\n' "$method" printf 'URL: %s\n' "$url" printf 'HEADERS:\n' indent_text "$(mask_sensitive_text "$headers")" printf 'BODY:\n' indent_text "$(mask_sensitive_text "$body")" printf '\n' } >> "$API_TRACE_FILE" } trace_response() { local request_id="$1" local curl_exit="$2" local http_code="$3" local response="$4" ensure_trace_file { printf '[%s] [%s] RESPONSE\n' "$(timestamp_now)" "$request_id" printf 'CURL_EXIT: %s\n' "$curl_exit" printf 'HTTP_CODE: %s\n' "${http_code:-N/A}" printf 'BODY:\n' indent_text "$(mask_sensitive_text "$response")" printf '\n' } >> "$API_TRACE_FILE" } trace_upload_request() { local request_id="$1" local url="$2" local file_path="$3" local fields="$4" ensure_trace_file { printf '[%s] [%s] REQUEST\n' "$(timestamp_now)" "$request_id" printf 'METHOD: POST\n' printf 'URL: %s\n' "$url" printf 'UPLOAD_FILE: %s\n' "$file_path" printf 'FORM_FIELDS:\n' indent_text "$(mask_sensitive_text "$fields")" printf '\n' } >> "$API_TRACE_FILE" } trace_download_result() { local request_id="$1" local url="$2" local http_code="$3" local curl_exit="$4" local output_file="$5" local error_text="$6" ensure_trace_file { printf '[%s] [%s] FILE_RESPONSE\n' "$(timestamp_now)" "$request_id" printf 'URL: %s\n' "$url" printf 'CURL_EXIT: %s\n' "$curl_exit" printf 'HTTP_CODE: %s\n' "${http_code:-N/A}" printf 'OUTPUT_FILE: %s\n' "$output_file" if [[ -f "$output_file" ]]; then printf 'OUTPUT_SIZE: %s\n' "$(wc -c < "$output_file" | tr -d ' ')" fi printf 'STDERR:\n' indent_text "$(mask_sensitive_text "$error_text")" printf '\n' } >> "$API_TRACE_FILE" } trim() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" printf '%s' "$value" } strip_inline_comment() { local value="$1" local comment_regex='^(.*[^[:space:]])[[:space:]]+[;#].*$' if [[ "$value" =~ $comment_regex ]]; then printf '%s' "${BASH_REMATCH[1]}" else printf '%s' "$value" fi } is_success_http_code() { local http_code="$1" [[ "$http_code" =~ ^2[0-9][0-9]$ ]] } set_defaults() { : "${HOME_BASE_URL:=https://pam.home.com}" : "${CLIENT_ID:=your_client_id}" : "${CLIENT_SECRET:=your_client_secret}" : "${AIRPORT_CODE:=HET}" : "${APP_NAME:=PAM}" : "${MODULE_NAME:=Node}" : "${VERSION_NUMBER:=2.0.5}" : "${ZIP_FILE_PATH:=/path/to/pam-2.0.5.zip}" : "${ACTION_TYPE:=FULL}" : "${TIMEOUT:=120}" : "${LOG_NAME:=app.log}" } load_config() { local config_path="$1" if [[ -f "$config_path" ]]; then while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do raw_line="${raw_line%$'\r'}" local line line="$(trim "$raw_line")" [[ -z "$line" ]] && continue case "$line" in \#*|\;*) continue ;; esac [[ "$line" != *"="* ]] && continue local key="${line%%=*}" local value="${line#*=}" key="$(trim "$key")" value="$(trim "$value")" value="$(strip_inline_comment "$value")" case "$key" in HOME_BASE_URL|CLIENT_ID|CLIENT_SECRET|AIRPORT_CODE|APP_NAME|MODULE_NAME|VERSION_NUMBER|ZIP_FILE_PATH|ACTION_TYPE|TIMEOUT|LOG_NAME) printf -v "$key" '%s' "$value" ;; esac done < "$config_path" else log_warn "未找到配置文件: $config_path,将使用默认值。" fi set_defaults } require_tool() { local tool="$1" if ! command -v "$tool" >/dev/null 2>&1; then log_error "缺少依赖: $tool" exit 1 fi } ensure_dependencies() { require_tool curl } ensure_zip_file() { if [[ ! -f "$ZIP_FILE_PATH" ]]; then log_error "软件包不存在: $ZIP_FILE_PATH" return 1 fi } sanitize_field() { local value="$1" value="${value//$'\r'/ }" value="${value//$'\n'/ }" value="${value//$'\t'/ }" printf '%s' "$value" } safe_trace_name_part() { local value="$1" value="${value//\\/_}" value="${value//\//_}" value="${value// /_}" value="${value//:/_}" printf '%s' "$value" } require_ip_arg() { local ip="$1" [[ -n "$ip" ]] && return 0 log_error "该 action 需要 --ip" return 1 } require_hash_code_arg() { local hash_code="$1" [[ -n "$hash_code" ]] && return 0 log_error "该 action 需要 --hash-code" return 1 } print_online_ips_result() { result_line "ACTION" "get-online-ips" result_line "COUNT" "${#ONLINE_IPS[@]}" for ip in "${ONLINE_IPS[@]}"; do result_line "IP" "$ip" done } print_progress_result() { local action_name="${1:-poll-download-progress}" result_line "ACTION" "$action_name" result_line "STEP" "$DOWNLOAD_PROGRESS_STEP" result_line "MSG" "$DOWNLOAD_PROGRESS_MSG" result_line "RATE_OF_PROGRESS" "$DOWNLOAD_PROGRESS_RATE" result_line "STATUS" "$DOWNLOAD_PROGRESS_STATUS" result_line "SUCCESS" "$DOWNLOAD_PROGRESS_SUCCESS" result_line "MESSAGE" "$DOWNLOAD_PROGRESS_MESSAGE" } print_upgrade_progress_result() { local ip="$1" result_line "ACTION" "poll-upgrade-progress" result_line "IP" "$ip" result_line "STEP" "$UPGRADE_PROGRESS_STEP" result_line "MSG" "$UPGRADE_PROGRESS_MSG" result_line "RATE_OF_PROGRESS" "$UPGRADE_PROGRESS_RATE" result_line "STATUS" "$UPGRADE_PROGRESS_STATUS" result_line "SUCCESS" "$UPGRADE_PROGRESS_SUCCESS" result_line "CODE" "$UPGRADE_PROGRESS_CODE" result_line "FINISH" "$UPGRADE_PROGRESS_FINISH" result_line "LAST_MODIFY" "$UPGRADE_PROGRESS_LAST_MODIFY" result_line "MESSAGE" "$UPGRADE_PROGRESS_MESSAGE" } json_value() { local input="$1" local query="$2" case "$query" in '.access_token') json_get_string_by_key "$input" "access_token" ;; '.status') json_get_string_by_key "$input" "status" ;; '.success') json_get_scalar_by_key "$input" "success" ;; '.message') json_get_string_by_key "$input" "message" ;; '.code') json_get_scalar_by_key "$input" "code" ;; '.finish') json_get_scalar_by_key "$input" "finish" ;; '.lastModify') json_get_scalar_by_key "$input" "lastModify" ;; '.msg') json_get_string_by_key "$input" "msg" ;; '.step') json_get_string_by_key "$input" "step" ;; '.rateOfProgress') json_get_scalar_by_key "$input" "rateOfProgress" ;; '.data.rateOfProgress') json_get_nested_string_by_key "$input" "data" "rateOfProgress" ;; '.progress') json_get_scalar_by_key "$input" "progress" ;; '.percent') json_get_scalar_by_key "$input" "percent" ;; '.data.progress') json_get_nested_string_by_key "$input" "data" "progress" ;; '.data.percent') json_get_nested_string_by_key "$input" "data" "percent" ;; '.hashCode // .data.hashCode') local value value="$(json_get_string_by_key "$input" "hashCode")" if [[ -n "$value" ]]; then printf '%s' "$value" else json_get_nested_string_by_key "$input" "data" "hashCode" fi ;; '.[]') json_array_items "$input" ;; *) return 1 ;; esac } json_compact() { local input="$1" input="${input//$'\r'/}" input="${input//$'\n'/}" printf '%s' "$input" } regex_escape_ere() { local text="$1" printf '%s' "$text" | sed -E 's/[][(){}.^$*+?|\\]/\\&/g' } json_unescape_basic() { local value="$1" value="${value//\\\\/\\}" value="${value//\\\"/\"}" value="${value//\\n/$'\n'}" value="${value//\\r/$'\r'}" value="${value//\\t/$'\t'}" printf '%s' "$value" } json_get_string_by_key() { local input="$1" local key="$2" local compact local value local pattern_key compact="$(json_compact "$input")" pattern_key="$(regex_escape_ere "$key")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$pattern_key'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p')" [[ -n "$value" ]] && json_unescape_basic "$value" } json_get_scalar_by_key() { local input="$1" local key="$2" local compact local value local pattern_key compact="$(json_compact "$input")" pattern_key="$(regex_escape_ere "$key")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$pattern_key'"[[:space:]]*:[[:space:]]*([^,}]+).*/\1/p')" value="$(trim "$value")" value="${value%\"}" value="${value#\"}" printf '%s' "$value" } json_get_nested_string_by_key() { local input="$1" local parent_key="$2" local child_key="$3" local compact local value local pattern_parent_key local pattern_child_key compact="$(json_compact "$input")" pattern_parent_key="$(regex_escape_ere "$parent_key")" pattern_child_key="$(regex_escape_ere "$child_key")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$pattern_parent_key'"[[:space:]]*:[[:space:]]*\{[^}]*"'$pattern_child_key'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p')" [[ -n "$value" ]] && json_unescape_basic "$value" } json_get_nested_scalar_by_key() { local input="$1" local parent_key="$2" local child_key="$3" local compact local value local pattern_parent_key local pattern_child_key compact="$(json_compact "$input")" pattern_parent_key="$(regex_escape_ere "$parent_key")" pattern_child_key="$(regex_escape_ere "$child_key")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$pattern_parent_key'"[[:space:]]*:[[:space:]]*\{[^}]*"'$pattern_child_key'"[[:space:]]*:[[:space:]]*([^,}]+).*/\1/p')" value="$(trim "$value")" value="${value%\"}" value="${value#\"}" printf '%s' "$value" } json_first_key() { local input="$1" local compact compact="$(json_compact "$input")" printf '%s' "$compact" | sed -nE 's/^[[:space:]]*\{[[:space:]]*"([^"]+)".*/\1/p' } json_array_items() { local input="$1" local compact compact="$(json_compact "$input")" compact="${compact#[}" compact="${compact%]}" [[ -z "$compact" ]] && return 0 printf '%s' "$compact" \ | sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//; s/"[[:space:]]*,[[:space:]]*"/\n/g' } json_scoped_value() { local input="$1" local scope_key="$2" local query="$3" local value="" if [[ -n "$scope_key" ]]; then case "$query" in '.status') value="$(json_get_nested_string_by_key "$input" "$scope_key" "status")" ;; '.success') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "success")" ;; '.message') value="$(json_get_nested_string_by_key "$input" "$scope_key" "message")" ;; '.msg') value="$(json_get_nested_string_by_key "$input" "$scope_key" "msg")" ;; '.step') value="$(json_get_nested_string_by_key "$input" "$scope_key" "step")" ;; '.rateOfProgress') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "rateOfProgress")" ;; '.progress') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "progress")" ;; '.percent') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "percent")" ;; '.code') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "code")" ;; '.finish') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "finish")" ;; '.lastModify') value="$(json_get_nested_scalar_by_key "$input" "$scope_key" "lastModify")" ;; esac fi if [[ -n "$value" ]]; then printf '%s' "$value" else json_value "$input" "$query" fi } response_message() { local input="$1" local message message="$(json_value "$input" '.message')" [[ -z "$message" ]] && message="$(json_value "$input" '.msg')" printf '%s' "$message" } response_indicates_failure() { local input="$1" local success_flag local code_value local status local message local error_regex='[Ff]ail|[Ee]rror' success_flag="$(json_value "$input" '.success')" code_value="$(json_value "$input" '.code')" status="$(json_value "$input" '.status')" message="$(response_message "$input")" if [[ "$success_flag" == "false" ]]; then return 0 fi if [[ -n "$code_value" && "$code_value" != "0" ]]; then return 0 fi if [[ "${status} ${message}" =~ $error_regex ]]; then return 0 fi return 1 } http_request() { local method="$1" local url="$2" local data="$3" local content_type="$4" shift 4 local -a cmd local request_id local headers_text="" local raw_output="" local response="" local http_code="" local curl_exit=0 local marker=$'\n__PAM_HTTP_CODE__:' cmd=(curl -sS -X "$method" "$url") if [[ -n "$TOKEN" ]]; then cmd+=(-H "Authorization: Bearer ${TOKEN}") headers_text+="Authorization: Bearer ${TOKEN}"$'\n' fi if [[ -n "$content_type" ]]; then cmd+=(-H "Content-Type: ${content_type}") headers_text+="Content-Type: ${content_type}"$'\n' fi while (($#)); do cmd+=(-H "$1") headers_text+="$1"$'\n' shift done if [[ -n "$data" ]]; then cmd+=(--data "$data") fi next_trace_id request_id="$CURRENT_TRACE_ID" trace_request "$request_id" "$method" "$url" "$headers_text" "$data" raw_output=$("${cmd[@]}" -w $'\n__PAM_HTTP_CODE__:%{http_code}' 2>&1) || curl_exit=$? if [[ "$raw_output" == *"$marker"* ]]; then response="${raw_output%"$marker"*}" http_code="${raw_output##*__PAM_HTTP_CODE__:}" else response="$raw_output" fi trace_response "$request_id" "$curl_exit" "$http_code" "$response" if (( curl_exit != 0 )); then log_error "请求失败: ${method} ${url}" log_error "$response" return 1 fi if [[ -n "$http_code" ]] && ! is_success_http_code "$http_code"; then log_error "请求返回非成功状态码: ${method} ${url} -> HTTP ${http_code}" log_error "$response" return 1 fi printf '%s' "$response" } upload_file() { local url="$1" local file_path="$2" shift 2 local -a cmd local request_id local fields_text="" local raw_output="" local response="" local http_code="" local curl_exit=0 local marker=$'\n__PAM_HTTP_CODE__:' cmd=(curl -sS -X POST "$url") if [[ -n "$TOKEN" ]]; then cmd+=(-H "Authorization: Bearer ${TOKEN}") fields_text+="Authorization: Bearer ${TOKEN}"$'\n' fi cmd+=(-F "file=@${file_path}") while (($#)); do cmd+=(-F "$1") fields_text+="$1"$'\n' shift done next_trace_id request_id="$CURRENT_TRACE_ID" trace_upload_request "$request_id" "$url" "$file_path" "$fields_text" raw_output=$("${cmd[@]}" -w $'\n__PAM_HTTP_CODE__:%{http_code}' 2>&1) || curl_exit=$? if [[ "$raw_output" == *"$marker"* ]]; then response="${raw_output%"$marker"*}" http_code="${raw_output##*__PAM_HTTP_CODE__:}" else response="$raw_output" fi trace_response "$request_id" "$curl_exit" "$http_code" "$response" if (( curl_exit != 0 )); then log_error "上传失败: $response" return 1 fi if [[ -n "$http_code" ]] && ! is_success_http_code "$http_code"; then log_error "上传返回非成功状态码: ${url} -> HTTP ${http_code}" log_error "$response" return 1 fi printf '%s' "$response" } get_token() { log_info "正在获取 Token..." local response response=$(http_request "POST" \ "${HOME_BASE_URL}/oauth/token" \ "grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \ "application/x-www-form-urlencoded") || { log_error "获取 Token 失败: $response" return 1 } TOKEN="$(json_value "$response" '.access_token')" if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then log_error "Token 响应无效: $response" return 1 fi } create_version() { log_info "Step 2.1: 新建版本..." http_request "POST" \ "${HOME_BASE_URL}/api/version/upgrade" \ "versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&description=Auto Deploy" \ "application/x-www-form-urlencoded" >/dev/null } upload_package() { log_info "Step 2.2: 上传软件包..." local response response=$(upload_file \ "${HOME_BASE_URL}/api/version/upgrade/upload" \ "$ZIP_FILE_PATH" \ "applicationName=${APP_NAME}" \ "moduleName=${MODULE_NAME}" \ "versionNumber=${VERSION_NUMBER}") || return 1 HASH_CODE="$(json_value "$response" '.hashCode // .data.hashCode')" if [[ -z "$HASH_CODE" ]]; then HASH_CODE="$(sanitize_field "$response")" fi if [[ -z "$HASH_CODE" ]]; then log_error "无法从上传响应中解析 hashCode: $response" return 1 fi } publish_version() { log_info "Step 2.3: 发布版本..." local payload payload=$(printf '{"airportCodesWhite":["%s"],"hashCode":"%s","state":"RELEASE"}' "$AIRPORT_CODE" "$HASH_CODE") http_request "PUT" \ "${HOME_BASE_URL}/api/version/upgrade/profile?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}" \ "$payload" \ "application/json" >/dev/null } get_node_url() { log_info "Step 3.1: 获取 Node 地址..." local response response=$(http_request "GET" \ "${HOME_BASE_URL}/api/mcp/airport/target-node?airportCode=${AIRPORT_CODE}" \ "" \ "") || return 1 NODE_URL="$(json_first_key "$response")" if [[ -z "$NODE_URL" ]]; then log_error "无法获取 Node 地址: $response" return 1 fi case "$NODE_URL" in http://*|https://*) ;; *) log_error "Target-Node 不是有效 URL: $NODE_URL" return 1 ;; esac if [[ "$NODE_URL" == *" "* || "$NODE_URL" == *$'\t'* || "$NODE_URL" == *$'\r'* || "$NODE_URL" == *$'\n'* ]]; then log_error "Target-Node 包含非法空白字符: $NODE_URL" return 1 fi } get_online_ips() { log_info "Step 3.2: 获取在线工作站列表..." local response local ip_lines response=$(http_request "GET" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/ips?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}" \ "" \ "" \ "Target-Node: ${NODE_URL}") || return 1 ONLINE_IPS=() ip_lines="$(json_array_items "$response")" while IFS= read -r ip; do [[ -n "$ip" ]] && ONLINE_IPS+=("$ip") done <<< "$ip_lines" TOTAL_COUNT=${#ONLINE_IPS[@]} if (( TOTAL_COUNT == 0 )); then log_error "无在线工作站匹配该模块。原始响应: $response" return 1 fi } poll_download_progress() { local progress_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud/progress?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&versionNumber=${VERSION_NUMBER}" local attempt=0 local max_attempts=60 local error_regex='[Ff]ail|[Ee]rror' DOWNLOAD_PROGRESS_STATUS="" DOWNLOAD_PROGRESS_SUCCESS="" DOWNLOAD_PROGRESS_STEP="" DOWNLOAD_PROGRESS_MSG="" DOWNLOAD_PROGRESS_MESSAGE="" DOWNLOAD_PROGRESS_RATE="" DOWNLOAD_PROGRESS_RESPONSE="" while (( attempt < max_attempts )); do local response response=$(http_request "GET" "$progress_url" "" "" "Target-Node: ${NODE_URL}") || return 1 local status status="$(json_value "$response" '.status')" local success_flag success_flag="$(json_value "$response" '.success')" local step_value step_value="$(json_value "$response" '.step')" local msg_value msg_value="$(json_value "$response" '.msg')" local message message="$(json_value "$response" '.message')" local progress_value progress_value="$(json_value "$response" '.rateOfProgress')" [[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.progress')" [[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.percent')" [[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.data.progress')" [[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.data.percent')" [[ -z "$message" ]] && message="$msg_value" DOWNLOAD_PROGRESS_STATUS="$status" DOWNLOAD_PROGRESS_SUCCESS="$success_flag" DOWNLOAD_PROGRESS_STEP="$step_value" DOWNLOAD_PROGRESS_MSG="$msg_value" DOWNLOAD_PROGRESS_MESSAGE="$message" DOWNLOAD_PROGRESS_RATE="$progress_value" DOWNLOAD_PROGRESS_RESPONSE="$response" if [[ -n "$msg_value" || -n "$step_value" || -n "$progress_value" || -n "$status" || -n "$success_flag" || -n "$message" ]]; then local -a progress_parts=() [[ -n "$msg_value" ]] && progress_parts+=("msg=${msg_value}") [[ -n "$step_value" ]] && progress_parts+=("step=${step_value}") [[ -n "$progress_value" ]] && progress_parts+=("rateOfProgress=${progress_value}") [[ -n "$status" ]] && progress_parts+=("status=${status}") [[ -n "$success_flag" ]] && progress_parts+=("success=${success_flag}") [[ -n "$message" && "$message" != "$msg_value" ]] && progress_parts+=("message=${message}") log_info "Step 3.3b: 异步下载进度 -> ${progress_parts[*]}" else log_info "Step 3.3b: 异步下载进度轮询中... ($((attempt + 1))/${max_attempts})" fi if [[ "$step_value" == "DONE" || "$status" == "completed" || "$success_flag" == "true" ]]; then return 0 fi if [[ "$msg_value" == "success" && "$progress_value" == "100" ]]; then return 0 fi if [[ "${step_value} ${message} ${msg_value}" =~ $error_regex ]]; then [[ -z "$message" ]] && message="$step_value" [[ -z "$message" ]] && message="$msg_value" log_error "Node 下载失败: $message" return 1 fi attempt=$((attempt + 1)) sleep 2 done log_error "Node 下载超时。" return 1 } create_download_task() { log_info "Step 3.3: 下载软件包到 Node..." http_request "GET" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=0" \ "" \ "" \ "Target-Node: ${NODE_URL}" \ "airport-code: ${AIRPORT_CODE}" >/dev/null } download_cloud_to_node() { create_download_task || return 1 poll_download_progress } poll_upgrade_progress() { local ip="$1" local progress_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/progress?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&versionNumber=${VERSION_NUMBER}" local attempt=0 local max_attempts=600 local error_regex='[Ff]ail|[Ee]rror' UPGRADE_PROGRESS_STATUS="" UPGRADE_PROGRESS_SUCCESS="" UPGRADE_PROGRESS_STEP="" UPGRADE_PROGRESS_MSG="" UPGRADE_PROGRESS_MESSAGE="" UPGRADE_PROGRESS_RATE="" UPGRADE_PROGRESS_CODE="" UPGRADE_PROGRESS_FINISH="" UPGRADE_PROGRESS_LAST_MODIFY="" UPGRADE_PROGRESS_RESPONSE="" while (( attempt < max_attempts )); do local response response=$(http_request "GET" "$progress_url" "" "" "Target-Node: ${NODE_URL}") || return 1 local status status="$(json_scoped_value "$response" "$ip" '.status')" local success_flag success_flag="$(json_scoped_value "$response" "$ip" '.success')" local step_value step_value="$(json_scoped_value "$response" "$ip" '.step')" local msg_value msg_value="$(json_scoped_value "$response" "$ip" '.msg')" local message message="$(json_scoped_value "$response" "$ip" '.message')" local progress_value progress_value="$(json_scoped_value "$response" "$ip" '.rateOfProgress')" [[ -z "$progress_value" ]] && progress_value="$(json_scoped_value "$response" "$ip" '.progress')" [[ -z "$progress_value" ]] && progress_value="$(json_scoped_value "$response" "$ip" '.percent')" local code_value code_value="$(json_scoped_value "$response" "$ip" '.code')" local finish_value finish_value="$(json_scoped_value "$response" "$ip" '.finish')" local last_modify_value last_modify_value="$(json_scoped_value "$response" "$ip" '.lastModify')" [[ -z "$message" ]] && message="$msg_value" UPGRADE_PROGRESS_STATUS="$status" UPGRADE_PROGRESS_SUCCESS="$success_flag" UPGRADE_PROGRESS_STEP="$step_value" UPGRADE_PROGRESS_MSG="$msg_value" UPGRADE_PROGRESS_MESSAGE="$message" UPGRADE_PROGRESS_RATE="$progress_value" UPGRADE_PROGRESS_CODE="$code_value" UPGRADE_PROGRESS_FINISH="$finish_value" UPGRADE_PROGRESS_LAST_MODIFY="$last_modify_value" UPGRADE_PROGRESS_RESPONSE="$response" if [[ -n "$msg_value" || -n "$step_value" || -n "$progress_value" || -n "$status" || -n "$success_flag" || -n "$message" || -n "$code_value" || -n "$finish_value" || -n "$last_modify_value" ]]; then local -a progress_parts=() progress_parts+=("ip=${ip}") [[ -n "$msg_value" ]] && progress_parts+=("msg=${msg_value}") [[ -n "$step_value" ]] && progress_parts+=("step=${step_value}") [[ -n "$progress_value" ]] && progress_parts+=("rateOfProgress=${progress_value}") [[ -n "$code_value" ]] && progress_parts+=("code=${code_value}") [[ -n "$finish_value" ]] && progress_parts+=("finish=${finish_value}") [[ -n "$status" ]] && progress_parts+=("status=${status}") [[ -n "$success_flag" ]] && progress_parts+=("success=${success_flag}") [[ -n "$last_modify_value" ]] && progress_parts+=("lastModify=${last_modify_value}") [[ -n "$message" && "$message" != "$msg_value" ]] && progress_parts+=("message=${message}") log_info "Step 3.4a: async push progress -> ${progress_parts[*]}" else log_info "Step 3.4a: async push progress polling... ip=${ip} ($((attempt + 1))/${max_attempts})" fi if [[ "$step_value" == "DONE" || "$finish_value" == "true" || "$status" == "completed" || "$success_flag" == "true" ]]; then return 0 fi if [[ "$msg_value" == "success" && "$progress_value" == "100" ]] && [[ -z "$code_value" || "$code_value" == "0" ]]; then return 0 fi if [[ -n "$code_value" && "$code_value" != "0" ]]; then [[ -z "$message" ]] && message="$msg_value" [[ -z "$message" ]] && message="$step_value" [[ -z "$message" ]] && message="code=${code_value}" log_error "Node push failed: ip=${ip}, message=${message}" return 1 fi if [[ "${step_value} ${message} ${msg_value} ${status}" =~ $error_regex ]]; then [[ -z "$message" ]] && message="$step_value" [[ -z "$message" ]] && message="$msg_value" log_error "Node push failed: ip=${ip}, message=${message}" return 1 fi attempt=$((attempt + 1)) sleep 2 done log_error "Node push timed out: ip=${ip}" return 1 } upgrade_ip() { local ip="$1" http_request "POST" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade?airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}&action=${ACTION_TYPE}&autoStart=false&timeOut=0" \ "" \ "" \ "Target-Node: ${NODE_URL}" } start_application() { local ip="$1" http_request "POST" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop?airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runStart=true" \ "" \ "" \ "Target-Node: ${NODE_URL}" >/dev/null } stop_application() { local ip="$1" http_request "POST" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop?airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runStart=false" \ "" \ "" \ "Target-Node: ${NODE_URL}" >/dev/null } verify_ip() { local ip="$1" http_request "GET" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/verify?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}" \ "" \ "" \ "Target-Node: ${NODE_URL}" } download_log() { local ip="$1" local logs_dir="${SCRIPT_DIR}/logs" local log_file="${logs_dir}/deploy_${ip}.zip" local err_file="${logs_dir}/error_${ip}.log" local request_id local trace_url="${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/log-download?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}&logName=${LOG_NAME}" local curl_exit=0 local http_code="" local trace_error="" mkdir -p "$logs_dir" next_trace_id request_id="$CURRENT_TRACE_ID" trace_request "$request_id" "GET" "$trace_url" "Authorization: Bearer ${TOKEN}"$'\n'"Target-Node: ${NODE_URL}" "" if curl -sS -X GET \ "$trace_url" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Target-Node: ${NODE_URL}" \ -o "$log_file" \ -w '%{http_code}' > "${err_file}.code" 2>"$err_file"; then http_code="$(cat "${err_file}.code" 2>/dev/null)" if [[ -s "$log_file" ]]; then { printf 'Archive format: zip\n' printf 'Saved path: %s\n' "$log_file" printf 'Size: %s bytes\n' "$(wc -c < "$log_file" | tr -d ' ')" } > "${log_file}.summary" else printf 'Zip archive empty or no data\n' > "${log_file}.summary" fi else curl_exit=$? printf 'Log download failed. See %s\n' "$err_file" > "$log_file" printf 'Log download failed\n' > "${log_file}.summary" fi trace_error="$(cat "$err_file" 2>/dev/null)" trace_download_result "$request_id" "$trace_url" "$http_code" "$curl_exit" "$log_file" "$trace_error" rm -f "${err_file}.code" if (( curl_exit != 0 )); then printf '%s' "$log_file" return 1 fi if [[ -n "$http_code" ]] && ! is_success_http_code "$http_code"; then printf 'HTTP %s\n' "$http_code" > "${log_file}.summary" printf '%s' "$log_file" return 1 fi printf '%s' "$log_file" } rollback_ip() { local ip="$1" local stop_first="$2" if [[ "$stop_first" == "true" ]]; then stop_application "$ip" >/dev/null 2>&1 || true fi local response if ! response=$(http_request "POST" \ "${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/rollback" \ "airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}" \ "application/x-www-form-urlencoded" \ "Target-Node: ${NODE_URL}"); then printf '%s' "ROLLBACK_REQUEST_FAILED" return 0 fi local rollback_success rollback_success="$(json_value "$response" '.success')" if [[ -n "$rollback_success" && "$rollback_success" != "true" ]]; then printf '%s' "ROLLBACK_FAILED" return 0 fi local verify_response if ! verify_response="$(verify_ip "$ip")"; then printf '%s' "ROLLBACK_VERIFY_FAILED" return 0 fi if [[ "$(json_value "$verify_response" '.success')" == "true" ]]; then printf '%s' "ROLLBACK_SUCCESS" else printf '%s' "ROLLBACK_VERIFY_FAILED" fi } run_manual_rollback() { local config_path="$1" local ip="$2" local stop_first="$3" local rollback_result="" ACTIVE_CONFIG_PATH="$config_path" init_runtime load_config "$config_path" ensure_dependencies log_info "PAM 手动回滚开始" log_info "机场: ${AIRPORT_CODE}, 目标IP: ${ip}, stopFirst=${stop_first}" run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_capture rollback_result "rollback_ip[${ip}]" rollback_ip "$ip" "$stop_first" || return 1 log_info "手动回滚完成: ip=${ip}, result=${rollback_result}" printf 'ROLLBACK RESULT: %s\n' "$rollback_result" } run_action() { local config_path="$1" local action="$2" local ip="$3" local hash_code="$4" local stop_first="$5" local trace_file="$6" local response="" local log_file="" local rollback_result="" ACTIVE_CONFIG_PATH="$config_path" API_TRACE_FILE="$trace_file" init_runtime load_config "$config_path" ensure_dependencies case "$action" in get-token) run_flow_step "get_token" get_token || return 1 result_line "ACTION" "get-token" result_line "TOKEN" "$TOKEN" ;; create-version) run_flow_step "get_token" get_token || return 1 run_flow_step "create_version" create_version || return 1 result_line "ACTION" "create-version" result_line "VERSION_NUMBER" "$VERSION_NUMBER" result_line "RESULT" "OK" ;; upload-package) ensure_zip_file || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "upload_package" upload_package || return 1 result_line "ACTION" "upload-package" result_line "HASH_CODE" "$HASH_CODE" ;; publish-version) require_hash_code_arg "$hash_code" || return 1 HASH_CODE="$hash_code" run_flow_step "get_token" get_token || return 1 run_flow_step "publish_version" publish_version || return 1 result_line "ACTION" "publish-version" result_line "HASH_CODE" "$HASH_CODE" result_line "RESULT" "OK" ;; get-node-url) run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 result_line "ACTION" "get-node-url" result_line "NODE_URL" "$NODE_URL" ;; get-online-ips) run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "get_online_ips" get_online_ips || return 1 print_online_ips_result ;; create-download-task) run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "create_download_task" create_download_task || return 1 result_line "ACTION" "create-download-task" result_line "TIME_OUT" "0" result_line "RESULT" "TASK_CREATED" ;; poll-download-progress) run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "poll_download_progress" poll_download_progress || return 1 print_progress_result ;; download-cloud-to-node) run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "download_cloud_to_node" download_cloud_to_node || return 1 print_progress_result "download-cloud-to-node" result_line "RESULT" "DONE" ;; poll-upgrade-progress) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "poll_upgrade_progress[${ip}]" poll_upgrade_progress "$ip" || return 1 print_upgrade_progress_result "$ip" ;; upgrade-ip) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_capture response "upgrade_ip[${ip}]" upgrade_ip "$ip" || return 1 if response_indicates_failure "$response"; then local message message="$(response_message "$response")" [[ -z "$message" ]] && message="Upgrade task creation failed" log_error "Upgrade task creation failed: ip=${ip}, message=${message}" return 1 fi result_line "ACTION" "upgrade-ip" result_line "IP" "$ip" result_line "TIME_OUT" "0" result_line "RESULT" "TASK_CREATED" result_line "SUCCESS" "$(json_value "$response" '.success')" result_line "MESSAGE" "$(response_message "$response")" result_line "RAW_RESPONSE" "$(sanitize_field "$response")" ;; start-ip) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "start_application[${ip}]" start_application "$ip" || return 1 result_line "ACTION" "start-ip" result_line "IP" "$ip" result_line "RUN_START" "true" result_line "RESULT" "OK" ;; stop-ip) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_step "stop_application[${ip}]" stop_application "$ip" || return 1 result_line "ACTION" "stop-ip" result_line "IP" "$ip" result_line "RUN_START" "false" result_line "RESULT" "OK" ;; verify-ip) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_capture response "verify_ip[${ip}]" verify_ip "$ip" || return 1 result_line "ACTION" "verify-ip" result_line "IP" "$ip" result_line "SUCCESS" "$(json_value "$response" '.success')" result_line "MESSAGE" "$(json_value "$response" '.message')" result_line "RAW_RESPONSE" "$(sanitize_field "$response")" ;; download-log) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true result_line "ACTION" "download-log" result_line "IP" "$ip" result_line "LOG_FILE" "$log_file" ;; rollback-ip) require_ip_arg "$ip" || return 1 run_flow_step "get_token" get_token || return 1 run_flow_step "get_node_url" get_node_url || return 1 run_flow_capture rollback_result "rollback_ip[${ip}]" rollback_ip "$ip" "$stop_first" || return 1 result_line "ACTION" "rollback-ip" result_line "IP" "$ip" result_line "STOP_FIRST" "$stop_first" result_line "ROLLBACK_RESULT" "$rollback_result" ;; *) log_error "未知 action: $action" return 1 ;; esac if [[ -n "$API_TRACE_FILE" ]]; then result_line "TRACE_FILE" "$API_TRACE_FILE" fi } add_result() { local ip="$1" local status="$2" local stage="$3" local message="$4" local rollback="$5" local log_file="$6" printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ "$(sanitize_field "$ip")" \ "$(sanitize_field "$status")" \ "$(sanitize_field "$stage")" \ "$(sanitize_field "$message")" \ "$(sanitize_field "$rollback")" \ "$(sanitize_field "$log_file")" >> "$RESULTS_FILE" if [[ "$status" == "SUCCESS" ]]; then SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) else FAIL_COUNT=$((FAIL_COUNT + 1)) fi } deploy_one_ip() { local ip="$1" log_info "处理工作站: $ip" local upgrade_response="" if ! run_flow_capture upgrade_response "upgrade_ip[${ip}]" upgrade_ip "$ip"; then local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "UPGRADE" "Upgrade request failed" "ROLLBACK_NOT_RUN" "$log_file" return fi if response_indicates_failure "$upgrade_response"; then local message message="$(response_message "$upgrade_response")" [[ -z "$message" ]] && message="Upgrade task creation failed" local rollback_result rollback_result="$(pending_rollback_status "$ip" "UPGRADE" "$message" "false")" local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "UPGRADE" "$message" "$rollback_result" "$log_file" return fi if ! run_flow_step "poll_upgrade_progress[${ip}]" poll_upgrade_progress "$ip"; then local message message="$UPGRADE_PROGRESS_MESSAGE" [[ -z "$message" ]] && message="$UPGRADE_PROGRESS_MSG" [[ -z "$message" ]] && message="$UPGRADE_PROGRESS_STEP" [[ -z "$message" ]] && message="Upgrade progress polling failed" local rollback_result rollback_result="$(pending_rollback_status "$ip" "UPGRADE_PROGRESS" "$message" "false")" local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "UPGRADE_PROGRESS" "$message" "$rollback_result" "$log_file" return fi if ! run_flow_step "start_application[${ip}]" start_application "$ip"; then local rollback_result rollback_result="$(pending_rollback_status "$ip" "START" "Application start failed" "true")" local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "START" "Application start failed" "$rollback_result" "$log_file" return fi local verify_response="" if ! run_flow_capture verify_response "verify_ip[${ip}]" verify_ip "$ip"; then local rollback_result rollback_result="$(pending_rollback_status "$ip" "VERIFY" "Health check request failed" "true")" local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "VERIFY" "Health check request failed" "$rollback_result" "$log_file" return fi if [[ "$(json_value "$verify_response" '.success')" == "true" ]]; then local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "SUCCESS" "-" "-" "-" "$log_file" return fi local message message="$(json_value "$verify_response" '.message')" [[ -z "$message" ]] && message="Health check failed" local rollback_result rollback_result="$(pending_rollback_status "$ip" "VERIFY" "$message" "true")" local log_file="" run_flow_capture log_file "download_log[${ip}]" download_log "$ip" || true add_result "$ip" "FAILED" "VERIFY" "$message" "$rollback_result" "$log_file" } print_report() { printf '\n====================== 部署报告 ======================\n' printf '模式: Shell\n' printf '机场: %s\n' "$AIRPORT_CODE" printf '应用: %s\n' "$APP_NAME" printf '模块: %s\n' "$MODULE_NAME" printf '版本: %s\n' "$VERSION_NUMBER" printf '总工作站数: %s\n' "$TOTAL_COUNT" printf '成功: %s\n' "$SUCCESS_COUNT" printf '失败: %s\n' "$FAIL_COUNT" printf '\n%-18s %-8s %-12s %-22s %s\n' "IP" "状态" "失败阶段" "回滚结果" "日志" while IFS=$'\t' read -r ip status stage message rollback log_file; do printf '%-18s %-8s %-12s %-22s %s\n' "$ip" "$status" "$stage" "$rollback" "$log_file" if [[ "$status" != "SUCCESS" ]]; then printf ' 原因: %s\n' "$message" fi done < "$RESULTS_FILE" } cleanup() { [[ -n "$RESULTS_FILE" && -f "$RESULTS_FILE" ]] && rm -f "$RESULTS_FILE" } init_runtime() { RESULTS_FILE="$(mktemp "${TMPDIR:-/tmp}/pam_deploy_results.XXXXXX")" trap cleanup EXIT } main() { local config_path="$DEFAULT_CONFIG_PATH" local action="" local action_ip="" local action_hash_code="" local action_stop_first="false" local action_trace_file="" local manual_rollback_ip="" local manual_rollback_stop_first="false" while (($#)); do case "$1" in --config) [[ $# -lt 2 ]] && { log_error "--config 缺少路径"; exit 1; } config_path="$2" shift 2 ;; --rollback-ip) [[ $# -lt 2 ]] && { log_error "--rollback-ip 缺少IP"; exit 1; } manual_rollback_ip="$2" shift 2 ;; --action) [[ $# -lt 2 ]] && { log_error "--action 缺少名称"; exit 1; } action="$2" shift 2 ;; --ip) [[ $# -lt 2 ]] && { log_error "--ip 缺少目标IP"; exit 1; } action_ip="$2" shift 2 ;; --hash-code) [[ $# -lt 2 ]] && { log_error "--hash-code 缺少值"; exit 1; } action_hash_code="$2" shift 2 ;; --trace-file) [[ $# -lt 2 ]] && { log_error "--trace-file 缺少路径"; exit 1; } action_trace_file="$2" shift 2 ;; --stop-first) action_stop_first="true" manual_rollback_stop_first="true" shift ;; --rollback-stop-first) manual_rollback_stop_first="true" action_stop_first="true" shift ;; -h|--help) usage exit 0 ;; *) log_error "未知参数: $1" usage exit 1 ;; esac done ACTIVE_CONFIG_PATH="$config_path" if [[ -n "$action" ]]; then run_action "$config_path" "$action" "$action_ip" "$action_hash_code" "$action_stop_first" "$action_trace_file" return fi if [[ -n "$manual_rollback_ip" ]]; then run_manual_rollback "$config_path" "$manual_rollback_ip" "$manual_rollback_stop_first" return fi init_runtime load_config "$config_path" ensure_dependencies ensure_zip_file || exit 1 log_info "PAM 智能部署开始" log_info "机场: ${AIRPORT_CODE}, 版本: ${VERSION_NUMBER}, 模块: ${APP_NAME}/${MODULE_NAME}" run_flow_step "get_token" get_token || exit 1 run_flow_step "create_version" create_version || exit 1 run_flow_step "upload_package" upload_package || exit 1 run_flow_step "publish_version" publish_version || exit 1 run_flow_step "get_node_url" get_node_url || exit 1 run_flow_step "get_online_ips" get_online_ips || exit 1 run_flow_step "download_cloud_to_node" download_cloud_to_node || exit 1 for ip in "${ONLINE_IPS[@]}"; do deploy_one_ip "$ip" done print_report } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi