1028 lines
29 KiB
Bash
1028 lines
29 KiB
Bash
#!/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
|