760 lines
29 KiB
Markdown
760 lines
29 KiB
Markdown
# 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: Bearer ${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: Bearer ${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: Bearer %TOKEN%'" > "%OUTPUT_FILE%" 2>"%ERROR_FILE%"
|
||
:: 注意:如果 DATA 非空,需要重新构造命令。为了简化,我们假设大部分 POST 数据是简单的 form-urlencoded,且不含极端特殊字符,或者使用 PowerShell 内部变量传递。
|
||
:: **更安全的做法**:对于包含复杂 Data 的请求,直接使用 PowerShell。
|
||
) else (
|
||
curl -X %METHOD% '%URL%' -H 'Authorization: Bearer %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 脚本中执行。
|