dark 143cd76c6a 1、新增action给agent使用
2、新增SKILL描述
2026-05-20 16:58:01 +08:00

1395 lines
42 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"
ACTIVE_CONFIG_PATH="$DEFAULT_CONFIG_PATH"
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=""
DOWNLOAD_PROGRESS_STATUS=""
DOWNLOAD_PROGRESS_SUCCESS=""
DOWNLOAD_PROGRESS_STEP=""
DOWNLOAD_PROGRESS_MSG=""
DOWNLOAD_PROGRESS_MESSAGE=""
DOWNLOAD_PROGRESS_RATE=""
DOWNLOAD_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]
配置项:
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; }
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
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"
}
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"
}
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'
DOWNLOAD_PROGRESS_STATUS=""
DOWNLOAD_PROGRESS_SUCCESS=""
DOWNLOAD_PROGRESS_STEP=""
DOWNLOAD_PROGRESS_MSG=""
DOWNLOAD_PROGRESS_MESSAGE=""
DOWNLOAD_PROGRESS_RATE=""
DOWNLOAD_PROGRESS_RESPONSE=""
while (( attempt < max_attempts )); do
local response
response=$(http_request "GET" "$progress_url" "" "" "Target-Node: ${NODE_URL}") || return 1
local status
status="$(json_value "$response" '.status')"
local success_flag
success_flag="$(json_value "$response" '.success')"
local step_value
step_value="$(json_value "$response" '.step')"
local msg_value
msg_value="$(json_value "$response" '.msg')"
local message
message="$(json_value "$response" '.message')"
local progress_value
progress_value="$(json_value "$response" '.rateOfProgress')"
[[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.progress')"
[[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.percent')"
[[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.data.progress')"
[[ -z "$progress_value" ]] && progress_value="$(json_value "$response" '.data.percent')"
[[ -z "$message" ]] && message="$msg_value"
DOWNLOAD_PROGRESS_STATUS="$status"
DOWNLOAD_PROGRESS_SUCCESS="$success_flag"
DOWNLOAD_PROGRESS_STEP="$step_value"
DOWNLOAD_PROGRESS_MSG="$msg_value"
DOWNLOAD_PROGRESS_MESSAGE="$message"
DOWNLOAD_PROGRESS_RATE="$progress_value"
DOWNLOAD_PROGRESS_RESPONSE="$response"
if [[ -n "$msg_value" || -n "$step_value" || -n "$progress_value" || -n "$status" || -n "$success_flag" || -n "$message" ]]; then
local -a progress_parts=()
[[ -n "$msg_value" ]] && progress_parts+=("msg=${msg_value}")
[[ -n "$step_value" ]] && progress_parts+=("step=${step_value}")
[[ -n "$progress_value" ]] && progress_parts+=("rateOfProgress=${progress_value}")
[[ -n "$status" ]] && progress_parts+=("status=${status}")
[[ -n "$success_flag" ]] && progress_parts+=("success=${success_flag}")
[[ -n "$message" && "$message" != "$msg_value" ]] && progress_parts+=("message=${message}")
log_info "Step 3.3b: 异步下载进度 -> ${progress_parts[*]}"
else
log_info "Step 3.3b: 异步下载进度轮询中... ($((attempt + 1))/${max_attempts})"
fi
if [[ "$step_value" == "DONE" || "$status" == "completed" || "$success_flag" == "true" ]]; then
return 0
fi
if [[ "$msg_value" == "success" && "$progress_value" == "100" ]]; then
return 0
fi
if [[ "${step_value} ${message} ${msg_value}" =~ $error_regex ]]; then
[[ -z "$message" ]] && message="$step_value"
[[ -z "$message" ]] && message="$msg_value"
log_error "Node 下载失败: $message"
return 1
fi
attempt=$((attempt + 1))
sleep 2
done
log_error "Node 下载超时。"
return 1
}
create_download_task() {
log_info "Step 3.3: 下载软件包到 Node..."
http_request "GET" \
"${HOME_BASE_URL}/node-proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=0" \
"" \
"" \
"Target-Node: ${NODE_URL}" \
"airport-code: ${AIRPORT_CODE}" >/dev/null
}
download_cloud_to_node() {
create_download_task || return 1
poll_download_progress
}
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}" \
"" \
"" \
"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}.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
}
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 response=""
local log_file=""
local rollback_result=""
ACTIVE_CONFIG_PATH="$config_path"
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"
;;
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
result_line "ACTION" "upgrade-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")"
;;
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
}
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 [[ "$(json_value "$upgrade_response" '.success')" != "true" ]]; then
local message
message="$(json_value "$upgrade_response" '.message')"
[[ -z "$message" ]] && message="Upgrade 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 "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 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
;;
--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"
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