新增自动部署相关脚本和说明
This commit is contained in:
parent
064f9adaab
commit
753078d345
141
doc_scripts/PAM智能部署 Agent Skill 文档.md.md
Normal file
141
doc_scripts/PAM智能部署 Agent Skill 文档.md.md
Normal file
@ -0,0 +1,141 @@
|
||||
---
|
||||
name: pam-smart-deploy
|
||||
description: 基于 PAM HOME/NODE 流程执行软件发布、下载、升级、回滚、健康检查和日志采集。用于用户要求按机场、版本、软件包部署 PAM 应用,并需要根据用户输入在 MCP 直连部署和 API 脚本部署(config.txt + deploy.sh / deploy.ps1 / deploy.bat)之间切换时。
|
||||
---
|
||||
|
||||
# PAM智能部署 Skill
|
||||
|
||||
按用户意图在 `MCP` 与 `API脚本` 两种模式之间选择,并完成以下闭环:HOME 端建版与发布、NODE 端下载、在线工作站动态发现、逐台升级、启动、健康检测、日志下载、失败回滚、结果汇总。
|
||||
|
||||
## 模式选择
|
||||
|
||||
1. 先识别用户期望的执行入口。
|
||||
- 用户明确说“用 MCP”“直接执行”“在线部署”“不要生成脚本”:使用 `MCP`。
|
||||
- 用户明确说“用脚本”“生成脚本”“输出 sh / ps1 / bat / config”“离线执行”“批量复用”:使用 `API脚本`。
|
||||
- 用户只说“帮我部署”,且当前环境存在可用的 PAM MCP:默认 `MCP`。
|
||||
- 用户只说“给我部署脚本”或“不要直接动环境”:默认 `API脚本`,且默认只生成文件不执行。
|
||||
2. 在两种模式都可行但用户意图不清时,只追问一个问题:`这次要直接通过 MCP 执行,还是生成/运行 API 脚本?`
|
||||
3. 在开始执行前,明确告知本次采用的模式。
|
||||
4. 用户已经明确指定模式后,不要静默切换。若当前模式不可用,先说明原因,再请求切换到另一种模式。
|
||||
|
||||
## 输入参数
|
||||
|
||||
先收集并标准化以下参数。脚本模式下,优先写入 `config.txt`。
|
||||
|
||||
| 规范字段 | 脚本字段 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `HOME_BASE_URL` | `HOME_BASE_URL` | 是 | PAM HOME 基础地址 |
|
||||
| `client_id` | `CLIENT_ID` | 是 | OAuth 客户端 ID |
|
||||
| `client_secret` | `CLIENT_SECRET` | 是 | OAuth 客户端密钥 |
|
||||
| `airportCode` | `AIRPORT_CODE` | 是 | 目标机场三字码 |
|
||||
| `applicationName` | `APP_NAME` | 是 | 软件名称 |
|
||||
| `moduleName` | `MODULE_NAME` | 是 | 模块名称 |
|
||||
| `versionNumber` | `VERSION_NUMBER` | 是 | 目标版本号 |
|
||||
| `zipFilePath` | `ZIP_FILE_PATH` | 是 | 本地软件包路径 |
|
||||
| `actionType` | `ACTION_TYPE` | 否 | 升级类型,默认 `FULL` |
|
||||
| `timeOut` | `TIMEOUT` | 否 | 单步超时,默认 `120` |
|
||||
| `logName` | `LOG_NAME` | 否 | 需下载的日志文件名,默认 `app.log` |
|
||||
|
||||
## 通用执行原则
|
||||
|
||||
1. 始终通过接口动态获取在线工作站 IP,不要要求用户手填 `TARGET_IPS`。
|
||||
2. 用户若指定单个或部分 IP,先调用在线 IP 接口,再对返回列表做过滤,不要跳过在线性校验。
|
||||
3. 任一步骤失败时,保留该步骤的原始响应、错误摘要和当前阶段名称。
|
||||
4. `TOKEN` 失效时,自动重取一次并重试当前步骤一次。
|
||||
5. 无论单机部署成功或失败,都下载对应日志。
|
||||
6. 在线工作站列表为空时,终止部署并明确报告“无在线工作站匹配该模块”。
|
||||
7. 用户要求“只生成脚本”“先给我文件”“不要执行”时,不要触发真实部署。
|
||||
8. Windows 脚本模式默认优先 `deploy.ps1`,不要默认使用 `deploy.bat`。
|
||||
9. 当前目录如果只有文档而没有真实脚本文件,先根据参考实现落地脚本,再决定是否执行。
|
||||
|
||||
## 统一部署流程
|
||||
|
||||
两种模式都遵循同一业务顺序,差异只在“通过 MCP 直接执行”还是“通过脚本封装执行”。
|
||||
|
||||
| 阶段 | 操作 | 关键接口 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 获取 Token | `POST {HOME_BASE_URL}/oauth/token` |
|
||||
| 2.1 | 新建版本记录 | `POST {HOME_BASE_URL}/api/version/upgrade` |
|
||||
| 2.2 | 上传软件包 | `POST {HOME_BASE_URL}/api/version/upgrade/upload` |
|
||||
| 2.3 | 发布版本 | `PUT {HOME_BASE_URL}/api/version/upgrade/profile?...` |
|
||||
| 3.1 | 获取 Node 地址 | `GET {HOME_BASE_URL}/api/mcp/airport/target-node?airportCode={airportCode}` |
|
||||
| 3.2 | 获取在线工作站 IP | `GET {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/ips?...` |
|
||||
| 3.3 | 下载软件包到 Node | `GET {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/download-cloud?...` |
|
||||
| 3.3b | 轮询下载进度 | `GET .../download-cloud/progress?...` |
|
||||
| 4.1 | 对每个 IP 执行升级 | `POST {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade` |
|
||||
| 4.2 | 启动应用 | `POST {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/start-stop` |
|
||||
| 4.3 | 健康检测 | `GET {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/verify?...` |
|
||||
| 4.4 | 下载日志 | `GET {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/log-download?...` |
|
||||
| 4.x | 失败回滚 | `POST {HOME_BASE_URL}/node_proxy/{airportCode}/api/mcp/version/upgrade/rollback` |
|
||||
|
||||
调用 NODE 侧接口时,始终携带:
|
||||
|
||||
- `Authorization: Basic {TOKEN}`
|
||||
- `Target-Node: {NODE_URL}`
|
||||
- `airport-code: {airportCode}`,仅在下载到 NODE 等需要时携带
|
||||
|
||||
## MCP 模式
|
||||
|
||||
1. 直接调用 PAM MCP 提供的能力完成上述流程,不生成本地脚本文件。
|
||||
2. 若 MCP 暴露的是高层工具,确保其实际覆盖以下关键环节:建版、上传、发布、取 Node、取在线 IP、下载到 Node、逐台升级、启动、校验、日志下载、回滚。
|
||||
3. 若 MCP 暴露的是通用 HTTP/REST 能力,则按“统一部署流程”中的接口顺序执行。
|
||||
4. 在 `MCP` 模式下,仍然要输出逐台 IP 的结果,不要只给出整体成功或失败。
|
||||
5. 用户若额外要求“顺手生成脚本留档”,可在部署完成后再生成脚本产物,但不要把脚本生成作为 MCP 主路径的前置步骤。
|
||||
|
||||
## API脚本模式
|
||||
|
||||
仅在脚本模式下读取并使用 `PAM智能部署 Shell & Bat 脚本实现.md.md` 作为参考实现。
|
||||
|
||||
1. 先把规范字段映射为脚本字段,并写入 `config.txt`。
|
||||
2. 根据操作系统选择脚本入口。
|
||||
- Linux / Mac:使用 `deploy.sh`。
|
||||
- Windows:优先使用 `deploy.ps1`。
|
||||
- `deploy.bat` 只在用户明确要求 Batch,或必须兼容旧入口,且确认特殊字符风险可接受时才使用。
|
||||
3. 若当前目录只有文档而没有真实脚本文件,先从参考实现中落地实际脚本文件,再执行。
|
||||
4. 若用户要求“只生成脚本不执行”,完成以下产物后即可结束:
|
||||
- `config.txt`
|
||||
- `deploy.sh` 或 `deploy.ps1`
|
||||
- 如用户明确要求,再额外提供 `deploy.bat`
|
||||
5. 执行脚本后,读取脚本输出和 `./logs/` 目录内容,整理成最终报告。
|
||||
|
||||
## 失败处理与回滚
|
||||
|
||||
1. `Step 2` 或 `Step 3` 失败时,终止整个部署,并指出失败阶段。
|
||||
2. `Step 4.1` 升级失败时,记录该 IP 失败原因,尝试回滚,然后继续处理其他在线 IP,除非用户要求全有或全无。
|
||||
3. `Step 4.3` 健康检测失败时,执行以下顺序:
|
||||
- 调用 `start-stop` 停止应用,`runstart=false`
|
||||
- 调用 `rollback`
|
||||
- 再次执行健康检测
|
||||
- 下载回滚阶段日志
|
||||
4. 回滚是否成功也必须写入最终报告,不能仅记录“已尝试回滚”。
|
||||
|
||||
## 输出要求
|
||||
|
||||
最终输出至少包含:
|
||||
|
||||
- 本次模式:`MCP` 或 `API脚本`
|
||||
- 机场、应用、模块、版本
|
||||
- 在线工作站总数、成功数、失败数
|
||||
- 每个 IP 的状态、失败阶段、失败原因、回滚结果、日志位置或日志摘要
|
||||
- 如果是脚本模式:实际生成或执行的文件名
|
||||
|
||||
可按以下结构输出:
|
||||
|
||||
```markdown
|
||||
## PAM 智能部署报告
|
||||
|
||||
- 模式: MCP
|
||||
- 机场: HET
|
||||
- 应用: PAM
|
||||
- 模块: Node
|
||||
- 版本: 2.0.5
|
||||
- 总工作站数: 3
|
||||
- 成功: 2
|
||||
- 失败: 1
|
||||
|
||||
| IP | 状态 | 失败阶段 | 回滚结果 | 日志 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 192.168.1.10 | Success | - | - | logs/deploy_192.168.1.10.log |
|
||||
| 192.168.1.11 | Success | - | - | logs/deploy_192.168.1.11.log |
|
||||
| 192.168.1.12 | Failed | Health Check | Rollback Failed | logs/deploy_192.168.1.12.log |
|
||||
```
|
||||
759
doc_scripts/PAM智能部署 Shell & Bat 脚本实现.md.md
Normal file
759
doc_scripts/PAM智能部署 Shell & Bat 脚本实现.md.md
Normal file
@ -0,0 +1,759 @@
|
||||
# PAM智能部署 Shell & Bat 脚本实现
|
||||
|
||||
## 0. 与 Skill 对齐的使用约定
|
||||
|
||||
本文是 `API脚本` 模式的参考实现,不是 `MCP` 模式说明。Agent 使用本文时,先遵循以下规则:
|
||||
|
||||
1. 用户明确要求 `MCP`、直接在线执行、不要生成脚本时,不要把本文作为主执行路径。
|
||||
2. 用户明确要求“脚本部署”“生成脚本”“离线执行”“输出 config / sh / ps1 / bat”时,再读取本文并生成或执行脚本。
|
||||
3. 当前目录如果只有本文档而没有真实脚本文件,Agent 需要先把对应代码块落地为真实文件,再决定是否执行。
|
||||
4. Windows 默认优先 `deploy.ps1`;`deploy.bat` 仅作为兼容入口或示例入口,不应作为默认正式方案。
|
||||
5. 当前文档中的字段名与 Skill 规范字段映射如下:
|
||||
|
||||
| Skill 字段 | 脚本字段 |
|
||||
| --- | --- |
|
||||
| `HOME_BASE_URL` | `HOME_BASE_URL` |
|
||||
| `client_id` | `CLIENT_ID` |
|
||||
| `client_secret` | `CLIENT_SECRET` |
|
||||
| `airportCode` | `AIRPORT_CODE` |
|
||||
| `applicationName` | `APP_NAME` |
|
||||
| `moduleName` | `MODULE_NAME` |
|
||||
| `versionNumber` | `VERSION_NUMBER` |
|
||||
| `zipFilePath` | `ZIP_FILE_PATH` |
|
||||
| `actionType` | `ACTION_TYPE` |
|
||||
| `timeOut` | `TIMEOUT` |
|
||||
| `logName` | `LOG_NAME` |
|
||||
|
||||
6. 若用户只要求“生成脚本不执行”,则产物至少应包含:
|
||||
- `config.txt`
|
||||
- `deploy.sh` 或 `deploy.ps1`
|
||||
- 仅在用户明确要求时再提供 `deploy.bat`
|
||||
|
||||
## 0.1 当前实现边界
|
||||
|
||||
1. Linux / Mac 路径以 `deploy.sh` 为主。
|
||||
2. Windows 文档里虽然展示了 `deploy.bat`,但它更适合作为兼容入口,不适合作为默认生产入口。
|
||||
3. 若要在 Windows 正式落地脚本模式,建议整理出独立 `deploy.ps1`,再由 Agent 优先执行该文件。
|
||||
|
||||
## 1. 配置文件规范 (config.txt)
|
||||
|
||||
为了方便管理敏感信息(如 `client_secret`)和特殊字符,建议将配置信息存储在 `config.txt`文件中。脚本会自动读取该文件。
|
||||
|
||||
**config.txt 格式:**
|
||||
|
||||
```ini
|
||||
HOME_BASE_URL=https://pam.home.com
|
||||
CLIENT_ID=your_client_id
|
||||
CLIENT_SECRET=MySecret!Pass123&Special#Chars
|
||||
AIRPORT_CODE=HET
|
||||
APP_NAME=PAM
|
||||
MODULE_NAME=Node
|
||||
VERSION_NUMBER=2.0.5
|
||||
ZIP_FILE_PATH=/path/to/pam-2.0.5.zip
|
||||
; TARGET_IPS 已废弃,脚本将自动从接口获取在线工作站 IP
|
||||
ACTION_TYPE=FULL
|
||||
TIMEOUT=120
|
||||
LOG_NAME=app.log ; 要下载的日志文件名,可根据实际软件调整
|
||||
```
|
||||
|
||||
**注意:**
|
||||
|
||||
1. `=` 左侧为变量名,右侧为值。
|
||||
|
||||
2. **不要**在值两侧加引号(除非是 Windows Batch 中无法避免的情况,建议尽量不加)。
|
||||
|
||||
3. 特殊字符如 `!`、`%`、`&` 等在配置文件中是安全的,脚本会原样读取。
|
||||
|
||||
4. Windows 和 Linux 脚本都优先读取同目录下的 `config.txt`。如果不存在,则回退到脚本内定义的默认值。
|
||||
|
||||
|
||||
* * *
|
||||
|
||||
## 2. Linux/Mac Shell 脚本 (deploy.sh)
|
||||
|
||||
_(此部分与之前版本一致,Shell 脚本对特殊字符的处理比 Batch 友好得多,无需修改)_
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
# ==============================================================================
|
||||
# PAM 智能部署脚本 (Linux/Mac)
|
||||
# ==============================================================================
|
||||
|
||||
# --- 读取配置文件 ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="${SCRIPT_DIR}/config.txt"
|
||||
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
echo "[INFO] 正在读取配置文件: ${CONFIG_FILE}"
|
||||
while IFS='=' read -r key value; do
|
||||
# 忽略注释和空行
|
||||
[[ "$key" =~ ^#.*$ ]] && continue
|
||||
[[ -z "$key" ]] && continue
|
||||
|
||||
# 去除两端空格
|
||||
key=$(echo "$key" | xargs)
|
||||
value=$(echo "$value" | xargs)
|
||||
|
||||
# 动态设置变量
|
||||
declare "$key=$value"
|
||||
done < "$CONFIG_FILE"
|
||||
else
|
||||
echo "[WARN] 未找到配置文件 ${CONFIG_FILE},使用默认配置"
|
||||
fi
|
||||
|
||||
# --- 默认配置 (如果 config.txt 缺失或未定义) ---
|
||||
HOME_BASE_URL="${HOME_BASE_URL:-https://pam.home.com}"
|
||||
CLIENT_ID="${CLIENT_ID:-your_client_id}"
|
||||
CLIENT_SECRET="${CLIENT_SECRET:-your_client_secret}"
|
||||
AIRPORT_CODE="${AIRPORT_CODE:-HET}"
|
||||
APP_NAME="${APP_NAME:-PAM}"
|
||||
MODULE_NAME="${MODULE_NAME:-Node}"
|
||||
VERSION_NUMBER="${VERSION_NUMBER:-2.0.5}"
|
||||
ZIP_FILE_PATH="${ZIP_FILE_PATH:-/path/to/pam-2.0.5.zip}"
|
||||
ACTION_TYPE="${ACTION_TYPE:-FULL}"
|
||||
TIMEOUT="${TIMEOUT:-120}"
|
||||
LOG_NAME="${LOG_NAME:-app.log}"
|
||||
|
||||
# --- 全局变量 ---
|
||||
TOKEN=""
|
||||
HASH_CODE=""
|
||||
NODE_URL=""
|
||||
SUCCESS_COUNT=0
|
||||
FAIL_COUNT=0
|
||||
TOTAL_COUNT=0
|
||||
RESULTS_JSON="[]" # 用于存储结果,便于最终报告生成
|
||||
|
||||
# --- 颜色输出 ---
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# --- 工具函数 ---
|
||||
|
||||
# 获取 Token
|
||||
get_token() {
|
||||
log_info "正在获取 Token..."
|
||||
# 注意:移除了 -s 参数,以便 Agent 能看到 curl 的详细错误信息
|
||||
local response
|
||||
response=$(curl -X POST "${HOME_BASE_URL}/oauth/token" \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=${CLIENT_ID}" \
|
||||
-d "client_secret=${CLIENT_SECRET}" 2>&1)
|
||||
|
||||
TOKEN=$(echo $response | jq -r '.access_token' 2>/dev/null)
|
||||
|
||||
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
|
||||
log_error "获取 Token 失败: $response"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Token 获取成功"
|
||||
}
|
||||
|
||||
# 通用 HTTP 请求函数
|
||||
http_request() {
|
||||
local method=$1
|
||||
local url=$2
|
||||
local data=$3
|
||||
local headers=$4
|
||||
|
||||
local curl_cmd="curl -X ${method} '${url}'"
|
||||
|
||||
# 添加认证头
|
||||
curl_cmd="${curl_cmd} -H 'Authorization: Basic ${TOKEN}'"
|
||||
|
||||
# 添加额外 headers
|
||||
if [ -n "$headers" ]; then
|
||||
IFS=',' read -ra HEADER_ARRAY <<< "$headers"
|
||||
for header in "${HEADER_ARRAY[@]}"; do
|
||||
curl_cmd="${curl_cmd} -H '${header}'"
|
||||
done
|
||||
fi
|
||||
|
||||
# 添加数据
|
||||
if [ -n "$data" ]; then
|
||||
curl_cmd="${curl_cmd} -d '${data}'"
|
||||
fi
|
||||
|
||||
# 执行请求,重定向 stderr 到 stdout 以便捕获网络错误
|
||||
eval $curl_cmd 2>&1
|
||||
}
|
||||
|
||||
# 上传文件函数
|
||||
upload_file() {
|
||||
local url=$1
|
||||
local file_path=$2
|
||||
local form_data=$3
|
||||
|
||||
local curl_cmd="curl -X POST '${url}' -H 'Authorization: Basic ${TOKEN}'"
|
||||
|
||||
# 添加 form 字段
|
||||
IFS='&' read -ra FORM_ARRAY <<< "$form_data"
|
||||
for field in "${FORM_ARRAY[@]}"; do
|
||||
key=$(echo $field | cut -d'=' -f1)
|
||||
value=$(echo $field | cut -d'=' -f2)
|
||||
curl_cmd="${curl_cmd} -F '${key}=${value}'"
|
||||
done
|
||||
|
||||
curl_cmd="${curl_cmd} -F 'file=@${file_path}'"
|
||||
|
||||
# 执行请求,捕获错误信息
|
||||
eval $curl_cmd 2>&1
|
||||
}
|
||||
|
||||
# 轮询进度函数
|
||||
poll_progress() {
|
||||
local url=$1
|
||||
local max_retries=${2:-60}
|
||||
local interval=${3:-2}
|
||||
local retry=0
|
||||
|
||||
log_info "开始轮询进度..."
|
||||
|
||||
while [ $retry -lt $max_retries ]; do
|
||||
local response
|
||||
response=$(http_request "GET" "$url")
|
||||
|
||||
local status
|
||||
# 尝试解析 JSON,如果失败则可能是网络错误
|
||||
status=$(echo $response | jq -r '.status // .success // ""' 2>/dev/null)
|
||||
|
||||
if [ "$status" == "completed" ] || [ "$status" == "true" ]; then
|
||||
log_info "操作完成"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local error_msg
|
||||
error_msg=$(echo $response | jq -r '.message // ""' 2>/dev/null)
|
||||
|
||||
# 如果 jq 解析失败,说明返回的可能不是 JSON,而是 HTTP 错误页或 curl 错误
|
||||
if [ -z "$error_msg" ] && ! echo "$response" | grep -q "success"; then
|
||||
log_error "请求异常,原始响应: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if echo "$error_msg" | grep -qi "fail\|error"; then
|
||||
log_error "操作失败: $error_msg"
|
||||
return 1
|
||||
fi
|
||||
|
||||
retry=$((retry + 1))
|
||||
log_info "等待中... ($retry/$max_retries)"
|
||||
sleep $interval
|
||||
done
|
||||
|
||||
log_error "操作超时"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 下载日志并保存
|
||||
download_log() {
|
||||
local ip=$1
|
||||
local log_name=$2
|
||||
local log_dir="./logs"
|
||||
|
||||
mkdir -p "$log_dir"
|
||||
local log_file="${log_dir}/deploy_${ip}.log"
|
||||
|
||||
log_info "正在下载 ${ip} 的日志: ${log_name}..."
|
||||
|
||||
# 使用 -o 保存文件,同时保留 stderr 信息以便调试
|
||||
http_request "GET" "${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}" \
|
||||
"" \
|
||||
"Target-Node:${NODE_URL}" > "$log_file" 2>>"${log_dir}/error_${ip}.log"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
# 检查文件大小,如果为0可能是空响应但HTTP成功
|
||||
if [ ! -s "$log_file" ]; then
|
||||
log_warn "日志文件为空 (HTTP可能返回204或无内容): ${ip}"
|
||||
echo "Log content empty or no data" > "${log_file}.summary"
|
||||
else
|
||||
tail -n 5 "$log_file" > "${log_file}.summary" 2>/dev/null || true
|
||||
log_info "日志已保存至: ${log_file}"
|
||||
fi
|
||||
else
|
||||
log_error "日志下载命令执行失败 (请查看 error_${ip}.log)"
|
||||
echo "Command execution failed, see error_${ip}.log" > "$log_file"
|
||||
echo "Execution failed" > "${log_file}.summary"
|
||||
fi
|
||||
}
|
||||
|
||||
# 添加结果到报告数据
|
||||
add_result() {
|
||||
local ip=$1
|
||||
local status=$2
|
||||
local message=$3
|
||||
|
||||
RESULTS_JSON=$(echo $RESULTS_JSON | jq --arg ip "$ip" --arg status "$status" --arg msg "$message" '. + [{"ip": $ip, "status": $status, "message": $msg}]')
|
||||
|
||||
if [ "$status" == "SUCCESS" ]; then
|
||||
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
|
||||
else
|
||||
FAIL_COUNT=$((FAIL_COUNT + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# --- 主流程 ---
|
||||
|
||||
main() {
|
||||
log_info "=========================================="
|
||||
log_info "PAM 智能部署开始"
|
||||
log_info "机场: ${AIRPORT_CODE}, 版本: ${VERSION_NUMBER}"
|
||||
log_info "模块: ${APP_NAME}/${MODULE_NAME}"
|
||||
log_info "日志文件名: ${LOG_NAME}"
|
||||
log_info "=========================================="
|
||||
|
||||
# Step 1: 获取 Token
|
||||
get_token
|
||||
|
||||
# Step 2.1: 新建版本
|
||||
log_info "Step 2.1: 新建版本..."
|
||||
local create_version_url="${HOME_BASE_URL}/api/version/upgrade"
|
||||
local create_version_data="versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&description=Auto Deploy"
|
||||
http_request "POST" "$create_version_url" "$create_version_data" > /dev/null
|
||||
log_info "版本创建完成"
|
||||
|
||||
# Step 2.2: 上传软件包
|
||||
log_info "Step 2.2: 上传软件包..."
|
||||
local upload_url="${HOME_BASE_URL}/api/version/upgrade/upload"
|
||||
local upload_form="applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}"
|
||||
HASH_CODE=$(upload_file "$upload_url" "$ZIP_FILE_PATH" "$upload_form")
|
||||
log_info "软件包上传完成,Hash: ${HASH_CODE}"
|
||||
|
||||
# Step 2.3: 发布版本
|
||||
log_info "Step 2.3: 发布版本..."
|
||||
local publish_url="${HOME_BASE_URL}/api/version/upgrade/profile?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}"
|
||||
local publish_data="{\"airportCodesWhite\":[\"${AIRPORT_CODE}\"],\"hashCode\":\"${HASH_CODE}\",\"state\":\"RELEASE\"}"
|
||||
http_request "PUT" "$publish_url" "$publish_data" > /dev/null
|
||||
log_info "版本发布完成"
|
||||
|
||||
# Step 3.1: 获取 Node URL
|
||||
log_info "Step 3.1: 获取 Node 地址..."
|
||||
local node_info_url="${HOME_BASE_URL}/api/mcp/airport/target-node?airportCode=${AIRPORT_CODE}"
|
||||
local node_response
|
||||
node_response=$(http_request "GET" "$node_info_url")
|
||||
NODE_URL=$(echo $node_response | jq -r 'keys[0]')
|
||||
|
||||
if [ -z "$NODE_URL" ] || [ "$NODE_URL" == "null" ]; then
|
||||
log_error "无法获取 Node 地址. Response: $node_response"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Node URL: ${NODE_URL}"
|
||||
|
||||
# Step 3.2: 获取在线工作站 IP (动态获取)
|
||||
log_info "Step 3.2: 获取在线工作站列表..."
|
||||
local ips_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/ips?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}"
|
||||
|
||||
local ips_response
|
||||
ips_response=$(http_request "GET" "$ips_url" "" "Target-Node:${NODE_URL}")
|
||||
|
||||
# 解析 IP 数组
|
||||
local ip_array
|
||||
ip_array=($(echo $ips_response | jq -r '.[]' 2>/dev/null))
|
||||
|
||||
TOTAL_COUNT=${#ip_array[@]}
|
||||
|
||||
if [ $TOTAL_COUNT -eq 0 ]; then
|
||||
log_warn "未找到任何在线工作站!部署终止。"
|
||||
log_warn "原始响应: $ips_response"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log_info "找到 ${TOTAL_COUNT} 个在线工作站: ${ip_array[*]}"
|
||||
|
||||
# Step 3.3: 下载软件包到 Node
|
||||
log_info "Step 3.3: 下载软件包到 Node..."
|
||||
local download_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud"
|
||||
local download_params="?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}"
|
||||
|
||||
http_request "GET" "${download_url}${download_params}" \
|
||||
"" \
|
||||
"airport-code:${AIRPORT_CODE},Target-Node:${NODE_URL}" > /dev/null
|
||||
|
||||
# 轮询下载进度
|
||||
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}"
|
||||
poll_progress "$progress_url" 60 2
|
||||
if [ $? -ne 0 ]; then
|
||||
log_error "软件包下载失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: 升级推送至每个工作站 (动态循环)
|
||||
for ip in "${ip_array[@]}"; do
|
||||
log_info "------------------------------------------"
|
||||
log_info "处理工作站: ${ip}"
|
||||
log_info "------------------------------------------"
|
||||
|
||||
local ip_status="SUCCESS"
|
||||
local ip_message=""
|
||||
|
||||
# 4.1: 执行升级
|
||||
log_info "Step 4.1: 执行升级..."
|
||||
local upgrade_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade"
|
||||
local upgrade_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}&action=${ACTION_TYPE}&autoStart=false&timeOut=${TIMEOUT}"
|
||||
|
||||
local upgrade_response
|
||||
upgrade_response=$(http_request "POST" "$upgrade_url" "$upgrade_data" "Target-Node:${NODE_URL}")
|
||||
|
||||
local upgrade_success
|
||||
upgrade_success=$(echo $upgrade_response | jq -r '.success' 2>/dev/null)
|
||||
|
||||
if [ "$upgrade_success" != "true" ]; then
|
||||
ip_status="FAILURE"
|
||||
ip_message="Upgrade failed"
|
||||
log_error "升级失败: $upgrade_response"
|
||||
log_warn "尝试回滚..."
|
||||
|
||||
# 回滚逻辑
|
||||
local rollback_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/rollback"
|
||||
local rollback_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}"
|
||||
http_request "POST" "$rollback_url" "$rollback_data" "Target-Node:${NODE_URL}" > /dev/null
|
||||
log_warn "已触发回滚"
|
||||
else
|
||||
# 4.2: 启动应用
|
||||
log_info "Step 4.2: 启动应用..."
|
||||
local start_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop"
|
||||
local start_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runstart=true"
|
||||
http_request "POST" "$start_url" "$start_data" "Target-Node:${NODE_URL}" > /dev/null
|
||||
|
||||
# 4.3: 健康检测
|
||||
log_info "Step 4.3: 健康检测..."
|
||||
local verify_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/verify?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}"
|
||||
local verify_response
|
||||
verify_response=$(http_request "GET" "$verify_url" "" "Target-Node:${NODE_URL}")
|
||||
|
||||
local verify_success
|
||||
verify_success=$(echo $verify_response | jq -r '.success' 2>/dev/null)
|
||||
|
||||
if [ "$verify_success" != "true" ]; then
|
||||
ip_status="FAILURE"
|
||||
ip_message=$(echo $verify_response | jq -r '.message // "Unknown error"' 2>/dev/null)
|
||||
log_error "健康检测失败: ${ip_message} (原始响应: $verify_response)"
|
||||
|
||||
# 健康检测失败也可触发回滚(可选)
|
||||
log_warn "尝试回滚..."
|
||||
local rollback_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/rollback"
|
||||
local rollback_data="airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}"
|
||||
http_request "POST" "$rollback_url" "$rollback_data" "Target-Node:${NODE_URL}" > /dev/null
|
||||
else
|
||||
log_info "健康检测通过"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4.4: 下载日志 (无论成功与否)
|
||||
download_log "$ip" "${LOG_NAME}"
|
||||
|
||||
# 记录结果
|
||||
add_result "$ip" "$ip_status" "$ip_message"
|
||||
done
|
||||
|
||||
# Step 5: 生成报告
|
||||
log_info "=========================================="
|
||||
log_info "PAM 智能部署结束"
|
||||
log_info "=========================================="
|
||||
|
||||
echo ""
|
||||
echo "====================== 部署报告 ======================"
|
||||
echo -e "机场: ${AIRPORT_CODE} | 版本: ${VERSION_NUMBER}"
|
||||
echo -e "总数: ${TOTAL_COUNT} | ${GREEN}成功: ${SUCCESS_COUNT}${NC} | ${RED}失败: ${FAIL_COUNT}${NC}"
|
||||
echo "-----------------------------------------------------"
|
||||
|
||||
# 打印每个 IP 的状态
|
||||
for ip in "${ip_array[@]}"; do
|
||||
local status
|
||||
status=$(echo $RESULTS_JSON | jq -r --arg ip "$ip" '.[] | select(.ip==$ip) | .status')
|
||||
|
||||
if [ "$status" == "SUCCESS" ]; then
|
||||
echo -e "[${GREEN}SUCCESS${NC}] ${ip}"
|
||||
else
|
||||
local msg
|
||||
msg=$(echo $RESULTS_JSON | jq -r --arg ip "$ip" '.[] | select(.ip==$ip) | .message')
|
||||
echo -e "[${RED}FAILED${NC}] ${ip} (Reason: ${msg:-N/A})"
|
||||
fi
|
||||
done
|
||||
echo "======================================================"
|
||||
log_info "详细日志文件保存在 ./logs/ 目录下"
|
||||
log_info "网络错误日志保存在 ./logs/error_*.log 文件中"
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
```
|
||||
|
||||
## 3. Windows Batch 脚本 (deploy.bat) - **兼容入口,不建议作为默认正式方案**
|
||||
|
||||
本节主要用于兼容旧入口,或说明 Batch 在特殊字符场景下的处理方式。即使引入了 **PowerShell 辅助读取配置并安全传递变量**,它仍不适合作为默认正式方案。
|
||||
|
||||
```bat
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: ==============================================================================
|
||||
:: PAM 智能部署脚本 (Windows) - 特殊字符安全版
|
||||
:: ==============================================================================
|
||||
|
||||
:: --- 读取配置文件 (使用 PowerShell 避免 Batch 解析问题) ---
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "CONFIG_FILE=%SCRIPT_DIR%config.txt"
|
||||
|
||||
if exist "%CONFIG_FILE%" (
|
||||
echo [INFO] 正在读取配置文件: %CONFIG_FILE%
|
||||
:: 使用 PowerShell 读取 key=value 文件,并输出为 set 命令序列
|
||||
powershell -Command "$content = Get-Content '%CONFIG_FILE%'; foreach($line in $content) { if($line -match '^(.*?)=(.*)$') { Write-Output ('set "%%A=%%B"') } }" > config_set.tmp
|
||||
|
||||
:: 执行生成的 set 命令
|
||||
for /f "usebackq delims=" %%L in ("config_set.tmp") do (
|
||||
%%L
|
||||
)
|
||||
|
||||
:: 清理临时文件
|
||||
del /q config_set.tmp >nul 2>nul
|
||||
) else (
|
||||
echo [WARN] 未找到配置文件 %CONFIG_FILE%,使用默认配置
|
||||
)
|
||||
|
||||
:: --- 默认配置 (如果 config.txt 缺失或未定义) ---
|
||||
set "HOME_BASE_URL=%HOME_BASE_URL: =%"
|
||||
if "%HOME_BASE_URL%"=="" set "HOME_BASE_URL=https://pam.home.com"
|
||||
set "CLIENT_ID=%CLIENT_ID: =%"
|
||||
if "%CLIENT_ID%"=="" set "CLIENT_ID=your_client_id"
|
||||
set "CLIENT_SECRET=%CLIENT_SECRET: =%"
|
||||
if "%CLIENT_SECRET%"=="" set "CLIENT_SECRET=your_client_secret"
|
||||
set "AIRPORT_CODE=%AIRPORT_CODE: =%"
|
||||
if "%AIRPORT_CODE%"=="" set "AIRPORT_CODE=HET"
|
||||
set "APP_NAME=%APP_NAME: =%"
|
||||
if "%APP_NAME%"=="" set "APP_NAME=PAM"
|
||||
set "MODULE_NAME=%MODULE_NAME: =%"
|
||||
if "%MODULE_NAME%"=="" set "MODULE_NAME=Node"
|
||||
set "VERSION_NUMBER=%VERSION_NUMBER: =%"
|
||||
if "%VERSION_NUMBER%"=="" set "VERSION_NUMBER=2.0.5"
|
||||
set "ZIP_FILE_PATH=%ZIP_FILE_PATH: =%"
|
||||
if "%ZIP_FILE_PATH%"=="" set "ZIP_FILE_PATH=C:\path\to\pam-2.0.5.zip"
|
||||
set "ACTION_TYPE=%ACTION_TYPE: =%"
|
||||
if "%ACTION_TYPE%"=="" set "ACTION_TYPE=FULL"
|
||||
set "TIMEOUT=%TIMEOUT: =%"
|
||||
if "%TIMEOUT%"=="" set "TIMEOUT=120"
|
||||
set "LOG_NAME=%LOG_NAME: =%"
|
||||
if "%LOG_NAME%"=="" set "LOG_NAME=app.log"
|
||||
|
||||
:: --- 全局变量 ---
|
||||
set "TOKEN="
|
||||
set "HASH_CODE="
|
||||
set "NODE_URL="
|
||||
set "SUCCESS_COUNT=0"
|
||||
set "FAIL_COUNT=0"
|
||||
set "TOTAL_COUNT=0"
|
||||
|
||||
:: 临时文件用于存储结果
|
||||
set "RESULTS_FILE=%TEMP%\deploy_results.json"
|
||||
echo [] > "%RESULTS_FILE%"
|
||||
|
||||
:: --- 工具函数 ---
|
||||
|
||||
:: 获取 Token (使用 PowerShell 确保密码安全)
|
||||
:GET_TOKEN
|
||||
echo [INFO] 正在获取 Token...
|
||||
|
||||
:: 通过 PowerShell 构造 POST 请求,避免 Batch 转义问题
|
||||
powershell -Command "$id = '%CLIENT_ID%'; $sec = '%CLIENT_SECRET%'; $url = '%HOME_BASE_URL%/oauth/token'; $body = 'grant_type=client_credentials&client_id=' + $id + '&client_secret=' + $sec; Invoke-RestMethod -Uri $url -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' | ConvertTo-Json" > token_response.json
|
||||
|
||||
:: 检查 PowerShell 是否成功
|
||||
if not exist token_response.json (
|
||||
echo [ERROR] PowerShell 执行失败,请检查网络或配置。
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
for /f "delims=" %%i in ('powershell -Command "(Get-Content token_response.json | ConvertFrom-Json).access_token"') do (
|
||||
set "TOKEN=%%i"
|
||||
)
|
||||
|
||||
if "%TOKEN%"=="" (
|
||||
echo [ERROR] 获取 Token 失败。请检查 client_secret 是否正确,或查看 token_response.json 内容。
|
||||
type token_response.json
|
||||
exit /b 1
|
||||
)
|
||||
echo [INFO] Token 获取成功
|
||||
goto :eof
|
||||
|
||||
:: 通用 HTTP 请求函数 (使用 PowerShell 辅助,确保参数安全)
|
||||
:HTTP_REQUEST
|
||||
set "METHOD=%1"
|
||||
set "URL=%2"
|
||||
set "DATA=%3"
|
||||
set "EXTRA_HEADERS=%4"
|
||||
set "OUTPUT_FILE=%5"
|
||||
set "ERROR_FILE=%6"
|
||||
|
||||
:: 构建 PowerShell 命令
|
||||
set "PS_CMD=powershell -Command """
|
||||
set "PS_CMD=!PS_CMD!$url = '%URL%'; $method = '%METHOD%'; "
|
||||
|
||||
:: 处理 Headers
|
||||
if defined EXTRA_HEADERS (
|
||||
set "PS_CMD=!PS_CMD!$headers = @{'Authorization'='Basic %TOKEN%'}; "
|
||||
for %%H in (%EXTRA_HEADERS%) do (
|
||||
set "key=%%~H"
|
||||
set "val=!key:*:=!"
|
||||
set "key=!key:=%VAL%=!"
|
||||
:: 这里简化处理,复杂 header 可能需要更精细的解析,通常 Target-Node 和 airport-code 是简单的 key-value
|
||||
)
|
||||
:: 更简单的方式:直接拼接到命令中
|
||||
)
|
||||
|
||||
:: 对于 HTTP 请求,如果数据中包含特殊字符,直接使用 PowerShell 的 Invoke-RestMethod 或 Invoke-WebRequest 更可靠
|
||||
:: 这里为了保持与 Batch 逻辑的一致性,我们使用 curl 但通过 PowerShell 调用 curl 以保留 stderr
|
||||
if defined OUTPUT_FILE (
|
||||
powershell -Command "curl.exe -X '%METHOD%' '%URL%' -H 'Authorization: Basic %TOKEN%'" > "%OUTPUT_FILE%" 2>"%ERROR_FILE%"
|
||||
:: 注意:如果 DATA 非空,需要重新构造命令。为了简化,我们假设大部分 POST 数据是简单的 form-urlencoded,且不含极端特殊字符,或者使用 PowerShell 内部变量传递。
|
||||
:: **更安全的做法**:对于包含复杂 Data 的请求,直接使用 PowerShell。
|
||||
) else (
|
||||
curl -X %METHOD% '%URL%' -H 'Authorization: Basic %TOKEN%' 2>&1
|
||||
)
|
||||
|
||||
:: *修正*:为了确保所有请求(特别是带 DATA 的)都能正确处理特殊字符,我们将核心请求逻辑交给 PowerShell 执行,或者确保 curl 命令被正确转义。
|
||||
:: 鉴于 Batch 的局限性,以下是一个折中方案:使用 PowerShell 执行 curl。
|
||||
|
||||
goto :eof
|
||||
|
||||
:: 由于 Batch 处理复杂 HTTP 请求困难,建议将核心 API 调用脚本提取为独立的 .ps1 文件,或接受此版本的限制。
|
||||
:: 为保证可用性和特殊字符安全,下面提供基于 PowerShell 封装的通用请求函数
|
||||
|
||||
:PS_HTTP_REQUEST
|
||||
:: $1=Method, $2=URL, $3=Data(JSON), $4=Headers(Key=Value;Key=Value)
|
||||
set "METHOD=%~1"
|
||||
set "URL=%~2"
|
||||
set "DATA=%~3"
|
||||
set "HEADERS=%~4"
|
||||
set "OUT_FILE=%~5"
|
||||
|
||||
set "PS_SCRIPT=powershell -Command """
|
||||
set "PS_SCRIPT=!PS_SCRIPT!try {"
|
||||
|
||||
if "%METHOD%"=="GET" (
|
||||
set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS!"
|
||||
) else if "%METHOD%"=="POST" (
|
||||
if defined DATA (
|
||||
:: 如果 Data 是 JSON,直接传递;如果是 Form,需要转换
|
||||
set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Method POST -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS! -Body '@'"
|
||||
:: 注意:Body 的处理较为复杂,这里简化处理
|
||||
) else (
|
||||
set "PS_CMD=!PS_CMD!IWR -Uri '%URL%' -Method POST -Headers @{'Authorization'='Basic %TOKEN%'} !HEADERS!"
|
||||
)
|
||||
)
|
||||
|
||||
:: ... (由于 Batch 动态构造 PowerShell 命令极易出错,生产环境强烈建议将 API 调用逻辑完全迁移至 PowerShell)
|
||||
:: 此处为了演示,我们保留 curl 但强调特殊字符风险。对于包含特殊字符的请求,请单独使用 PowerShell 调用。
|
||||
|
||||
echo [WARN] 复杂的 HTTP 请求且含特殊字符时,Batch 脚本可能无法完美处理。建议将此类请求移至 .ps1 脚本中。
|
||||
goto :eof
|
||||
|
||||
:: --- 主流程简化版 (仅展示 Token 获取的安全处理) ---
|
||||
|
||||
:MAIN
|
||||
echo ==========================================
|
||||
echo PAM 智能部署开始 (注意:特殊字符支持有限,建议配置文件中避免使用极端特殊字符或改用 PowerShell 入口)
|
||||
echo ==========================================
|
||||
|
||||
call :GET_TOKEN
|
||||
|
||||
:: ... 后续步骤由于 Batch 的局限性,对于含特殊字符的 Data/URL 仍可能存在风险。
|
||||
:: **最佳实践**:如果 CLIENT_SECRET 包含 !, %, &, # 等,请考虑将 `deploy.bat` 改为 `deploy.ps1`。
|
||||
echo [INFO] 脚本执行完成 (测试模式)
|
||||
|
||||
endlocal
|
||||
pause
|
||||
```
|
||||
|
||||
### ⚠️ 重要说明:关于 Windows Batch 的特殊字符限制
|
||||
|
||||
经过测试,**Windows Batch 原生无法完美支持在变量中存储并安全传递包含** `!`**,** `%`**,** `&`**,** `<`**,** `>`**,** `&` **等字符的值用于后续命令参数**。即使使用 `EnableDelayedExpansion`,在构建 `curl -d "client_secret=!CLIENT_SECRET!"` 时,`!...!` 的语法冲突也无法避免。
|
||||
|
||||
**最终建议方案:**
|
||||
如果你的 `client_secret` 确实包含这些特殊字符,**请不要使用 Batch 脚本作为入口**。请创建一个 `deploy.ps1` (PowerShell 脚本),它可以完美处理所有特殊字符,并且能直接调用 .NET API 或封装 `curl.exe`,实现与 Shell 脚本同等甚至更强的功能。
|
||||
|
||||
如果你必须使用 Batch,请尝试以下 **最小化修改** 来确保 Token 获取成功:
|
||||
|
||||
1. 在 `config.txt` 中,确保 `CLIENT_SECRET` 没有引号。
|
||||
|
||||
2. 在 `deploy.bat` 中,使用我提供的 `powershell -Command "... Invoke-RestMethod ..."` 方式获取 Token,这是目前 Batch 环境下最安全的做法。
|
||||
|
||||
|
||||
* * *
|
||||
|
||||
## 4. Linux/Mac 测试脚本 (test_deploy.sh)
|
||||
|
||||
_(同前)_
|
||||
|
||||
## 5. Windows Batch 测试脚本 (test_deploy.bat)
|
||||
|
||||
_(同前,注意测试脚本也需更新以使用 PowerShell 获取 Token)_
|
||||
|
||||
```bat
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
:: ... (配置读取逻辑同上) ...
|
||||
|
||||
:TEST_GET_TOKEN
|
||||
echo ==========================================
|
||||
echo 测试 1: 获取 Token
|
||||
echo ==========================================
|
||||
|
||||
:: 使用 PowerShell 获取,确保特殊字符安全
|
||||
powershell -Command "$id = '%CLIENT_ID%'; $sec = '%CLIENT_SECRET%'; $url = '%HOME_BASE_URL%/oauth/token'; $body = 'grant_type=client_credentials&client_id=' + $id + '&client_secret=' + $sec; Invoke-RestMethod -Uri $url -Method POST -Body $body -ContentType 'application/x-www-form-urlencoded' | ConvertTo-Json" > token_test.json
|
||||
|
||||
if not exist token_test.json (
|
||||
echo %RED% Token 获取失败 (PowerShell 错误)
|
||||
set /a FAIL_COUNT+=1
|
||||
) else (
|
||||
for /f "delims=" %%i in ('powershell -Command "(Get-Content token_test.json | ConvertFrom-Json).access_token"') do (
|
||||
set "TOKEN=%%i"
|
||||
)
|
||||
|
||||
if "%TOKEN%"=="" (
|
||||
echo %RED% Token 获取失败
|
||||
type token_test.json
|
||||
set /a FAIL_COUNT+=1
|
||||
) else (
|
||||
echo %GREEN% Token 获取成功
|
||||
set /a PASS_COUNT+=1
|
||||
)
|
||||
)
|
||||
goto :eof
|
||||
|
||||
:: ... (其他测试步骤类似,对于涉及 POST Data 的测试,建议也使用 PowerShell) ...
|
||||
```
|
||||
|
||||
## 6. 使用说明
|
||||
|
||||
### 前置依赖
|
||||
|
||||
1. **Linux/Mac**:
|
||||
|
||||
* `curl`: 通常预装
|
||||
|
||||
* `jq`: JSON 解析工具
|
||||
|
||||
2. **Windows**:
|
||||
|
||||
* `PowerShell`: 必须使用 PowerShell 5.0+ (Windows 10 默认)
|
||||
|
||||
* `curl.exe`: Windows 10+ 自带
|
||||
|
||||
|
||||
### 关键配置建议
|
||||
|
||||
* **Linux/Mac**: `config.txt` 中的特殊字符可以安全使用。
|
||||
|
||||
* **Windows**:
|
||||
|
||||
* Token 获取已通过 PowerShell 包装,支持特殊字符。
|
||||
|
||||
* 其他涉及 POST Body 包含复杂特殊字符的请求,Batch 脚本可能存在局限。如果遇到问题,请将对应步骤移至 PowerShell 脚本中执行。
|
||||
11
doc_scripts/config.txt.example
Normal file
11
doc_scripts/config.txt.example
Normal file
@ -0,0 +1,11 @@
|
||||
HOME_BASE_URL=https://pam.home.com
|
||||
CLIENT_ID=your_client_id
|
||||
CLIENT_SECRET=MySecret!Pass123&Special#Chars
|
||||
AIRPORT_CODE=HET
|
||||
APP_NAME=PAM
|
||||
MODULE_NAME=Node
|
||||
VERSION_NUMBER=2.0.5
|
||||
ZIP_FILE_PATH=C:\path\to\pam-2.0.5.zip
|
||||
ACTION_TYPE=FULL
|
||||
TIMEOUT=120
|
||||
LOG_NAME=app.log
|
||||
5
doc_scripts/deploy.bat
Normal file
5
doc_scripts/deploy.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%deploy.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
763
doc_scripts/deploy.ps1
Normal file
763
doc_scripts/deploy.ps1
Normal file
@ -0,0 +1,763 @@
|
||||
param(
|
||||
[string]$ConfigPath = (Join-Path $PSScriptRoot 'config.txt'),
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# PAM 部署主脚本(PowerShell 实现)。
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Show-DeployUsage {
|
||||
@'
|
||||
Usage:
|
||||
powershell -File .\deploy.ps1 [-ConfigPath .\config.txt]
|
||||
|
||||
Notes:
|
||||
- deploy.bat is only a wrapper for this script.
|
||||
- The wrapper avoids cmd.exe delayed-expansion issues with CLIENT_SECRET values
|
||||
containing exclamation marks.
|
||||
'@ | Write-Host
|
||||
}
|
||||
|
||||
function Write-Info([string]$Message) { Write-Host "[INFO] $Message" }
|
||||
function Write-WarnLog([string]$Message) { Write-Host "[WARN] $Message" }
|
||||
function Write-ErrLog([string]$Message) { Write-Host "[ERROR] $Message" }
|
||||
|
||||
function Convert-ResponseContent {
|
||||
param([AllowNull()][string]$Content)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Content)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
if ($PSVersionTable.PSVersion.Major -ge 6) {
|
||||
return $Content | ConvertFrom-Json -Depth 100
|
||||
}
|
||||
|
||||
return $Content | ConvertFrom-Json
|
||||
} catch {
|
||||
return $Content
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ErrorBody {
|
||||
param([System.Management.Automation.ErrorRecord]$ErrorRecord)
|
||||
|
||||
try {
|
||||
$response = $ErrorRecord.Exception.Response
|
||||
if ($null -eq $response) {
|
||||
return $ErrorRecord.Exception.Message
|
||||
}
|
||||
|
||||
$stream = $response.GetResponseStream()
|
||||
if ($null -eq $stream) {
|
||||
return $ErrorRecord.Exception.Message
|
||||
}
|
||||
|
||||
$reader = New-Object System.IO.StreamReader($stream)
|
||||
try {
|
||||
return $reader.ReadToEnd()
|
||||
} finally {
|
||||
$reader.Dispose()
|
||||
}
|
||||
} catch {
|
||||
return $ErrorRecord.Exception.Message
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ResponseValue {
|
||||
param(
|
||||
$Response,
|
||||
[string[]]$Candidates
|
||||
)
|
||||
|
||||
foreach ($candidate in $Candidates) {
|
||||
$current = $Response
|
||||
foreach ($segment in ($candidate -split '\.')) {
|
||||
if ($null -eq $current) {
|
||||
break
|
||||
}
|
||||
|
||||
if ($current -is [string]) {
|
||||
$current = $null
|
||||
break
|
||||
}
|
||||
|
||||
if ($current -is [System.Collections.IDictionary]) {
|
||||
if ($current.Contains($segment)) {
|
||||
$current = $current[$segment]
|
||||
} else {
|
||||
$current = $null
|
||||
break
|
||||
}
|
||||
} elseif ($current.PSObject.Properties.Name -contains $segment) {
|
||||
$current = $current.$segment
|
||||
} else {
|
||||
$current = $null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $current -and -not [string]::IsNullOrWhiteSpace([string]$current)) {
|
||||
return [string]$current
|
||||
}
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-PamConfig {
|
||||
param([string]$Path)
|
||||
|
||||
$config = [ordered]@{}
|
||||
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
foreach ($rawLine in Get-Content -LiteralPath $Path -Encoding UTF8) {
|
||||
$line = $rawLine.TrimEnd("`r")
|
||||
if ($line -match '^\s*$' -or $line -match '^\s*[#;]') {
|
||||
continue
|
||||
}
|
||||
|
||||
$index = $line.IndexOf('=')
|
||||
if ($index -lt 1) {
|
||||
continue
|
||||
}
|
||||
|
||||
$key = $line.Substring(0, $index).Trim()
|
||||
$value = $line.Substring($index + 1).Trim()
|
||||
if ($value -match '^(.*?\S)\s+[;#].*$') {
|
||||
$value = $Matches[1]
|
||||
}
|
||||
|
||||
switch ($key) {
|
||||
'HOME_BASE_URL' { $config[$key] = $value }
|
||||
'CLIENT_ID' { $config[$key] = $value }
|
||||
'CLIENT_SECRET' { $config[$key] = $value }
|
||||
'AIRPORT_CODE' { $config[$key] = $value }
|
||||
'APP_NAME' { $config[$key] = $value }
|
||||
'MODULE_NAME' { $config[$key] = $value }
|
||||
'VERSION_NUMBER' { $config[$key] = $value }
|
||||
'ZIP_FILE_PATH' { $config[$key] = $value }
|
||||
'ACTION_TYPE' { $config[$key] = $value }
|
||||
'TIMEOUT' { $config[$key] = $value }
|
||||
'LOG_NAME' { $config[$key] = $value }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-WarnLog "Config file not found: $Path. Defaults will be used."
|
||||
}
|
||||
|
||||
$defaults = [ordered]@{
|
||||
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 = 'C:\path\to\pam-2.0.5.zip'
|
||||
ACTION_TYPE = 'FULL'
|
||||
TIMEOUT = '120'
|
||||
LOG_NAME = 'app.log'
|
||||
}
|
||||
|
||||
foreach ($name in $defaults.Keys) {
|
||||
if (-not $config.Contains($name) -or [string]::IsNullOrWhiteSpace([string]$config[$name])) {
|
||||
$config[$name] = $defaults[$name]
|
||||
}
|
||||
}
|
||||
|
||||
return [pscustomobject]$config
|
||||
}
|
||||
|
||||
function Join-RequestPairs {
|
||||
param([System.Collections.IDictionary]$Values)
|
||||
|
||||
$pairs = foreach ($key in $Values.Keys) {
|
||||
$encodedValue = [System.Uri]::EscapeDataString([string]$Values[$key])
|
||||
'{0}={1}' -f $key, $encodedValue
|
||||
}
|
||||
|
||||
return ($pairs -join '&')
|
||||
}
|
||||
|
||||
function Test-ZipFile {
|
||||
param($Config)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Config.ZIP_FILE_PATH)) {
|
||||
throw "Package file not found: $($Config.ZIP_FILE_PATH)"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PamWebRequest {
|
||||
param(
|
||||
[ValidateSet('GET', 'POST', 'PUT')]
|
||||
[string]$Method,
|
||||
[string]$Url,
|
||||
[string]$Token,
|
||||
[hashtable]$Headers = @{},
|
||||
[AllowNull()]$Body = $null,
|
||||
[string]$ContentType = '',
|
||||
[string]$OutFile = ''
|
||||
)
|
||||
|
||||
$allHeaders = @{}
|
||||
if ($Token) {
|
||||
$allHeaders['Authorization'] = "Basic $Token"
|
||||
}
|
||||
foreach ($key in $Headers.Keys) {
|
||||
$allHeaders[$key] = $Headers[$key]
|
||||
}
|
||||
|
||||
$params = @{
|
||||
Uri = $Url
|
||||
Method = $Method
|
||||
Headers = $allHeaders
|
||||
ErrorAction = 'Stop'
|
||||
}
|
||||
|
||||
if ($PSVersionTable.PSVersion.Major -lt 6) {
|
||||
$params['UseBasicParsing'] = $true
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body -and "$Body" -ne '') {
|
||||
$params['Body'] = $Body
|
||||
}
|
||||
|
||||
if ($ContentType) {
|
||||
$params['ContentType'] = $ContentType
|
||||
}
|
||||
|
||||
if ($OutFile) {
|
||||
$params['OutFile'] = $OutFile
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest @params
|
||||
if ($OutFile) {
|
||||
return $OutFile
|
||||
}
|
||||
return Convert-ResponseContent $response.Content
|
||||
} catch {
|
||||
$body = Get-ErrorBody $_
|
||||
throw "Request failed [$Method] $Url`n$body"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PamMultipartUpload {
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$Token,
|
||||
[string]$FilePath,
|
||||
[hashtable]$Fields
|
||||
)
|
||||
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
try {
|
||||
$client.DefaultRequestHeaders.Authorization =
|
||||
[System.Net.Http.Headers.AuthenticationHeaderValue]::new('Basic', $Token)
|
||||
|
||||
$content = [System.Net.Http.MultipartFormDataContent]::new()
|
||||
foreach ($entry in $Fields.GetEnumerator()) {
|
||||
$stringContent = [System.Net.Http.StringContent]::new([string]$entry.Value, [System.Text.Encoding]::UTF8)
|
||||
$content.Add($stringContent, $entry.Key)
|
||||
}
|
||||
|
||||
$stream = [System.IO.File]::OpenRead($FilePath)
|
||||
try {
|
||||
$fileContent = [System.Net.Http.StreamContent]::new($stream)
|
||||
$fileContent.Headers.ContentType =
|
||||
[System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream')
|
||||
$content.Add($fileContent, 'file', [System.IO.Path]::GetFileName($FilePath))
|
||||
|
||||
$response = $client.PostAsync($Url, $content).GetAwaiter().GetResult()
|
||||
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "Upload failed [HTTP $([int]$response.StatusCode)]`n$body"
|
||||
}
|
||||
|
||||
return Convert-ResponseContent $body
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Token {
|
||||
param($Config)
|
||||
|
||||
Write-Info 'Getting token...'
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
grant_type = 'client_credentials'
|
||||
client_id = $Config.CLIENT_ID
|
||||
client_secret = $Config.CLIENT_SECRET
|
||||
})
|
||||
$response = Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/oauth/token" -Token '' -Body $body -ContentType 'application/x-www-form-urlencoded'
|
||||
$token = Get-ResponseValue -Response $response -Candidates @('access_token')
|
||||
if (-not $token) {
|
||||
throw "Invalid token response: $response"
|
||||
}
|
||||
return $token
|
||||
}
|
||||
|
||||
function New-VersionRecord {
|
||||
param($Config, [string]$Token)
|
||||
|
||||
Write-Info 'Step 2.1: create version record'
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
versionNumber = $Config.VERSION_NUMBER
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
description = 'Auto Deploy'
|
||||
})
|
||||
[void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/api/version/upgrade" -Token $Token -Body $body -ContentType 'application/x-www-form-urlencoded')
|
||||
}
|
||||
|
||||
function Upload-Package {
|
||||
param($Config, [string]$Token)
|
||||
|
||||
Write-Info 'Step 2.2: upload package'
|
||||
$response = Invoke-PamMultipartUpload -Url "$($Config.HOME_BASE_URL)/api/version/upgrade/upload" -Token $Token -FilePath $Config.ZIP_FILE_PATH -Fields @{
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
versionNumber = $Config.VERSION_NUMBER
|
||||
}
|
||||
|
||||
$hashCode = Get-ResponseValue -Response $response -Candidates @('hashCode', 'data.hashCode')
|
||||
if (-not $hashCode -and $response -is [string]) {
|
||||
$hashCode = $response.Trim()
|
||||
}
|
||||
if (-not $hashCode) {
|
||||
throw "Unable to parse hashCode from upload response: $response"
|
||||
}
|
||||
|
||||
return $hashCode
|
||||
}
|
||||
|
||||
function Publish-Version {
|
||||
param($Config, [string]$Token, [string]$HashCode)
|
||||
|
||||
Write-Info 'Step 2.3: publish version'
|
||||
$payload = @{
|
||||
airportCodesWhite = @($Config.AIRPORT_CODE)
|
||||
hashCode = $HashCode
|
||||
state = 'RELEASE'
|
||||
} | ConvertTo-Json -Depth 5
|
||||
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
versionNumber = $Config.VERSION_NUMBER
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
})
|
||||
|
||||
[void](Invoke-PamWebRequest -Method PUT -Url "$($Config.HOME_BASE_URL)/api/version/upgrade/profile?$query" -Token $Token -Body $payload -ContentType 'application/json')
|
||||
}
|
||||
|
||||
function Get-NodeUrl {
|
||||
param($Config, [string]$Token)
|
||||
|
||||
Write-Info 'Step 3.1: resolve node url'
|
||||
$response = Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/api/mcp/airport/target-node?airportCode=$($Config.AIRPORT_CODE)" -Token $Token
|
||||
|
||||
if ($response -is [System.Collections.IDictionary]) {
|
||||
return [string]($response.Keys | Select-Object -First 1)
|
||||
}
|
||||
|
||||
$propertyNames = @($response.PSObject.Properties.Name)
|
||||
if ($propertyNames.Count -gt 0) {
|
||||
return [string]$propertyNames[0]
|
||||
}
|
||||
|
||||
throw "Unable to resolve node url: $response"
|
||||
}
|
||||
|
||||
function Get-OnlineIps {
|
||||
param($Config, [string]$Token, [string]$NodeUrl)
|
||||
|
||||
Write-Info 'Step 3.2: query online IP list'
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
})
|
||||
|
||||
$response = Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/ips?$query" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
}
|
||||
|
||||
$ips = @()
|
||||
if ($response -is [System.Array]) {
|
||||
$ips = @($response | ForEach-Object { [string]$_ } | Where-Object { $_ })
|
||||
} elseif ($response -is [System.Collections.IEnumerable] -and -not ($response -is [string])) {
|
||||
$ips = @($response | ForEach-Object { [string]$_ } | Where-Object { $_ })
|
||||
}
|
||||
|
||||
if ($ips.Count -eq 0) {
|
||||
throw "No online workstation matched the module. Raw response: $response"
|
||||
}
|
||||
|
||||
return $ips
|
||||
}
|
||||
|
||||
function Wait-DownloadProgress {
|
||||
param($Config, [string]$Token, [string]$NodeUrl)
|
||||
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
})
|
||||
$progressUrl = "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/download-cloud/progress?$query"
|
||||
|
||||
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
||||
$response = Invoke-PamWebRequest -Method GET -Url $progressUrl -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
}
|
||||
|
||||
$status = Get-ResponseValue -Response $response -Candidates @('status')
|
||||
$successFlag = Get-ResponseValue -Response $response -Candidates @('success')
|
||||
if ($status -eq 'completed' -or $successFlag -eq 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
$message = Get-ResponseValue -Response $response -Candidates @('message')
|
||||
if ($message -and $message -match '(?i)fail|error') {
|
||||
throw "Node download failed: $message"
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
throw 'Node download timed out.'
|
||||
}
|
||||
|
||||
function Download-CloudToNode {
|
||||
param($Config, [string]$Token, [string]$NodeUrl)
|
||||
|
||||
Write-Info 'Step 3.3: download package to node'
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
versionNumber = $Config.VERSION_NUMBER
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
timeOut = $Config.TIMEOUT
|
||||
})
|
||||
|
||||
[void](Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/download-cloud?$query" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
'airport-code' = $Config.AIRPORT_CODE
|
||||
})
|
||||
|
||||
Wait-DownloadProgress -Config $Config -Token $Token -NodeUrl $NodeUrl
|
||||
}
|
||||
|
||||
function Invoke-UpgradeRequest {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip)
|
||||
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
versionNumber = $Config.VERSION_NUMBER
|
||||
action = $Config.ACTION_TYPE
|
||||
autoStart = 'false'
|
||||
timeOut = $Config.TIMEOUT
|
||||
})
|
||||
|
||||
Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
} -Body $body -ContentType 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
function Start-Application {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip)
|
||||
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
runstart = 'true'
|
||||
})
|
||||
|
||||
[void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/start-stop" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
} -Body $body -ContentType 'application/x-www-form-urlencoded')
|
||||
}
|
||||
|
||||
function Stop-Application {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip)
|
||||
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
runstart = 'false'
|
||||
})
|
||||
|
||||
[void](Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/start-stop" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
} -Body $body -ContentType 'application/x-www-form-urlencoded')
|
||||
}
|
||||
|
||||
function Verify-Ip {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip)
|
||||
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
})
|
||||
|
||||
Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/verify?$query" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
}
|
||||
}
|
||||
|
||||
function Download-DeployLog {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip)
|
||||
|
||||
$logsDir = Join-Path $PSScriptRoot 'logs'
|
||||
if (-not (Test-Path -LiteralPath $logsDir)) {
|
||||
$null = New-Item -ItemType Directory -Path $logsDir
|
||||
}
|
||||
|
||||
$logFile = Join-Path $logsDir ("deploy_{0}.log" -f $Ip)
|
||||
$errorFile = Join-Path $logsDir ("error_{0}.log" -f $Ip)
|
||||
$query = Join-RequestPairs ([ordered]@{
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
logName = $Config.LOG_NAME
|
||||
})
|
||||
|
||||
try {
|
||||
[void](Invoke-PamWebRequest -Method GET -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/log-download?$query" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
} -OutFile $logFile)
|
||||
|
||||
if ((Get-Item -LiteralPath $logFile).Length -gt 0) {
|
||||
Get-Content -LiteralPath $logFile -Tail 5 | Set-Content -LiteralPath "$logFile.summary"
|
||||
} else {
|
||||
'Log content empty or no data' | Set-Content -LiteralPath "$logFile.summary"
|
||||
}
|
||||
} catch {
|
||||
Get-ErrorBody $_ | Set-Content -LiteralPath $errorFile
|
||||
"Log download failed. See $errorFile" | Set-Content -LiteralPath $logFile
|
||||
'Log download failed' | Set-Content -LiteralPath "$logFile.summary"
|
||||
}
|
||||
|
||||
return $logFile
|
||||
}
|
||||
|
||||
function Invoke-Rollback {
|
||||
param($Config, [string]$Token, [string]$NodeUrl, [string]$Ip, [bool]$StopFirst)
|
||||
|
||||
if ($StopFirst) {
|
||||
try {
|
||||
Stop-Application -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$body = Join-RequestPairs ([ordered]@{
|
||||
airportCode = $Config.AIRPORT_CODE
|
||||
targetIp = $Ip
|
||||
applicationName = $Config.APP_NAME
|
||||
moduleName = $Config.MODULE_NAME
|
||||
timeOut = $Config.TIMEOUT
|
||||
})
|
||||
$response = Invoke-PamWebRequest -Method POST -Url "$($Config.HOME_BASE_URL)/node_proxy/$($Config.AIRPORT_CODE)/api/mcp/version/upgrade/rollback" -Token $Token -Headers @{
|
||||
'Target-Node' = $NodeUrl
|
||||
} -Body $body -ContentType 'application/x-www-form-urlencoded'
|
||||
|
||||
$rollbackSuccess = Get-ResponseValue -Response $response -Candidates @('success')
|
||||
if ($rollbackSuccess -and $rollbackSuccess -ne 'true') {
|
||||
return 'ROLLBACK_FAILED'
|
||||
}
|
||||
} catch {
|
||||
return 'ROLLBACK_REQUEST_FAILED'
|
||||
}
|
||||
|
||||
try {
|
||||
$verify = Verify-Ip -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
if ((Get-ResponseValue -Response $verify -Candidates @('success')) -eq 'true') {
|
||||
return 'ROLLBACK_SUCCESS'
|
||||
}
|
||||
return 'ROLLBACK_VERIFY_FAILED'
|
||||
} catch {
|
||||
return 'ROLLBACK_VERIFY_FAILED'
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-IpDeploy {
|
||||
param(
|
||||
$Config,
|
||||
[string]$Token,
|
||||
[string]$NodeUrl,
|
||||
[string]$Ip
|
||||
)
|
||||
|
||||
Write-Info "Processing IP: $Ip"
|
||||
|
||||
try {
|
||||
$upgrade = Invoke-UpgradeRequest -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
} catch {
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'FAILED'
|
||||
Stage = 'UPGRADE'
|
||||
Message = 'Upgrade request failed'
|
||||
Rollback = 'ROLLBACK_NOT_RUN'
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
if ((Get-ResponseValue -Response $upgrade -Candidates @('success')) -ne 'true') {
|
||||
$rollback = Invoke-Rollback -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip -StopFirst:$false
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
$message = Get-ResponseValue -Response $upgrade -Candidates @('message')
|
||||
if (-not $message) { $message = 'Upgrade failed' }
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'FAILED'
|
||||
Stage = 'UPGRADE'
|
||||
Message = $message
|
||||
Rollback = $rollback
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Application -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
} catch {
|
||||
$rollback = Invoke-Rollback -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip -StopFirst:$true
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'FAILED'
|
||||
Stage = 'START'
|
||||
Message = 'Application start failed'
|
||||
Rollback = $rollback
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$verify = Verify-Ip -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
} catch {
|
||||
$rollback = Invoke-Rollback -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip -StopFirst:$true
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'FAILED'
|
||||
Stage = 'VERIFY'
|
||||
Message = 'Health check request failed'
|
||||
Rollback = $rollback
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
if ((Get-ResponseValue -Response $verify -Candidates @('success')) -eq 'true') {
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'SUCCESS'
|
||||
Stage = '-'
|
||||
Message = '-'
|
||||
Rollback = '-'
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
$rollback = Invoke-Rollback -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip -StopFirst:$true
|
||||
$logFile = Download-DeployLog -Config $Config -Token $Token -NodeUrl $NodeUrl -Ip $Ip
|
||||
$message = Get-ResponseValue -Response $verify -Candidates @('message')
|
||||
if (-not $message) { $message = 'Health check failed' }
|
||||
return [pscustomobject]@{
|
||||
Ip = $Ip
|
||||
Status = 'FAILED'
|
||||
Stage = 'VERIFY'
|
||||
Message = $message
|
||||
Rollback = $rollback
|
||||
LogFile = $logFile
|
||||
}
|
||||
}
|
||||
|
||||
function Write-DeployReport {
|
||||
param(
|
||||
$Config,
|
||||
[System.Collections.Generic.List[object]]$Results,
|
||||
[int]$TotalCount
|
||||
)
|
||||
|
||||
$successCount = @($Results | Where-Object { $_.Status -eq 'SUCCESS' }).Count
|
||||
$failCount = @($Results | Where-Object { $_.Status -ne 'SUCCESS' }).Count
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '====================== DEPLOY REPORT ======================'
|
||||
Write-Host 'Mode: Batch/PowerShell'
|
||||
Write-Host "Airport: $($Config.AIRPORT_CODE)"
|
||||
Write-Host "Application: $($Config.APP_NAME)"
|
||||
Write-Host "Module: $($Config.MODULE_NAME)"
|
||||
Write-Host "Version: $($Config.VERSION_NUMBER)"
|
||||
Write-Host "Total: $TotalCount"
|
||||
Write-Host "Success: $successCount"
|
||||
Write-Host "Failed: $failCount"
|
||||
Write-Host ''
|
||||
Write-Host ('{0,-18} {1,-8} {2,-12} {3,-22} {4}' -f 'IP', 'STATUS', 'STAGE', 'ROLLBACK', 'LOG')
|
||||
foreach ($item in $Results) {
|
||||
Write-Host ('{0,-18} {1,-8} {2,-12} {3,-22} {4}' -f $item.Ip, $item.Status, $item.Stage, $item.Rollback, $item.LogFile)
|
||||
if ($item.Status -ne 'SUCCESS') {
|
||||
Write-Host (" Reason: {0}" -f $item.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PamDeploy {
|
||||
param([string]$ConfigPath)
|
||||
|
||||
$config = Get-PamConfig -Path $ConfigPath
|
||||
Test-ZipFile -Config $config
|
||||
|
||||
Write-Info "Deploy start: airport=$($config.AIRPORT_CODE), version=$($config.VERSION_NUMBER), module=$($config.APP_NAME)/$($config.MODULE_NAME)"
|
||||
|
||||
$token = Get-Token -Config $config
|
||||
New-VersionRecord -Config $config -Token $token
|
||||
$hashCode = Upload-Package -Config $config -Token $token
|
||||
Publish-Version -Config $config -Token $token -HashCode $hashCode
|
||||
$nodeUrl = Get-NodeUrl -Config $config -Token $token
|
||||
$ips = Get-OnlineIps -Config $config -Token $token -NodeUrl $nodeUrl
|
||||
Download-CloudToNode -Config $config -Token $token -NodeUrl $nodeUrl
|
||||
|
||||
$results = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($ip in $ips) {
|
||||
$results.Add((Invoke-IpDeploy -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip))
|
||||
}
|
||||
|
||||
Write-DeployReport -Config $config -Results $results -TotalCount $ips.Count
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-DeployUsage
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -ne '.') {
|
||||
try {
|
||||
Invoke-PamDeploy -ConfigPath $ConfigPath
|
||||
} catch {
|
||||
Write-ErrLog $_
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
976
doc_scripts/deploy.sh
Normal file
976
doc_scripts/deploy.sh
Normal file
@ -0,0 +1,976 @@
|
||||
#!/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 )[^\r\n]+/\1***MASKED***/g' \
|
||||
-e 's/("access_token"[[:space:]]*:[[:space:]]*")[^"]+/\1***MASKED***/g' \
|
||||
-e 's/("client_secret"[[:space:]]*:[[:space:]]*")[^"]+/\1***MASKED***/g')"
|
||||
|
||||
printf '%s' "$text"
|
||||
}
|
||||
|
||||
indent_text() {
|
||||
local text="$1"
|
||||
|
||||
if [[ -z "$text" ]]; then
|
||||
printf ' (empty)\n'
|
||||
else
|
||||
printf '%s\n' "$text" | sed 's/^/ /'
|
||||
fi
|
||||
}
|
||||
|
||||
trace_request() {
|
||||
local request_id="$1"
|
||||
local method="$2"
|
||||
local url="$3"
|
||||
local headers="$4"
|
||||
local body="$5"
|
||||
|
||||
ensure_trace_file
|
||||
{
|
||||
printf '[%s] [%s] REQUEST\n' "$(timestamp_now)" "$request_id"
|
||||
printf 'METHOD: %s\n' "$method"
|
||||
printf 'URL: %s\n' "$url"
|
||||
printf 'HEADERS:\n'
|
||||
indent_text "$(mask_sensitive_text "$headers")"
|
||||
printf 'BODY:\n'
|
||||
indent_text "$(mask_sensitive_text "$body")"
|
||||
printf '\n'
|
||||
} >> "$API_TRACE_FILE"
|
||||
}
|
||||
|
||||
trace_response() {
|
||||
local request_id="$1"
|
||||
local curl_exit="$2"
|
||||
local http_code="$3"
|
||||
local response="$4"
|
||||
|
||||
ensure_trace_file
|
||||
{
|
||||
printf '[%s] [%s] RESPONSE\n' "$(timestamp_now)" "$request_id"
|
||||
printf 'CURL_EXIT: %s\n' "$curl_exit"
|
||||
printf 'HTTP_CODE: %s\n' "${http_code:-N/A}"
|
||||
printf 'BODY:\n'
|
||||
indent_text "$(mask_sensitive_text "$response")"
|
||||
printf '\n'
|
||||
} >> "$API_TRACE_FILE"
|
||||
}
|
||||
|
||||
trace_upload_request() {
|
||||
local request_id="$1"
|
||||
local url="$2"
|
||||
local file_path="$3"
|
||||
local fields="$4"
|
||||
|
||||
ensure_trace_file
|
||||
{
|
||||
printf '[%s] [%s] REQUEST\n' "$(timestamp_now)" "$request_id"
|
||||
printf 'METHOD: POST\n'
|
||||
printf 'URL: %s\n' "$url"
|
||||
printf 'UPLOAD_FILE: %s\n' "$file_path"
|
||||
printf 'FORM_FIELDS:\n'
|
||||
indent_text "$(mask_sensitive_text "$fields")"
|
||||
printf '\n'
|
||||
} >> "$API_TRACE_FILE"
|
||||
}
|
||||
|
||||
trace_download_result() {
|
||||
local request_id="$1"
|
||||
local url="$2"
|
||||
local http_code="$3"
|
||||
local curl_exit="$4"
|
||||
local output_file="$5"
|
||||
local error_text="$6"
|
||||
|
||||
ensure_trace_file
|
||||
{
|
||||
printf '[%s] [%s] FILE_RESPONSE\n' "$(timestamp_now)" "$request_id"
|
||||
printf 'URL: %s\n' "$url"
|
||||
printf 'CURL_EXIT: %s\n' "$curl_exit"
|
||||
printf 'HTTP_CODE: %s\n' "${http_code:-N/A}"
|
||||
printf 'OUTPUT_FILE: %s\n' "$output_file"
|
||||
if [[ -f "$output_file" ]]; then
|
||||
printf 'OUTPUT_SIZE: %s\n' "$(wc -c < "$output_file" | tr -d ' ')"
|
||||
fi
|
||||
printf 'STDERR:\n'
|
||||
indent_text "$(mask_sensitive_text "$error_text")"
|
||||
printf '\n'
|
||||
} >> "$API_TRACE_FILE"
|
||||
}
|
||||
|
||||
trim() {
|
||||
local value="$1"
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
strip_inline_comment() {
|
||||
local value="$1"
|
||||
local comment_regex='^(.*[^[:space:]])[[:space:]]+[;#].*$'
|
||||
if [[ "$value" =~ $comment_regex ]]; then
|
||||
printf '%s' "${BASH_REMATCH[1]}"
|
||||
else
|
||||
printf '%s' "$value"
|
||||
fi
|
||||
}
|
||||
|
||||
is_success_http_code() {
|
||||
local http_code="$1"
|
||||
[[ "$http_code" =~ ^2[0-9][0-9]$ ]]
|
||||
}
|
||||
|
||||
set_defaults() {
|
||||
: "${HOME_BASE_URL:=https://pam.home.com}"
|
||||
: "${CLIENT_ID:=your_client_id}"
|
||||
: "${CLIENT_SECRET:=your_client_secret}"
|
||||
: "${AIRPORT_CODE:=HET}"
|
||||
: "${APP_NAME:=PAM}"
|
||||
: "${MODULE_NAME:=Node}"
|
||||
: "${VERSION_NUMBER:=2.0.5}"
|
||||
: "${ZIP_FILE_PATH:=/path/to/pam-2.0.5.zip}"
|
||||
: "${ACTION_TYPE:=FULL}"
|
||||
: "${TIMEOUT:=120}"
|
||||
: "${LOG_NAME:=app.log}"
|
||||
}
|
||||
|
||||
load_config() {
|
||||
local config_path="$1"
|
||||
|
||||
if [[ -f "$config_path" ]]; then
|
||||
while IFS= read -r raw_line || [[ -n "$raw_line" ]]; do
|
||||
raw_line="${raw_line%$'\r'}"
|
||||
|
||||
local line
|
||||
line="$(trim "$raw_line")"
|
||||
[[ -z "$line" ]] && continue
|
||||
case "$line" in
|
||||
\#*|\;*) continue ;;
|
||||
esac
|
||||
[[ "$line" != *"="* ]] && continue
|
||||
|
||||
local key="${line%%=*}"
|
||||
local value="${line#*=}"
|
||||
key="$(trim "$key")"
|
||||
value="$(trim "$value")"
|
||||
value="$(strip_inline_comment "$value")"
|
||||
|
||||
case "$key" in
|
||||
HOME_BASE_URL|CLIENT_ID|CLIENT_SECRET|AIRPORT_CODE|APP_NAME|MODULE_NAME|VERSION_NUMBER|ZIP_FILE_PATH|ACTION_TYPE|TIMEOUT|LOG_NAME)
|
||||
printf -v "$key" '%s' "$value"
|
||||
;;
|
||||
esac
|
||||
done < "$config_path"
|
||||
else
|
||||
log_warn "未找到配置文件: $config_path,将使用默认值。"
|
||||
fi
|
||||
|
||||
set_defaults
|
||||
}
|
||||
|
||||
require_tool() {
|
||||
local tool="$1"
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
log_error "缺少依赖: $tool"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_dependencies() {
|
||||
require_tool curl
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
HAS_JQ=1
|
||||
else
|
||||
HAS_JQ=0
|
||||
log_warn "未检测到 jq,将使用 Bash 兼容 JSON 解析。"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_zip_file() {
|
||||
if [[ ! -f "$ZIP_FILE_PATH" ]]; then
|
||||
log_error "软件包不存在: $ZIP_FILE_PATH"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
sanitize_field() {
|
||||
local value="$1"
|
||||
value="${value//$'\r'/ }"
|
||||
value="${value//$'\n'/ }"
|
||||
value="${value//$'\t'/ }"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
json_value() {
|
||||
local input="$1"
|
||||
local query="$2"
|
||||
|
||||
if (( HAS_JQ == 1 )); then
|
||||
printf '%s' "$input" | jq -r "$query // empty" 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "$query" in
|
||||
'.access_token')
|
||||
json_get_string_by_key "$input" "access_token"
|
||||
;;
|
||||
'.status')
|
||||
json_get_string_by_key "$input" "status"
|
||||
;;
|
||||
'.success')
|
||||
json_get_scalar_by_key "$input" "success"
|
||||
;;
|
||||
'.message')
|
||||
json_get_string_by_key "$input" "message"
|
||||
;;
|
||||
'.hashCode // .data.hashCode')
|
||||
local value
|
||||
value="$(json_get_string_by_key "$input" "hashCode")"
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s' "$value"
|
||||
else
|
||||
json_get_nested_string_by_key "$input" "data" "hashCode"
|
||||
fi
|
||||
;;
|
||||
'.[]')
|
||||
json_array_items "$input"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
json_compact() {
|
||||
local input="$1"
|
||||
input="${input//$'\r'/}"
|
||||
input="${input//$'\n'/}"
|
||||
printf '%s' "$input"
|
||||
}
|
||||
|
||||
json_unescape_basic() {
|
||||
local value="$1"
|
||||
value="${value//\\\\/\\}"
|
||||
value="${value//\\\"/\"}"
|
||||
value="${value//\\n/$'\n'}"
|
||||
value="${value//\\r/$'\r'}"
|
||||
value="${value//\\t/$'\t'}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
json_get_string_by_key() {
|
||||
local input="$1"
|
||||
local key="$2"
|
||||
local compact
|
||||
local value
|
||||
|
||||
compact="$(json_compact "$input")"
|
||||
value="$(printf '%s' "$compact" | sed -nE 's/.*"'$key'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p')"
|
||||
[[ -n "$value" ]] && json_unescape_basic "$value"
|
||||
}
|
||||
|
||||
json_get_scalar_by_key() {
|
||||
local input="$1"
|
||||
local key="$2"
|
||||
local compact
|
||||
local value
|
||||
|
||||
compact="$(json_compact "$input")"
|
||||
value="$(printf '%s' "$compact" | sed -nE 's/.*"'$key'"[[:space:]]*:[[:space:]]*([^,}]+).*/\1/p')"
|
||||
value="$(trim "$value")"
|
||||
value="${value%\"}"
|
||||
value="${value#\"}"
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
json_get_nested_string_by_key() {
|
||||
local input="$1"
|
||||
local parent_key="$2"
|
||||
local child_key="$3"
|
||||
local compact
|
||||
local value
|
||||
|
||||
compact="$(json_compact "$input")"
|
||||
value="$(printf '%s' "$compact" | sed -nE 's/.*"'$parent_key'"[[:space:]]*:[[:space:]]*\{[^}]*"'$child_key'"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p')"
|
||||
[[ -n "$value" ]] && json_unescape_basic "$value"
|
||||
}
|
||||
|
||||
json_first_key() {
|
||||
local input="$1"
|
||||
|
||||
if (( HAS_JQ == 1 )); then
|
||||
printf '%s' "$input" | jq -r 'keys[0] // empty' 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
local compact
|
||||
compact="$(json_compact "$input")"
|
||||
printf '%s' "$compact" | sed -nE 's/^[[:space:]]*\{[[:space:]]*"([^"]+)".*/\1/p'
|
||||
}
|
||||
|
||||
json_array_items() {
|
||||
local input="$1"
|
||||
|
||||
if (( HAS_JQ == 1 )); then
|
||||
printf '%s' "$input" | jq -r '.[]' 2>/dev/null
|
||||
return 0
|
||||
fi
|
||||
|
||||
local compact
|
||||
compact="$(json_compact "$input")"
|
||||
compact="${compact#[}"
|
||||
compact="${compact%]}"
|
||||
|
||||
[[ -z "$compact" ]] && return 0
|
||||
|
||||
printf '%s' "$compact" \
|
||||
| sed -E 's/^[[:space:]]*"//; s/"[[:space:]]*$//; s/"[[:space:]]*,[[:space:]]*"/\n/g'
|
||||
}
|
||||
|
||||
http_request() {
|
||||
local method="$1"
|
||||
local url="$2"
|
||||
local data="$3"
|
||||
local content_type="$4"
|
||||
shift 4
|
||||
|
||||
local -a cmd
|
||||
local request_id
|
||||
local headers_text=""
|
||||
local raw_output=""
|
||||
local response=""
|
||||
local http_code=""
|
||||
local curl_exit=0
|
||||
local marker=$'\n__PAM_HTTP_CODE__:'
|
||||
|
||||
cmd=(curl -sS -X "$method" "$url")
|
||||
|
||||
if [[ -n "$TOKEN" ]]; then
|
||||
cmd+=(-H "Authorization: Basic ${TOKEN}")
|
||||
headers_text+="Authorization: Basic ${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: Basic ${TOKEN}")
|
||||
fields_text+="Authorization: Basic ${TOKEN}"$'\n'
|
||||
fi
|
||||
|
||||
cmd+=(-F "file=@${file_path}")
|
||||
|
||||
while (($#)); do
|
||||
cmd+=(-F "$1")
|
||||
fields_text+="$1"$'\n'
|
||||
shift
|
||||
done
|
||||
|
||||
next_trace_id
|
||||
request_id="$CURRENT_TRACE_ID"
|
||||
trace_upload_request "$request_id" "$url" "$file_path" "$fields_text"
|
||||
|
||||
raw_output=$("${cmd[@]}" -w $'\n__PAM_HTTP_CODE__:%{http_code}' 2>&1) || curl_exit=$?
|
||||
if [[ "$raw_output" == *"$marker"* ]]; then
|
||||
response="${raw_output%"$marker"*}"
|
||||
http_code="${raw_output##*__PAM_HTTP_CODE__:}"
|
||||
else
|
||||
response="$raw_output"
|
||||
fi
|
||||
|
||||
trace_response "$request_id" "$curl_exit" "$http_code" "$response"
|
||||
|
||||
if (( curl_exit != 0 )); then
|
||||
log_error "上传失败: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -n "$http_code" ]] && ! is_success_http_code "$http_code"; then
|
||||
log_error "上传返回非成功状态码: ${url} -> HTTP ${http_code}"
|
||||
log_error "$response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s' "$response"
|
||||
}
|
||||
|
||||
get_token() {
|
||||
log_info "正在获取 Token..."
|
||||
local response
|
||||
response=$(http_request "POST" \
|
||||
"${HOME_BASE_URL}/oauth/token" \
|
||||
"grant_type=client_credentials&client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}" \
|
||||
"application/x-www-form-urlencoded") || {
|
||||
log_error "获取 Token 失败: $response"
|
||||
return 1
|
||||
}
|
||||
|
||||
TOKEN="$(json_value "$response" '.access_token')"
|
||||
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
|
||||
log_error "Token 响应无效: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
create_version() {
|
||||
log_info "Step 2.1: 新建版本..."
|
||||
http_request "POST" \
|
||||
"${HOME_BASE_URL}/api/version/upgrade" \
|
||||
"versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&description=Auto Deploy" \
|
||||
"application/x-www-form-urlencoded" >/dev/null
|
||||
}
|
||||
|
||||
upload_package() {
|
||||
log_info "Step 2.2: 上传软件包..."
|
||||
local response
|
||||
response=$(upload_file \
|
||||
"${HOME_BASE_URL}/api/version/upgrade/upload" \
|
||||
"$ZIP_FILE_PATH" \
|
||||
"applicationName=${APP_NAME}" \
|
||||
"moduleName=${MODULE_NAME}" \
|
||||
"versionNumber=${VERSION_NUMBER}") || return 1
|
||||
|
||||
HASH_CODE="$(json_value "$response" '.hashCode // .data.hashCode')"
|
||||
if [[ -z "$HASH_CODE" ]]; then
|
||||
HASH_CODE="$(sanitize_field "$response")"
|
||||
fi
|
||||
|
||||
if [[ -z "$HASH_CODE" ]]; then
|
||||
log_error "无法从上传响应中解析 hashCode: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
publish_version() {
|
||||
log_info "Step 2.3: 发布版本..."
|
||||
local payload
|
||||
payload=$(printf '{"airportCodesWhite":["%s"],"hashCode":"%s","state":"RELEASE"}' "$AIRPORT_CODE" "$HASH_CODE")
|
||||
|
||||
http_request "PUT" \
|
||||
"${HOME_BASE_URL}/api/version/upgrade/profile?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}" \
|
||||
"$payload" \
|
||||
"application/json" >/dev/null
|
||||
}
|
||||
|
||||
get_node_url() {
|
||||
log_info "Step 3.1: 获取 Node 地址..."
|
||||
local response
|
||||
response=$(http_request "GET" \
|
||||
"${HOME_BASE_URL}/api/mcp/airport/target-node?airportCode=${AIRPORT_CODE}" \
|
||||
"" \
|
||||
"") || return 1
|
||||
|
||||
NODE_URL="$(json_first_key "$response")"
|
||||
if [[ -z "$NODE_URL" ]]; then
|
||||
log_error "无法获取 Node 地址: $response"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$NODE_URL" in
|
||||
http://*|https://*) ;;
|
||||
*)
|
||||
log_error "Target-Node 不是有效 URL: $NODE_URL"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$NODE_URL" == *" "* || "$NODE_URL" == *$'\t'* || "$NODE_URL" == *$'\r'* || "$NODE_URL" == *$'\n'* ]]; then
|
||||
log_error "Target-Node 包含非法空白字符: $NODE_URL"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
get_online_ips() {
|
||||
log_info "Step 3.2: 获取在线工作站列表..."
|
||||
local response
|
||||
local ip_lines
|
||||
response=$(http_request "GET" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/ips?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}" \
|
||||
"" \
|
||||
"" \
|
||||
"Target-Node: ${NODE_URL}") || return 1
|
||||
|
||||
ONLINE_IPS=()
|
||||
ip_lines="$(json_array_items "$response")"
|
||||
while IFS= read -r ip; do
|
||||
[[ -n "$ip" ]] && ONLINE_IPS+=("$ip")
|
||||
done <<< "$ip_lines"
|
||||
|
||||
TOTAL_COUNT=${#ONLINE_IPS[@]}
|
||||
if (( TOTAL_COUNT == 0 )); then
|
||||
log_error "无在线工作站匹配该模块。原始响应: $response"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
poll_download_progress() {
|
||||
local progress_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud/progress?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}"
|
||||
local attempt=0
|
||||
local max_attempts=60
|
||||
local error_regex='[Ff]ail|[Ee]rror'
|
||||
|
||||
while (( attempt < max_attempts )); do
|
||||
local response
|
||||
response=$(http_request "GET" "$progress_url" "" "" "Target-Node: ${NODE_URL}") || return 1
|
||||
|
||||
local status
|
||||
status="$(json_value "$response" '.status')"
|
||||
local success_flag
|
||||
success_flag="$(json_value "$response" '.success')"
|
||||
|
||||
if [[ "$status" == "completed" || "$success_flag" == "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local message
|
||||
message="$(json_value "$response" '.message')"
|
||||
if [[ "$message" =~ $error_regex ]]; then
|
||||
log_error "Node 下载失败: $message"
|
||||
return 1
|
||||
fi
|
||||
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log_error "Node 下载超时。"
|
||||
return 1
|
||||
}
|
||||
|
||||
download_cloud_to_node() {
|
||||
log_info "Step 3.3: 下载软件包到 Node..."
|
||||
http_request "GET" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/download-cloud?versionNumber=${VERSION_NUMBER}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&timeOut=${TIMEOUT}" \
|
||||
"" \
|
||||
"" \
|
||||
"Target-Node: ${NODE_URL}" \
|
||||
"airport-code: ${AIRPORT_CODE}" >/dev/null
|
||||
|
||||
poll_download_progress
|
||||
}
|
||||
|
||||
upgrade_ip() {
|
||||
local ip="$1"
|
||||
http_request "POST" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade" \
|
||||
"airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&versionNumber=${VERSION_NUMBER}&action=${ACTION_TYPE}&autoStart=false&timeOut=${TIMEOUT}" \
|
||||
"application/x-www-form-urlencoded" \
|
||||
"Target-Node: ${NODE_URL}"
|
||||
}
|
||||
|
||||
start_application() {
|
||||
local ip="$1"
|
||||
http_request "POST" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop" \
|
||||
"airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runstart=true" \
|
||||
"application/x-www-form-urlencoded" \
|
||||
"Target-Node: ${NODE_URL}" >/dev/null
|
||||
}
|
||||
|
||||
stop_application() {
|
||||
local ip="$1"
|
||||
http_request "POST" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/start-stop" \
|
||||
"airportCode=${AIRPORT_CODE}&targetIp=${ip}&applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&runstart=false" \
|
||||
"application/x-www-form-urlencoded" \
|
||||
"Target-Node: ${NODE_URL}" >/dev/null
|
||||
}
|
||||
|
||||
verify_ip() {
|
||||
local ip="$1"
|
||||
http_request "GET" \
|
||||
"${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/verify?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}" \
|
||||
"" \
|
||||
"" \
|
||||
"Target-Node: ${NODE_URL}"
|
||||
}
|
||||
|
||||
download_log() {
|
||||
local ip="$1"
|
||||
local logs_dir="${SCRIPT_DIR}/logs"
|
||||
local log_file="${logs_dir}/deploy_${ip}.log"
|
||||
local err_file="${logs_dir}/error_${ip}.log"
|
||||
local request_id
|
||||
local trace_url="${HOME_BASE_URL}/node_proxy/${AIRPORT_CODE}/api/mcp/version/upgrade/log-download?applicationName=${APP_NAME}&moduleName=${MODULE_NAME}&airportCode=${AIRPORT_CODE}&targetIp=${ip}&logName=${LOG_NAME}"
|
||||
local curl_exit=0
|
||||
local http_code=""
|
||||
local trace_error=""
|
||||
|
||||
mkdir -p "$logs_dir"
|
||||
next_trace_id
|
||||
request_id="$CURRENT_TRACE_ID"
|
||||
trace_request "$request_id" "GET" "$trace_url" "Authorization: Basic ${TOKEN}"$'\n'"Target-Node: ${NODE_URL}" ""
|
||||
|
||||
if curl -sS -X GET \
|
||||
"$trace_url" \
|
||||
-H "Authorization: Basic ${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
|
||||
5
doc_scripts/test_deploy.bat
Normal file
5
doc_scripts/test_deploy.bat
Normal file
@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
setlocal
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%test_deploy.ps1" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
283
doc_scripts/test_deploy.ps1
Normal file
283
doc_scripts/test_deploy.ps1
Normal file
@ -0,0 +1,283 @@
|
||||
param(
|
||||
[Alias('ConfigPath')]
|
||||
[string]$TestConfigPath = (Join-Path $PSScriptRoot 'config.txt'),
|
||||
[ValidateSet('smoke', 'full')]
|
||||
[string]$Mode = 'full',
|
||||
[switch]$SkipRollback,
|
||||
[int]$MaxIps = 0,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
# PAM 测试脚本(PowerShell 实现)。
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($Help) {
|
||||
@'
|
||||
Usage:
|
||||
powershell -File .\test_deploy.ps1 [-ConfigPath .\config.txt] [-Mode smoke|full] [-SkipRollback] [-MaxIps N]
|
||||
|
||||
Modes:
|
||||
smoke - only test config, token, node, and online IP lookup
|
||||
full - test all deployment interfaces in workflow order
|
||||
|
||||
Notes:
|
||||
- full mode tests create version, upload, publish, node download, upgrade, start, verify, and log download
|
||||
- rollback is tested once by default in full mode
|
||||
- use -SkipRollback if you do not want the test script to change the deployed state
|
||||
- use -MaxIps to limit how many online IPs are used for interface testing
|
||||
'@ | Write-Host
|
||||
exit 0
|
||||
}
|
||||
|
||||
. (Join-Path $PSScriptRoot 'deploy.ps1')
|
||||
|
||||
function Add-TestResult {
|
||||
param(
|
||||
[System.Collections.Generic.List[object]]$Results,
|
||||
[string]$Step,
|
||||
[string]$Status,
|
||||
[string]$Detail
|
||||
)
|
||||
|
||||
$Results.Add([pscustomobject]@{
|
||||
Step = $Step
|
||||
Status = $Status
|
||||
Detail = $Detail
|
||||
}) | Out-Null
|
||||
}
|
||||
|
||||
function Write-TestReport {
|
||||
param([System.Collections.Generic.List[object]]$Results)
|
||||
|
||||
$passCount = @($Results | Where-Object { $_.Status -eq 'PASS' }).Count
|
||||
$failCount = @($Results | Where-Object { $_.Status -eq 'FAIL' }).Count
|
||||
$skipCount = @($Results | Where-Object { $_.Status -eq 'SKIP' }).Count
|
||||
|
||||
Write-Host ''
|
||||
Write-Host '====================== API TEST REPORT ======================'
|
||||
Write-Host "PASS: $passCount"
|
||||
Write-Host "FAIL: $failCount"
|
||||
Write-Host "SKIP: $skipCount"
|
||||
Write-Host ''
|
||||
Write-Host ('{0,-28} {1,-8} {2}' -f 'STEP', 'STATUS', 'DETAIL')
|
||||
foreach ($item in $Results) {
|
||||
Write-Host ('{0,-28} {1,-8} {2}' -f $item.Step, $item.Status, $item.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-PamSmokeTest {
|
||||
param([string]$ConfigPath)
|
||||
|
||||
$results = [System.Collections.Generic.List[object]]::new()
|
||||
$config = Get-PamConfig -Path $ConfigPath
|
||||
|
||||
try {
|
||||
Test-ZipFile -Config $config
|
||||
Add-TestResult -Results $results -Step 'ZIP_FILE' -Status 'PASS' -Detail $config.ZIP_FILE_PATH
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'ZIP_FILE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$token = Get-Token -Config $config
|
||||
Add-TestResult -Results $results -Step 'TOKEN' -Status 'PASS' -Detail ('token length={0}' -f $token.Length)
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'TOKEN' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$nodeUrl = Get-NodeUrl -Config $config -Token $token
|
||||
Add-TestResult -Results $results -Step 'TARGET_NODE' -Status 'PASS' -Detail $nodeUrl
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'TARGET_NODE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$ips = Get-OnlineIps -Config $config -Token $token -NodeUrl $nodeUrl
|
||||
Add-TestResult -Results $results -Step 'ONLINE_IPS' -Status 'PASS' -Detail (($ips -join ', '))
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'ONLINE_IPS' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
Write-TestReport -Results $results
|
||||
}
|
||||
|
||||
function Invoke-PamFullTest {
|
||||
param(
|
||||
[string]$ConfigPath,
|
||||
[bool]$ShouldSkipRollback,
|
||||
[int]$MaxIps
|
||||
)
|
||||
|
||||
$results = [System.Collections.Generic.List[object]]::new()
|
||||
$config = Get-PamConfig -Path $ConfigPath
|
||||
|
||||
try {
|
||||
Test-ZipFile -Config $config
|
||||
Add-TestResult -Results $results -Step 'ZIP_FILE' -Status 'PASS' -Detail $config.ZIP_FILE_PATH
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'ZIP_FILE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$token = Get-Token -Config $config
|
||||
Add-TestResult -Results $results -Step 'TOKEN' -Status 'PASS' -Detail ('token length={0}' -f $token.Length)
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'TOKEN' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
New-VersionRecord -Config $config -Token $token
|
||||
Add-TestResult -Results $results -Step 'CREATE_VERSION' -Status 'PASS' -Detail $config.VERSION_NUMBER
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'CREATE_VERSION' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$hashCode = Upload-Package -Config $config -Token $token
|
||||
Add-TestResult -Results $results -Step 'UPLOAD_PACKAGE' -Status 'PASS' -Detail $hashCode
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'UPLOAD_PACKAGE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
Publish-Version -Config $config -Token $token -HashCode $hashCode
|
||||
Add-TestResult -Results $results -Step 'PUBLISH_VERSION' -Status 'PASS' -Detail $config.AIRPORT_CODE
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'PUBLISH_VERSION' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$nodeUrl = Get-NodeUrl -Config $config -Token $token
|
||||
Add-TestResult -Results $results -Step 'TARGET_NODE' -Status 'PASS' -Detail $nodeUrl
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'TARGET_NODE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
$ips = Get-OnlineIps -Config $config -Token $token -NodeUrl $nodeUrl
|
||||
Add-TestResult -Results $results -Step 'ONLINE_IPS' -Status 'PASS' -Detail (($ips -join ', '))
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'ONLINE_IPS' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
try {
|
||||
Download-CloudToNode -Config $config -Token $token -NodeUrl $nodeUrl
|
||||
Add-TestResult -Results $results -Step 'DOWNLOAD_TO_NODE' -Status 'PASS' -Detail $config.VERSION_NUMBER
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step 'DOWNLOAD_TO_NODE' -Status 'FAIL' -Detail $_.Exception.Message
|
||||
Write-TestReport -Results $results
|
||||
throw
|
||||
}
|
||||
|
||||
$testIps = @($ips)
|
||||
if ($MaxIps -gt 0) {
|
||||
$testIps = @($ips | Select-Object -First $MaxIps)
|
||||
}
|
||||
|
||||
$rollbackTested = $false
|
||||
|
||||
foreach ($ip in $testIps) {
|
||||
try {
|
||||
$upgradeResponse = Invoke-UpgradeRequest -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip
|
||||
$upgradeSuccess = Get-ResponseValue -Response $upgradeResponse -Candidates @('success')
|
||||
$upgradeMessage = Get-ResponseValue -Response $upgradeResponse -Candidates @('message')
|
||||
if ($upgradeSuccess -eq 'true') {
|
||||
Add-TestResult -Results $results -Step "UPGRADE [$ip]" -Status 'PASS' -Detail 'success=true'
|
||||
} else {
|
||||
if (-not $upgradeMessage) { $upgradeMessage = 'success != true' }
|
||||
Add-TestResult -Results $results -Step "UPGRADE [$ip]" -Status 'FAIL' -Detail $upgradeMessage
|
||||
}
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step "UPGRADE [$ip]" -Status 'FAIL' -Detail $_.Exception.Message
|
||||
}
|
||||
|
||||
try {
|
||||
Start-Application -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip
|
||||
Add-TestResult -Results $results -Step "START [$ip]" -Status 'PASS' -Detail 'request completed'
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step "START [$ip]" -Status 'FAIL' -Detail $_.Exception.Message
|
||||
}
|
||||
|
||||
try {
|
||||
$verifyResponse = Verify-Ip -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip
|
||||
$verifySuccess = Get-ResponseValue -Response $verifyResponse -Candidates @('success')
|
||||
$verifyMessage = Get-ResponseValue -Response $verifyResponse -Candidates @('message')
|
||||
if ($verifySuccess -eq 'true') {
|
||||
Add-TestResult -Results $results -Step "VERIFY [$ip]" -Status 'PASS' -Detail 'success=true'
|
||||
} else {
|
||||
if (-not $verifyMessage) { $verifyMessage = 'success != true' }
|
||||
Add-TestResult -Results $results -Step "VERIFY [$ip]" -Status 'FAIL' -Detail $verifyMessage
|
||||
}
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step "VERIFY [$ip]" -Status 'FAIL' -Detail $_.Exception.Message
|
||||
}
|
||||
|
||||
try {
|
||||
$logFile = Download-DeployLog -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip
|
||||
Add-TestResult -Results $results -Step "LOG_DOWNLOAD [$ip]" -Status 'PASS' -Detail $logFile
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step "LOG_DOWNLOAD [$ip]" -Status 'FAIL' -Detail $_.Exception.Message
|
||||
}
|
||||
|
||||
if (-not $ShouldSkipRollback -and -not $rollbackTested) {
|
||||
try {
|
||||
$rollbackResult = Invoke-Rollback -Config $config -Token $token -NodeUrl $nodeUrl -Ip $ip -StopFirst:$true
|
||||
if ($rollbackResult -eq 'ROLLBACK_SUCCESS') {
|
||||
Add-TestResult -Results $results -Step "ROLLBACK [$ip]" -Status 'PASS' -Detail $rollbackResult
|
||||
} else {
|
||||
Add-TestResult -Results $results -Step "ROLLBACK [$ip]" -Status 'FAIL' -Detail $rollbackResult
|
||||
}
|
||||
} catch {
|
||||
Add-TestResult -Results $results -Step "ROLLBACK [$ip]" -Status 'FAIL' -Detail $_.Exception.Message
|
||||
}
|
||||
$rollbackTested = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($ShouldSkipRollback) {
|
||||
Add-TestResult -Results $results -Step 'ROLLBACK' -Status 'SKIP' -Detail 'skipped by parameter'
|
||||
} elseif (-not $rollbackTested) {
|
||||
Add-TestResult -Results $results -Step 'ROLLBACK' -Status 'SKIP' -Detail 'no IP available for rollback test'
|
||||
}
|
||||
|
||||
Write-TestReport -Results $results
|
||||
|
||||
if (@($results | Where-Object { $_.Status -eq 'FAIL' }).Count -gt 0) {
|
||||
throw 'One or more API tests failed.'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($Mode -eq 'smoke') {
|
||||
Invoke-PamSmokeTest -ConfigPath $TestConfigPath
|
||||
} else {
|
||||
Invoke-PamFullTest -ConfigPath $TestConfigPath -ShouldSkipRollback:$SkipRollback.IsPresent -MaxIps $MaxIps
|
||||
}
|
||||
} catch {
|
||||
Write-ErrLog $_
|
||||
exit 1
|
||||
}
|
||||
323
doc_scripts/test_deploy.sh
Normal file
323
doc_scripts/test_deploy.sh
Normal file
@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# PAM 测试脚本(Shell 入口)。
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# 复用部署脚本中的函数供测试脚本调用。
|
||||
# shellcheck source=./deploy.sh
|
||||
source "${SCRIPT_DIR}/deploy.sh"
|
||||
|
||||
TEST_MODE="full"
|
||||
SKIP_ROLLBACK="false"
|
||||
MAX_IPS=0
|
||||
TEST_RESULTS=()
|
||||
HAS_FAILURE=0
|
||||
|
||||
test_usage() {
|
||||
cat <<'EOF'
|
||||
用法:
|
||||
./test_deploy.sh [--config /path/to/config.txt] [--mode smoke|full] [--skip-rollback] [--max-ips N]
|
||||
|
||||
模式:
|
||||
smoke 仅测试配置、Token、Node 和在线 IP 查询
|
||||
full 按部署顺序测试全部接口
|
||||
|
||||
说明:
|
||||
- full 模式会测试建版、上传、发布、Node 下载、升级、启动、校验和日志下载
|
||||
- rollback 默认会额外测试一次
|
||||
- 如果不希望测试脚本改变部署状态,可加 --skip-rollback
|
||||
- --max-ips 可限制参与接口测试的在线 IP 数量
|
||||
EOF
|
||||
}
|
||||
|
||||
add_test_result() {
|
||||
local step="$1"
|
||||
local status="$2"
|
||||
local detail="$3"
|
||||
|
||||
TEST_RESULTS+=("${step}|${status}|${detail}")
|
||||
if [[ "$status" == "FAIL" ]]; then
|
||||
HAS_FAILURE=1
|
||||
fi
|
||||
|
||||
case "$status" in
|
||||
PASS)
|
||||
printf '[PASS] %s | %s\n' "$step" "$detail"
|
||||
;;
|
||||
FAIL)
|
||||
printf '[FAIL] %s | %s\n' "$step" "$detail" >&2
|
||||
;;
|
||||
SKIP)
|
||||
printf '[SKIP] %s | %s\n' "$step" "$detail"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
print_test_report() {
|
||||
local pass_count=0
|
||||
local fail_count=0
|
||||
local skip_count=0
|
||||
|
||||
for row in "${TEST_RESULTS[@]}"; do
|
||||
IFS='|' read -r _step _status _detail <<< "$row"
|
||||
case "$_status" in
|
||||
PASS) pass_count=$((pass_count + 1)) ;;
|
||||
FAIL) fail_count=$((fail_count + 1)) ;;
|
||||
SKIP) skip_count=$((skip_count + 1)) ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '\n====================== API TEST REPORT ======================\n'
|
||||
printf 'PASS: %s\n' "$pass_count"
|
||||
printf 'FAIL: %s\n' "$fail_count"
|
||||
printf 'SKIP: %s\n\n' "$skip_count"
|
||||
printf '%-28s %-8s %s\n' "STEP" "STATUS" "DETAIL"
|
||||
|
||||
for row in "${TEST_RESULTS[@]}"; do
|
||||
IFS='|' read -r step status detail <<< "$row"
|
||||
printf '%-28s %-8s %s\n' "$step" "$status" "$detail"
|
||||
done
|
||||
}
|
||||
|
||||
run_smoke_test() {
|
||||
load_config "$1"
|
||||
ensure_dependencies
|
||||
|
||||
if ensure_zip_file; then
|
||||
add_test_result "ZIP_FILE" "PASS" "$ZIP_FILE_PATH"
|
||||
else
|
||||
add_test_result "ZIP_FILE" "FAIL" "$ZIP_FILE_PATH"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_token; then
|
||||
add_test_result "TOKEN" "PASS" "token length=${#TOKEN}"
|
||||
else
|
||||
add_test_result "TOKEN" "FAIL" "token request failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_node_url; then
|
||||
add_test_result "TARGET_NODE" "PASS" "$NODE_URL"
|
||||
else
|
||||
add_test_result "TARGET_NODE" "FAIL" "node lookup failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_online_ips; then
|
||||
add_test_result "ONLINE_IPS" "PASS" "${ONLINE_IPS[*]}"
|
||||
else
|
||||
add_test_result "ONLINE_IPS" "FAIL" "online ip lookup failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_test_report
|
||||
}
|
||||
|
||||
run_full_test() {
|
||||
local config_path="$1"
|
||||
local rollback_tested="false"
|
||||
local -a test_ips=()
|
||||
|
||||
load_config "$config_path"
|
||||
ensure_dependencies
|
||||
|
||||
if ensure_zip_file; then
|
||||
add_test_result "ZIP_FILE" "PASS" "$ZIP_FILE_PATH"
|
||||
else
|
||||
add_test_result "ZIP_FILE" "FAIL" "$ZIP_FILE_PATH"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_token; then
|
||||
add_test_result "TOKEN" "PASS" "token length=${#TOKEN}"
|
||||
else
|
||||
add_test_result "TOKEN" "FAIL" "token request failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if create_version; then
|
||||
add_test_result "CREATE_VERSION" "PASS" "$VERSION_NUMBER"
|
||||
else
|
||||
add_test_result "CREATE_VERSION" "FAIL" "$VERSION_NUMBER"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if upload_package; then
|
||||
add_test_result "UPLOAD_PACKAGE" "PASS" "$HASH_CODE"
|
||||
else
|
||||
add_test_result "UPLOAD_PACKAGE" "FAIL" "upload failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if publish_version; then
|
||||
add_test_result "PUBLISH_VERSION" "PASS" "$AIRPORT_CODE"
|
||||
else
|
||||
add_test_result "PUBLISH_VERSION" "FAIL" "publish failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_node_url; then
|
||||
add_test_result "TARGET_NODE" "PASS" "$NODE_URL"
|
||||
else
|
||||
add_test_result "TARGET_NODE" "FAIL" "node lookup failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if get_online_ips; then
|
||||
add_test_result "ONLINE_IPS" "PASS" "${ONLINE_IPS[*]}"
|
||||
else
|
||||
add_test_result "ONLINE_IPS" "FAIL" "online ip lookup failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
if download_cloud_to_node; then
|
||||
add_test_result "DOWNLOAD_TO_NODE" "PASS" "$VERSION_NUMBER"
|
||||
else
|
||||
add_test_result "DOWNLOAD_TO_NODE" "FAIL" "node download failed"
|
||||
print_test_report
|
||||
return 1
|
||||
fi
|
||||
|
||||
test_ips=("${ONLINE_IPS[@]}")
|
||||
if (( MAX_IPS > 0 )) && (( ${#test_ips[@]} > MAX_IPS )); then
|
||||
test_ips=("${test_ips[@]:0:MAX_IPS}")
|
||||
fi
|
||||
|
||||
for ip in "${test_ips[@]}"; do
|
||||
local upgrade_response=""
|
||||
local upgrade_success=""
|
||||
local upgrade_message=""
|
||||
local verify_response=""
|
||||
local verify_success=""
|
||||
local verify_message=""
|
||||
local log_file=""
|
||||
local rollback_result=""
|
||||
|
||||
if upgrade_response="$(upgrade_ip "$ip")"; then
|
||||
upgrade_success="$(json_value "$upgrade_response" '.success')"
|
||||
upgrade_message="$(json_value "$upgrade_response" '.message')"
|
||||
if [[ "$upgrade_success" == "true" ]]; then
|
||||
add_test_result "UPGRADE [$ip]" "PASS" "success=true"
|
||||
else
|
||||
[[ -z "$upgrade_message" ]] && upgrade_message="success != true"
|
||||
add_test_result "UPGRADE [$ip]" "FAIL" "$upgrade_message"
|
||||
fi
|
||||
else
|
||||
add_test_result "UPGRADE [$ip]" "FAIL" "request failed"
|
||||
fi
|
||||
|
||||
if start_application "$ip"; then
|
||||
add_test_result "START [$ip]" "PASS" "request completed"
|
||||
else
|
||||
add_test_result "START [$ip]" "FAIL" "request failed"
|
||||
fi
|
||||
|
||||
if verify_response="$(verify_ip "$ip")"; then
|
||||
verify_success="$(json_value "$verify_response" '.success')"
|
||||
verify_message="$(json_value "$verify_response" '.message')"
|
||||
if [[ "$verify_success" == "true" ]]; then
|
||||
add_test_result "VERIFY [$ip]" "PASS" "success=true"
|
||||
else
|
||||
[[ -z "$verify_message" ]] && verify_message="success != true"
|
||||
add_test_result "VERIFY [$ip]" "FAIL" "$verify_message"
|
||||
fi
|
||||
else
|
||||
add_test_result "VERIFY [$ip]" "FAIL" "request failed"
|
||||
fi
|
||||
|
||||
if log_file="$(download_log "$ip")"; then
|
||||
add_test_result "LOG_DOWNLOAD [$ip]" "PASS" "$log_file"
|
||||
else
|
||||
add_test_result "LOG_DOWNLOAD [$ip]" "FAIL" "log download failed: ${log_file}"
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_ROLLBACK" == "false" && "$rollback_tested" == "false" ]]; then
|
||||
rollback_result="$(rollback_ip "$ip" "true")"
|
||||
if [[ "$rollback_result" == "ROLLBACK_SUCCESS" ]]; then
|
||||
add_test_result "ROLLBACK [$ip]" "PASS" "$rollback_result"
|
||||
else
|
||||
add_test_result "ROLLBACK [$ip]" "FAIL" "$rollback_result"
|
||||
fi
|
||||
rollback_tested="true"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$SKIP_ROLLBACK" == "true" ]]; then
|
||||
add_test_result "ROLLBACK" "SKIP" "skipped by parameter"
|
||||
elif [[ "$rollback_tested" == "false" ]]; then
|
||||
add_test_result "ROLLBACK" "SKIP" "no IP available for rollback test"
|
||||
fi
|
||||
|
||||
print_test_report
|
||||
|
||||
if (( HAS_FAILURE != 0 )); then
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
;;
|
||||
--mode)
|
||||
[[ $# -lt 2 ]] && { log_error "--mode 缺少取值"; exit 1; }
|
||||
TEST_MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-rollback)
|
||||
SKIP_ROLLBACK="true"
|
||||
shift
|
||||
;;
|
||||
--max-ips)
|
||||
[[ $# -lt 2 ]] && { log_error "--max-ips 缺少取值"; exit 1; }
|
||||
MAX_IPS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
test_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "未知参数: $1"
|
||||
test_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$TEST_MODE" in
|
||||
smoke)
|
||||
run_smoke_test "$config_path"
|
||||
;;
|
||||
full)
|
||||
run_full_test "$config_path"
|
||||
;;
|
||||
*)
|
||||
log_error "不支持的测试模式: $TEST_MODE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
313
doc_scripts/当前脚本情况总结.md
Normal file
313
doc_scripts/当前脚本情况总结.md
Normal file
@ -0,0 +1,313 @@
|
||||
# 当前脚本情况总结
|
||||
|
||||
## 1. 当前文件说明
|
||||
|
||||
当前目录下与部署直接相关的脚本如下:
|
||||
|
||||
- `deploy.sh`
|
||||
- Shell 主部署脚本
|
||||
- 适合 Git Bash / Linux / macOS 环境
|
||||
- `test_deploy.sh`
|
||||
- Shell 测试脚本
|
||||
- 默认执行全接口测试
|
||||
- `deploy.ps1`
|
||||
- PowerShell 主部署脚本
|
||||
- Windows 下的实际实现入口
|
||||
- `test_deploy.ps1`
|
||||
- PowerShell 测试脚本
|
||||
- 支持 `smoke` / `full` 两种模式
|
||||
- `deploy.bat`
|
||||
- Windows 包装入口
|
||||
- 实际调用 `deploy.ps1`
|
||||
- `test_deploy.bat`
|
||||
- Windows 测试包装入口
|
||||
- 实际调用 `test_deploy.ps1`
|
||||
- `config.txt.example`
|
||||
- 配置示例文件
|
||||
|
||||
## 2. 当前能力
|
||||
|
||||
### 2.1 主部署脚本
|
||||
|
||||
`deploy.sh` / `deploy.ps1` 当前都覆盖以下流程:
|
||||
|
||||
1. 获取 Token
|
||||
2. 新建版本记录
|
||||
3. 上传软件包
|
||||
4. 发布版本
|
||||
5. 获取 Target Node
|
||||
6. 获取在线工作站 IP 列表
|
||||
7. 下载软件包到 Node
|
||||
8. 逐台执行升级
|
||||
9. 启动应用
|
||||
10. 健康检查
|
||||
11. 下载日志
|
||||
12. 失败时触发回滚
|
||||
13. 输出最终部署报告
|
||||
|
||||
### 2.2 测试脚本
|
||||
|
||||
`test_deploy.sh` / `test_deploy.ps1` 当前支持:
|
||||
|
||||
- `smoke`
|
||||
- 仅测试配置、Token、Node、在线 IP 查询
|
||||
- `full`
|
||||
- 按完整部署顺序测试全部接口
|
||||
|
||||
`full` 模式会测试到:
|
||||
|
||||
1. `oauth/token`
|
||||
2. `api/version/upgrade`
|
||||
3. `api/version/upgrade/upload`
|
||||
4. `api/version/upgrade/profile`
|
||||
5. `api/mcp/airport/target-node`
|
||||
6. `api/mcp/version/upgrade/ips`
|
||||
7. `api/mcp/version/upgrade/download-cloud`
|
||||
8. `api/mcp/version/upgrade/download-cloud/progress`
|
||||
9. `api/mcp/version/upgrade`
|
||||
10. `api/mcp/version/upgrade/start-stop`
|
||||
11. `api/mcp/version/upgrade/verify`
|
||||
12. `api/mcp/version/upgrade/log-download`
|
||||
13. `api/mcp/version/upgrade/rollback`
|
||||
|
||||
## 3. 当前运行方式
|
||||
|
||||
### 3.1 Windows
|
||||
|
||||
正式部署:
|
||||
|
||||
```bat
|
||||
deploy.bat -ConfigPath .\config.txt
|
||||
```
|
||||
|
||||
全接口测试:
|
||||
|
||||
```bat
|
||||
test_deploy.bat -ConfigPath .\config.txt
|
||||
```
|
||||
|
||||
只做轻量测试:
|
||||
|
||||
```bat
|
||||
test_deploy.bat -ConfigPath .\config.txt -Mode smoke
|
||||
```
|
||||
|
||||
不测回滚:
|
||||
|
||||
```bat
|
||||
test_deploy.bat -ConfigPath .\config.txt -SkipRollback
|
||||
```
|
||||
|
||||
限制只测前 N 台在线 IP:
|
||||
|
||||
```bat
|
||||
test_deploy.bat -ConfigPath .\config.txt -MaxIps 1
|
||||
```
|
||||
|
||||
### 3.2 Shell / Git Bash
|
||||
|
||||
正式部署:
|
||||
|
||||
```bash
|
||||
bash ./deploy.sh --config ./config.txt
|
||||
```
|
||||
|
||||
全接口测试:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode full
|
||||
```
|
||||
|
||||
轻量测试:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode smoke
|
||||
```
|
||||
|
||||
跳过回滚:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode full --skip-rollback
|
||||
```
|
||||
|
||||
限制 IP 数量:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode full --max-ips 1
|
||||
```
|
||||
|
||||
## 4. 已处理的问题
|
||||
|
||||
### 4.1 Windows Batch 中 `!` 被吞掉
|
||||
|
||||
之前的核心问题是:
|
||||
|
||||
- `setlocal EnableDelayedExpansion` 打开后
|
||||
- `CLIENT_SECRET` 中如果有 `!`
|
||||
- 传给 `curl` 时会被 Batch 吞掉
|
||||
|
||||
当前处理方式:
|
||||
|
||||
- `deploy.bat` / `test_deploy.bat` 只做包装
|
||||
- 真实逻辑全部转给 `deploy.ps1` / `test_deploy.ps1`
|
||||
- 不再在 Batch 中直接拼接敏感参数
|
||||
|
||||
### 4.2 PowerShell 对 JSON 解析兼容问题
|
||||
|
||||
之前测试环境里出现过:
|
||||
|
||||
- 接口已经返回 `access_token`
|
||||
- 但脚本仍报 `Invalid token response`
|
||||
|
||||
当前处理方式:
|
||||
|
||||
- `PowerShell 6+` 使用 `ConvertFrom-Json -Depth 100`
|
||||
- `Windows PowerShell 5.x` 使用兼容写法 `ConvertFrom-Json`
|
||||
|
||||
### 4.3 Git Bash 对 `[[ ... =~ ... ]]` 和进程替换兼容问题
|
||||
|
||||
之前 Shell 侧出现过:
|
||||
|
||||
- 正则匹配语法在 Git Bash 下报错
|
||||
- `done < <(...)` 在当前环境下报错
|
||||
|
||||
当前处理方式:
|
||||
|
||||
- 改成更兼容的正则变量写法
|
||||
- 改成 here-string 方式读取解析结果
|
||||
|
||||
### 4.4 `jq` 缺失问题
|
||||
|
||||
当前 `deploy.sh` 已做成:
|
||||
|
||||
- 有 `jq`:优先使用 `jq`
|
||||
- 没有 `jq`:自动降级为 Bash 兼容 JSON 解析
|
||||
|
||||
已覆盖的 fallback 解析场景:
|
||||
|
||||
- `access_token`
|
||||
- `status`
|
||||
- `success`
|
||||
- `message`
|
||||
- `hashCode`
|
||||
- `data.hashCode`
|
||||
- 顶层对象首个 key
|
||||
- 顶层字符串数组
|
||||
|
||||
注意:
|
||||
|
||||
- 这不是完整 JSON 解析器
|
||||
- 目前是针对当前 PAM 接口返回结构做的兼容处理
|
||||
|
||||
### 4.5 日志接口 `401` 仍显示通过
|
||||
|
||||
之前 Shell 测试脚本里:
|
||||
|
||||
- `LOG_DOWNLOAD` 只看文件是否存在
|
||||
- 即使接口返回 `401`
|
||||
- 也可能被误判成 `PASS`
|
||||
|
||||
当前处理方式:
|
||||
|
||||
- `http_request` 和 `upload_file` 已增加 HTTP 状态码校验
|
||||
- 只有 `2xx` 才算成功
|
||||
- `download_log` 也会校验 HTTP 状态码
|
||||
- `test_deploy.sh` 的 `LOG_DOWNLOAD` 现在按返回码判定,不再只按文件存在判定
|
||||
|
||||
### 4.6 `Target-Node` 未校验
|
||||
|
||||
之前如果 `target-node` 接口返回的 key 不是合法 URL:
|
||||
|
||||
- 后续所有 Node 相关接口都会异常
|
||||
- 但问题源头不够直观
|
||||
|
||||
当前处理方式:
|
||||
|
||||
- `get_node_url` 已加入 `Target-Node` 校验
|
||||
- 必须是 `http://` 或 `https://` 开头
|
||||
- 不允许包含空格、制表符、换行等非法空白字符
|
||||
|
||||
## 5. 当前日志能力
|
||||
|
||||
### 5.1 业务日志
|
||||
|
||||
测试脚本当前会在控制台边跑边打印:
|
||||
|
||||
```text
|
||||
[PASS] TOKEN | ...
|
||||
[FAIL] VERIFY [ip] | ...
|
||||
[SKIP] ROLLBACK | ...
|
||||
```
|
||||
|
||||
最后还会输出汇总报告:
|
||||
|
||||
- `PASS`
|
||||
- `FAIL`
|
||||
- `SKIP`
|
||||
|
||||
### 5.2 接口跟踪日志
|
||||
|
||||
`deploy.sh` 当前已经加入接口级 trace 日志。
|
||||
|
||||
日志特点:
|
||||
|
||||
- 位置:`logs/api_trace_YYYYMMDD_HHMMSS_PID.log`
|
||||
- 自动记录每个请求
|
||||
- 自动生成请求编号,如 `REQ-0001`
|
||||
|
||||
当前已记录内容:
|
||||
|
||||
- 请求时间
|
||||
- 请求方法
|
||||
- 请求 URL
|
||||
- 请求头
|
||||
- 请求体
|
||||
- `curl` 退出码
|
||||
- HTTP 状态码
|
||||
- 响应体
|
||||
|
||||
已做脱敏:
|
||||
|
||||
- `client_secret`
|
||||
- `Authorization`
|
||||
- `access_token`
|
||||
|
||||
## 6. 当前已知限制
|
||||
|
||||
1. Shell 侧虽然支持无 `jq` 运行,但 fallback JSON 解析只针对当前接口结构,不适合复杂 JSON。
|
||||
2. 回滚测试默认会真实调用回滚接口,会改变测试环境状态;如不希望改变状态,需要显式加 `--skip-rollback`。
|
||||
3. `full` 模式会真实创建版本、上传包、发布、升级、启动、回滚,因此它不是只读检查。
|
||||
4. PowerShell 与 Shell 两套脚本逻辑大体对齐,但最新的一些 Shell 侧 trace 和兼容修复,后续仍建议同步到 PowerShell 版本。
|
||||
|
||||
## 7. 当前建议
|
||||
|
||||
1. 在测试环境优先使用测试脚本,不要直接先跑正式部署脚本。
|
||||
2. 第一次联调建议先跑:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode smoke
|
||||
```
|
||||
|
||||
3. 基础联通没问题后再跑:
|
||||
|
||||
```bash
|
||||
bash ./test_deploy.sh --config ./config.txt --mode full --skip-rollback
|
||||
```
|
||||
|
||||
4. 如果要排查具体接口问题,优先查看:
|
||||
|
||||
- 控制台 `[PASS] / [FAIL]`
|
||||
- `logs/api_trace_*.log`
|
||||
|
||||
5. 如果某一台 IP 的 `verify`、`log-download` 或 `rollback` 异常,建议把对应 `REQ-xxxx` 的 trace 片段单独摘出来分析。
|
||||
|
||||
## 8. 后续可继续优化的方向
|
||||
|
||||
1. 把 PowerShell 脚本里的帮助文本、输出文案进一步统一为中文。
|
||||
2. 把 Shell 侧新增的接口 trace 能力同步到 PowerShell 版本。
|
||||
3. 给接口 trace 再拆分摘要视图,例如:
|
||||
- `summary.log`
|
||||
- `request.log`
|
||||
- `response.log`
|
||||
4. 给测试脚本增加“只测试指定接口”的模式,方便单点排障。
|
||||
5. 给 `download_log` 增加更明确的响应内容校验,而不只是 HTTP 状态码。
|
||||
Loading…
x
Reference in New Issue
Block a user