dark e572a26e6f pam_deploy_graph/agent.py:progress action 未完成不标记 completed,超时暂停在当前 action,支持断点继续。
llm 提示词和规则:新增 progress_complete 判断字段。
deploy.sh / deploy.ps1:poll-* action 入口改为单次查询。
interactive.py:chat 会播报进度更新。
config.txt.example / README / packaging 文档 / Skill 文档:同步进度查询参数和新 workflow 语义。
测试补充了进度重复查询、超时暂停、chat 进度播报。
2026-06-04 16:28:18 +08:00

1738 lines
55 KiB
Bash

#!/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 <name> [--ip <target-ip>] [--hash-code <hash>] [--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
POLL_INTERVAL_SEC
DOWNLOAD_POLL_MAX_ATTEMPTS
UPGRADE_POLL_MAX_ATTEMPTS
说明:
--action poll-download-progress 和 poll-upgrade-progress 只执行一次进度查询。
Agent workflow 会重复调用单次进度查询,并在每次返回后交给 LLM/规则审核判断是否完成。
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}"
: "${POLL_INTERVAL_SEC:=2}"
: "${DOWNLOAD_POLL_MAX_ATTEMPTS:=60}"
: "${UPGRADE_POLL_MAX_ATTEMPTS:=600}"
}
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|POLL_INTERVAL_SEC|DOWNLOAD_POLL_MAX_ATTEMPTS|UPGRADE_POLL_MAX_ATTEMPTS)
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 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=""
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: 异步下载进度单次查询未返回明确进度字段。"
fi
if [[ "${step_value} ${message} ${msg_value} ${status}" =~ $error_regex ]]; then
[[ -z "$message" ]] && message="$step_value"
[[ -z "$message" ]] && message="$msg_value"
log_error "Node 下载失败: $message"
return 1
fi
return 0
}
download_progress_complete() {
[[ "$DOWNLOAD_PROGRESS_STEP" == "DONE" || "$DOWNLOAD_PROGRESS_STATUS" == "completed" || "$DOWNLOAD_PROGRESS_SUCCESS" == "true" ]] && return 0
[[ "$DOWNLOAD_PROGRESS_MSG" == "success" && "$DOWNLOAD_PROGRESS_RATE" == "100" ]] && return 0
return 1
}
wait_download_progress() {
local attempt=0
local max_attempts="${DOWNLOAD_POLL_MAX_ATTEMPTS:-60}"
local interval_sec="${POLL_INTERVAL_SEC:-2}"
[[ "$max_attempts" =~ ^[0-9]+$ ]] || max_attempts=60
[[ -n "$interval_sec" ]] || interval_sec=2
while (( attempt < max_attempts )); do
poll_download_progress || return 1
if download_progress_complete; then
return 0
fi
attempt=$((attempt + 1))
log_info "Step 3.3b: 异步下载进度未完成,等待下一次查询... (${attempt}/${max_attempts})"
sleep "$interval_sec"
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
wait_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 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=""
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 single query -> ${progress_parts[*]}"
else
log_info "Step 3.4a: async push progress single query returned no explicit progress fields: ip=${ip}"
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
return 0
}
upgrade_progress_complete() {
[[ "$UPGRADE_PROGRESS_STEP" == "DONE" || "$UPGRADE_PROGRESS_FINISH" == "true" || "$UPGRADE_PROGRESS_STATUS" == "completed" || "$UPGRADE_PROGRESS_SUCCESS" == "true" ]] && return 0
[[ "$UPGRADE_PROGRESS_MSG" == "success" && "$UPGRADE_PROGRESS_RATE" == "100" ]] && [[ -z "$UPGRADE_PROGRESS_CODE" || "$UPGRADE_PROGRESS_CODE" == "0" ]] && return 0
return 1
}
wait_upgrade_progress() {
local ip="$1"
local attempt=0
local max_attempts="${UPGRADE_POLL_MAX_ATTEMPTS:-600}"
local interval_sec="${POLL_INTERVAL_SEC:-2}"
[[ "$max_attempts" =~ ^[0-9]+$ ]] || max_attempts=600
[[ -n "$interval_sec" ]] || interval_sec=2
while (( attempt < max_attempts )); do
poll_upgrade_progress "$ip" || return 1
if upgrade_progress_complete; then
return 0
fi
attempt=$((attempt + 1))
log_info "Step 3.4a: async push progress not complete, waiting for next query... ip=${ip} (${attempt}/${max_attempts})"
sleep "$interval_sec"
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 "wait_upgrade_progress[${ip}]" wait_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