2026-05-19 17:19:51 +08:00

1028 lines
29 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
;;
'.msg')
json_get_string_by_key "$input" "msg"
;;
'.step')
json_get_string_by_key "$input" "step"
;;
'.rateOfProgress')
json_get_scalar_by_key "$input" "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"
}
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}&versionNumber=${VERSION_NUMBER}"
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')"
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"
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
}
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