#!/usr/bin/env bash # PAM 部署主脚本(Shell 入口)。 set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_CONFIG_PATH="${SCRIPT_DIR}/config.txt" TOKEN="" HASH_CODE="" NODE_URL="" TOTAL_COUNT=0 SUCCESS_COUNT=0 FAIL_COUNT=0 RESULTS_FILE="" ONLINE_IPS=() HAS_JQ=0 API_TRACE_FILE="" API_TRACE_SEQ=0 TRACE_ANNOUNCED=0 CURRENT_TRACE_ID="" usage() { cat <<'EOF' 用法: ./deploy.sh [--config /path/to/config.txt] 配置项: 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' "$*"; } log_warn() { printf '[WARN] %s\n' "$*"; } log_error() { printf '[ERROR] %s\n' "$*" >&2; } timestamp_now() { date '+%Y-%m-%d %H:%M:%S' } ensure_trace_file() { if [[ -n "$API_TRACE_FILE" ]]; then return 0 fi local trace_dir="${SCRIPT_DIR}/logs" local trace_name mkdir -p "$trace_dir" trace_name="api_trace_$(date '+%Y%m%d_%H%M%S')_$$.log" API_TRACE_FILE="${trace_dir}/${trace_name}" { printf 'PAM API TRACE LOG\n' printf 'Started: %s\n' "$(timestamp_now)" printf 'ScriptDir: %s\n' "$SCRIPT_DIR" printf '\n' } > "$API_TRACE_FILE" 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 if command -v jq >/dev/null 2>&1; then HAS_JQ=1 else HAS_JQ=0 log_warn "未检测到 jq,将使用 Bash 兼容 JSON 解析。" fi } 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" } json_value() { local input="$1" local query="$2" if (( HAS_JQ == 1 )); then printf '%s' "$input" | jq -r "$query // empty" 2>/dev/null return 0 fi 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" ;; '.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" } 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 compact="$(json_compact "$input")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$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 compact="$(json_compact "$input")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$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 compact="$(json_compact "$input")" value="$(printf '%s' "$compact" | sed -nE 's/.*"'$parent_key'"[[:space:]]*:[[:space:]]*\{[^}]*"'$child_key'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p')" [[ -n "$value" ]] && json_unescape_basic "$value" } json_first_key() { local input="$1" if (( HAS_JQ == 1 )); then printf '%s' "$input" | jq -r 'keys[0] // empty' 2>/dev/null return 0 fi local compact compact="$(json_compact "$input")" printf '%s' "$compact" | sed -nE 's/^[[:space:]]*\{[[:space:]]*"([^"]+)".*/\1/p' } json_array_items() { local input="$1" if (( HAS_JQ == 1 )); then printf '%s' "$input" | jq -r '.[]' 2>/dev/null return 0 fi 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' } 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}" local attempt=0 local max_attempts=60 local error_regex='[Ff]ail|[Ee]rror' 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')" if [[ "$status" == "completed" || "$success_flag" == "true" ]]; then return 0 fi local message message="$(json_value "$response" '.message')" if [[ "$message" =~ $error_regex ]]; then log_error "Node 下载失败: $message" return 1 fi attempt=$((attempt + 1)) sleep 2 done log_error "Node 下载超时。" return 1 } download_cloud_to_node() { 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=${TIMEOUT}" \ "" \ "" \ "Target-Node: ${NODE_URL}" \ "airport-code: ${AIRPORT_CODE}" >/dev/null poll_download_progress } 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=${TIMEOUT}" \ "application/x-www-form-urlencoded" \ "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" \ "application/x-www-form-urlencoded" \ "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" \ "application/x-www-form-urlencoded" \ "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}.log" 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 tail -n 5 "$log_file" > "${log_file}.summary" 2>/dev/null || true else printf 'Log content 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 } 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 ! upgrade_response=$(upgrade_ip "$ip"); then local log_file log_file="$(download_log "$ip")" add_result "$ip" "FAILED" "UPGRADE" "Upgrade request failed" "ROLLBACK_NOT_RUN" "$log_file" return fi if [[ "$(json_value "$upgrade_response" '.success')" != "true" ]]; then local rollback_result rollback_result="$(rollback_ip "$ip" "false")" local log_file log_file="$(download_log "$ip")" local message message="$(json_value "$upgrade_response" '.message')" [[ -z "$message" ]] && message="Upgrade failed" add_result "$ip" "FAILED" "UPGRADE" "$message" "$rollback_result" "$log_file" return fi if ! start_application "$ip"; then local rollback_result rollback_result="$(rollback_ip "$ip" "true")" local log_file log_file="$(download_log "$ip")" add_result "$ip" "FAILED" "START" "Application start failed" "$rollback_result" "$log_file" return fi local verify_response if ! verify_response="$(verify_ip "$ip")"; then local rollback_result rollback_result="$(rollback_ip "$ip" "true")" local log_file log_file="$(download_log "$ip")" 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 log_file="$(download_log "$ip")" add_result "$ip" "SUCCESS" "-" "-" "-" "$log_file" return fi local rollback_result rollback_result="$(rollback_ip "$ip" "true")" local log_file log_file="$(download_log "$ip")" local message message="$(json_value "$verify_response" '.message')" [[ -z "$message" ]] && message="Health check failed" 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" while (($#)); do case "$1" in --config) [[ $# -lt 2 ]] && { log_error "--config 缺少路径"; exit 1; } config_path="$2" shift 2 ;; -h|--help) usage exit 0 ;; *) log_error "未知参数: $1" usage exit 1 ;; esac done 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}" get_token create_version upload_package publish_version get_node_url get_online_ips download_cloud_to_node for ip in "${ONLINE_IPS[@]}"; do deploy_one_ip "$ip" done print_report } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@" fi