提交 b33053c4 authored 作者: 陈泽健's avatar 陈泽健

feat(script): 添加 Windows 服务自检脚本

- 实现远程服务器健康检查功能
- 支持预设服务器列表和手动输入
- 自动检测平台类型(新统一平台/传统平台)
- 识别系统容器类型(ujava/upython/upython_voice)
- 检测相应服务进程和端口状态
- 生成详细检测报告和日志记录
- 支持 plink 和 sshpass 两种 SSH 工具
- 兼容中英文混合环境编码问题
- 提供彩色控制台输出和日志文件记录
上级 381348db
...@@ -101,6 +101,28 @@ ...@@ -101,6 +101,28 @@
| 进度显示 | 打包和上传过程显示进度条 | ✅ 已实现 | | 进度显示 | 打包和上传过程显示进度条 | ✅ 已实现 |
| 密码加密 | 网盘密码使用 base64 加密存储 | ✅ 已实现 | | 密码加密 | 网盘密码使用 base64 加密存储 | ✅ 已实现 |
### 新增脚本remote_update_win
> 参考脚本:E:\GithubData\自动化\ubains-module-test\辅助工具\脚本工具\远程容器更新\remote_update.sh
> 新增脚本:E:\GithubData\自动化\ubains-module-test\辅助工具\脚本工具\远程容器更新\remote_update_win
> 脚本要求:最好能够直接运行,不用安装依赖
| 功能模块 | 描述 | 状态 |
|----------|------|------|
| 服务器选择 | 支持预设服务器列表和手动输入(IP/端口/用户名/密码) | ❌ 待开发 |
| 自定义端口 | 支持自定义 SSH 端口(默认 22) | ❌ 待开发 |
| 架构校验 | 校验目标服务器是否为 x86 架构 | ❌ 待开发 |
| 镜像传输 | 自动传输镜像文件和部署脚本 | ❌ 待开发|
| 容器停止 | 自动停止远端旧容器 | ❌ 待开发 |
| 平台识别 | 自动检测目标服务器平台类型(检测 /data/services 目录) | ❌ 待开发 |
| 版本校验 | 自动校验远端容器镜像版本是否已更新 | ❌ 待开发 |
| EMQX 同步 | 同步 EMQX 配置、数据、日志目录 | ❌ 待开发 |
| Python 同步 | 同步 Python 代码和配置 | ⏸️ 暂停 |
| Nginx 同步 | 同步 Nginx 配置、HTML、证书 | ❌ 待开发 |
| 容器编号 | 自动递增容器编号 | ❌ 待开发 |
| 远端执行 | 调用远端部署脚本 | ❌ 待开发 |
| 日志审计 | 每一步骤的日志都需要记录到log文件中 | ❌ 待开发 |
### 待开发功能 ### 待开发功能
| 功能 | 描述 | 状态 | | 功能 | 描述 | 状态 |
......
# 服务自检需求说明文档
## 📋 概述
本脚本主要用于服务自检,用于检测服务器上的服务是否正常启动,并返回结果。
### 背景
目前针对系统服务进行自检,需要区分两种平台环境:
- **新统一平台**:使用 `/data/` 目录结构
- **传统平台**:使用 `/var/www/` 目录结构
## 🎯 功能实现总览
### 服务自检 (`check_server_health.ps1`)
#### 检测需求
##### SSH连接(✅ 已实现):
支持预设服务器列表和手动输入(IP/端口/用户名/密码)
##### 平台识别(✅ 已实现):
自动检测目标服务器平台类型(检测 /data/services 目录,如果没有则是传统平台)
##### 系统识别(✅ 已实现):
自动检测目标服务器的系统类型(检测容器分为三种:ujava、upython、upython_voice,如果有ujava则有会议预定系统、python对应运维集控系统、upython_voice对应转录系统)
##### 服务进程检测(❌ 开发进行中):
根据平台类型不同需要分别在不同的位置进行检测,具体如下:
###### 新统一平台(✅ 已实现):
ujava后端服务分为容器内和容器外
需进入ujava2容器内检查,共有以下五个基础服务进程:
["auth"]="ubains-auth.jar"
["gateway"]="ubains-gateway.jar"
["system"]="ubains-modules-system.jar"
["meeting2.0"]="java-meeting2.0/ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
["meeting3.0"]="java-meeting3.0/ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
["mqtt"]="ubains-meeting-mqtt-1.0-SNAPSHOT.jar"
["quartz"]="ubains-meeting-quartz-1.0-SNAPSHOT.jar"
["message"]="ubains-meeting-message-scheduling-1.0-SNAPSHOT.jar"
在宿主机上路径:
/data/services/api/java-meeting/java-meeting-extapi目录下的ubains-meeting-api-1.0-SNAPSHOT.jar进程
upython后端服务需进入upython容器内检查,共有以下4个端口:
tcp 0 0 0.0.0.0:11211 0.0.0.0:* LISTEN 14/memcached
tcp 0 0 127.0.0.1:36917 0.0.0.0:* LISTEN 38/uwsgi
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 38/uwsgi
tcp6 0 0 :::11211 :::* LISTEN 14/memcached
upython_voice后端服务需进入upython_voice容器内检查,共有以下:
tcp 0 0 127.0.0.1:39573 0.0.0.0:* LISTEN 114/uwsgi
tcp 0 0 0.0.0.0:1883 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 114/uwsgi
tcp 0 0 0.0.0.0:9001 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:11211 0.0.0.0:* LISTEN 79/memcached
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 47/nginx: master pr
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 47/nginx: master pr
tcp6 0 0 :::1883 :::* LISTEN -
tcp6 0 0 :::11211 :::* LISTEN 79/memcached
tcp6 0 0 :::80 :::* LISTEN 47/nginx: master pr
###### 传统平台(❌ 未实现):
ujava后端服务分为容器内和容器外
需进入ujava2容器内检查,共有以下两个基础服务进程:
root 8 1 0 15:26 ? 00:00:00 nginx: master process /usr/local/nginx/sbin/nginx
ubains-meeting-inner-api-1.0-SNAPSHOT.jar
在宿主机路径:/var/www/java/external-meeting-api的进程:
ubains-meeting-api-1.0-SNAPSHOT.jar
upython后端服务在容器内:
tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN 101/nginx: master p
tcp 0 0 127.0.0.1:37817 0.0.0.0:* LISTEN 124/uwsgi
tcp 0 0 0.0.0.0:8443 0.0.0.0:* LISTEN 101/nginx: master p
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 124/uwsgi
tcp 0 0 0.0.0.0:8002 0.0.0.0:* LISTEN 14/httpd
tcp 0 0 0.0.0.0:11211 0.0.0.0:* LISTEN 105/memcached
tcp6 0 0 :::11211 :::* LISTEN 105/memcached
##### DNS解析问题(❌ 未实现):
检测目标服务器的DNS配置,能否正常进行解析等相关操作
##### 服务器资源分析(❌ 未实现):
检查目标服务器的磁盘空间情况、内存使用情况以及cpu使用情况
\ No newline at end of file
#Requires -Version 5.1
<#
.SYNOPSIS
服务自检脚本 (Windows 版本)
.DESCRIPTION
本脚本用于远程检测服务器上的服务是否正常启动,并返回结果。
功能特性:
1. 支持预设服务器列表和手动输入(IP/端口/用户名/密码)
2. 支持自定义 SSH 端口(默认 22)
3. 自动检测目标服务器平台类型(检测 /data/services 目录)
4. 自动识别系统类型(ujava/upython/upython_voice 容器)
5. 根据平台和容器类型检测相应的服务进程和端口
6. 生成详细的检测报告
.EXAMPLE
.\check_server_health.ps1
.NOTES
作者: 自动化运维团队
版本: 1.0.0
============================================================
依赖说明(零安装,开箱即用):
============================================================
本脚本需要 plink.exe(PuTTY 工具)来实现自动密码认证。
原因:Windows 原生 SSH 不支持通过命令行传递密码
使用方式(任选其一):
方式1 (推荐,离线可用):
将 plink.exe 放在脚本同目录下
脚本会自动检测并使用本地的可执行文件
下载地址(可在有网络的电脑下载后拷贝):
https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe
方式2: 已安装 PuTTY 到系统 PATH
脚本会自动检测系统中已安装的 plink
============================================================
#>
# 严格模式
Set-StrictMode -Version Latest
$ErrorActionPreference = "Continue"
# ================================
# 设置控制台编码为 UTF-8
# ================================
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8"
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
$PSDefaultParameterValues['*:Encoding'] = 'utf8'
# ================================
# 全局配置
# ================================
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$SSH_TIMEOUT = 30
# PuTTY 工具路径
$script:PLINK_PATH = $null
$script:PreferredSSHTool = $null
# ================================
# 日志配置
# ================================
$LOG_DIR = Join-Path $SCRIPT_DIR "logs"
$LOG_TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss"
$LOG_FILE = Join-Path $LOG_DIR "health_check_$LOG_TIMESTAMP.log"
# 确保日志目录存在
if (-not (Test-Path $LOG_DIR)) {
New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null
}
# 预设服务器列表
$ServerList = @{
"1" = @{
IP = "192.168.5.48"
User = "root"
Pass = "Ubains@123"
Desc = "标准版预定运维服务器"
}
"2" = @{
IP = "192.168.5.67"
User = "root"
Pass = "Ubains@123"
Desc = "阿曼项目预定服务器"
}
"3" = @{
IP = "192.168.5.47"
User = "root"
Pass = "Ubains@1234"
Desc = "标准版预定运维测试发布服务器"
}
}
# ================================
# 服务检测配置
# ================================
# ujava 基础服务进程(支持容器内和宿主机检测)
# 使用jar文件名作为匹配模式,不依赖完整路径
$UjavaServices = @{
"auth" = "ubains-auth.jar"
"gateway" = "ubains-gateway.jar"
"system" = "ubains-modules-system.jar"
"meeting2.0" = "ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
"meeting3.0" = "ubains-meeting-inner-api-1.0-SNAPSHOT.jar"
"mqtt" = "ubains-meeting-mqtt-1.0-SNAPSHOT.jar"
"quartz" = "ubains-meeting-quartz-1.0-SNAPSHOT.jar"
"message" = "ubains-meeting-message-scheduling-1.0-SNAPSHOT.jar"
}
# ujava 宿主机服务(新统一平台和传统平台)
# 新统一平台路径:/data/services/api/java-meeting/java-meeting-extapi/
# 传统平台路径:可能在 /var/www/ 下
$UjavaHostServices = @{
"extapi" = "ubains-meeting-api-1.0-SNAPSHOT.jar"
}
# upython 容器内需要监听的端口
$UpythonPorts = @(
@{ Port = 11211; Process = "memcached"; Description = "Memcached 缓存服务" }
@{ Port = 8000; Process = "uwsgi"; Description = "uWSGI 应用服务" }
)
# upython_voice 容器内需要监听的端口
$UpythonVoicePorts = @(
@{ Port = 1883; Process = "mosquitto"; Description = "MQTT Broker 服务" }
@{ Port = 8000; Process = "uwsgi"; Description = "uWSGI 应用服务" }
@{ Port = 9001; Process = "mosquitto"; Description = "MQTT WebSocket 服务" }
@{ Port = 11211; Process = "memcached"; Description = "Memcached 缓存服务" }
@{ Port = 8080; Process = "nginx"; Description = "Nginx 代理服务 (8080)" }
@{ Port = 80; Process = "nginx"; Description = "Nginx Web 服务 (80)" }
)
# ================================
# 日志函数
# ================================
function Write-Log {
param(
[Parameter(Mandatory=$true)]
[ValidateSet("INFO", "WARN", "ERROR", "SUCCESS")]
[string]$Level,
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[string]$Message
)
# 如果消息为空,直接输出空行到控制台,不记录到日志文件
if ([string]::IsNullOrEmpty($Message)) {
Write-Host ""
return
}
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "[$timestamp] [$Level] $Message"
$colorMap = @{
"INFO" = "Cyan"
"WARN" = "Yellow"
"ERROR" = "Red"
"SUCCESS" = "Green"
}
$color = $colorMap[$Level]
Write-Host $logLine -ForegroundColor $color
try {
$logLine | Out-File -FilePath $LOG_FILE -Append -Encoding utf8
}
catch {
# 日志文件写入失败不影响主流程
}
}
# ================================
# 检查依赖
# ================================
function Test-Dependencies {
Write-Log -Level "INFO" -Message "检查系统依赖..."
$osVersion = [System.Environment]::OSVersion.Version
Write-Log -Level "INFO" -Message " Windows 版本: $($osVersion.Major).$($osVersion.Minor).$($osVersion.Build)"
$hasPasswordTool = $false
# 1. 优先检查脚本同目录下的 plink.exe
$localPlinkPath = Join-Path $SCRIPT_DIR "plink.exe"
if (Test-Path $localPlinkPath) {
$script:PLINK_PATH = $localPlinkPath
Write-Log -Level "INFO" -Message " plink 已找到 (本地): $localPlinkPath"
$hasPasswordTool = $true
$script:PreferredSSHTool = "plink"
}
else {
# 2. 检查远程容器更新目录下的 plink.exe
$remoteUpdatePlinkPath = Join-Path (Split-Path $SCRIPT_DIR -Parent) "远程容器更新\plink.exe"
if (Test-Path $remoteUpdatePlinkPath) {
$script:PLINK_PATH = $remoteUpdatePlinkPath
Write-Log -Level "INFO" -Message " plink 已找到 (远程容器更新目录): $remoteUpdatePlinkPath"
$hasPasswordTool = $true
$script:PreferredSSHTool = "plink"
}
else {
# 3. 检查系统 PATH 中的 plink
try {
$systemPlink = Get-Command plink -ErrorAction Stop
$script:PLINK_PATH = $systemPlink.Source
Write-Log -Level "INFO" -Message " plink 已找到 (系统): $($script:PLINK_PATH)"
$hasPasswordTool = $true
$script:PreferredSSHTool = "plink"
}
catch {
Write-Log -Level "WARN" -Message " plink.exe 未找到"
}
}
}
# 如果没有 plink,检查 sshpass
if (-not $hasPasswordTool) {
try {
$sshpassPath = Get-Command sshpass -ErrorAction Stop
Write-Log -Level "INFO" -Message " sshpass 已找到: $($sshpassPath.Source)"
$hasPasswordTool = $true
$script:PreferredSSHTool = "sshpass"
}
catch {
Write-Log -Level "WARN" -Message " sshpass 未找到"
}
}
if (-not $hasPasswordTool) {
Write-Host ""
Write-Log -Level "ERROR" -Message " 未检测到密码认证工具"
Write-Host ""
Write-Log -Level "ERROR" -Message " 请按以下方式解决:"
Write-Host ""
Write-Log -Level "ERROR" -Message " 方式1 (推荐,离线可用):"
Write-Log -Level "ERROR" -Message " 将 plink.exe 放在脚本同目录下"
Write-Log -Level "ERROR" -Message " 当前脚本目录: $SCRIPT_DIR"
Write-Host ""
Write-Log -Level "ERROR" -Message " 下载地址:"
Write-Log -Level "ERROR" -Message " plink.exe: https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe"
Write-Host ""
return $false
}
Write-Log -Level "INFO" -Message "系统依赖检查通过 (使用 $script:PreferredSSHTool 进行密码认证)"
return $true
}
# ================================
# SSH 执行远程命令
# ================================
function Invoke-SSHCommand {
param(
[string]$HostName,
[string]$User,
[string]$Pass,
[int]$Port = 22,
[string]$Command
)
if ($script:PLINK_PATH -and (Test-Path $script:PLINK_PATH)) {
$plinkArgs = @(
"-ssh",
"-P", $Port,
"-l", $User,
"-pw", $Pass,
"-batch",
$HostName,
$Command
)
$result = & $script:PLINK_PATH @plinkArgs 2>&1
$exitCode = $LASTEXITCODE
# 处理首次连接主机密钥问题
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Cannot confirm")) {
$cmdLine = "echo y | `"$($script:PLINK_PATH)`" -ssh -P $Port -l $User -pw `"$Pass`" $HostName `"$Command`""
$result = cmd /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
}
elseif ($script:PreferredSSHTool -eq "sshpass") {
$env:SSHPASS = $Pass
$result = & sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=$SSH_TIMEOUT -p $Port "$User@$HostName" $Command 2>&1
$exitCode = $LASTEXITCODE
$env:SSHPASS = $null
}
else {
Write-Log -Level "ERROR" -Message "未找到 plink 或 sshpass,无法执行 SSH 命令"
return @{
Output = "ERROR: No password authentication tool available"
ExitCode = 1
}
}
return @{
Output = $result
ExitCode = $exitCode
}
}
# ================================
# 选择服务器
# ================================
function Select-Server {
Write-Log -Level "INFO" -Message "可选择的目标服务器:"
Write-Host ""
foreach ($key in ($ServerList.Keys | Sort-Object)) {
$server = $ServerList[$key]
Write-Host " [$key] $($server.Desc) ($($server.IP) $($server.User))"
}
Write-Host " [0] 手动输入服务器信息"
Write-Host ""
$serverKey = Read-Host "请输入服务器编号"
if ($serverKey -eq "0") {
Write-Log -Level "INFO" -Message "进入手动输入模式"
Write-Host ""
$remoteHost = Read-Host "请输入目标服务器 IP 地址"
if ([string]::IsNullOrEmpty($remoteHost)) {
Write-Log -Level "ERROR" -Message "服务器 IP 地址不能为空"
return $null
}
$sshPortInput = Read-Host "请输入 SSH 端口号 [默认 22]"
if ([string]::IsNullOrEmpty($sshPortInput)) {
$sshPort = 22
} else {
$sshPort = [int]$sshPortInput
}
$remoteUserInput = Read-Host "请输入登录用户名 [默认 root]"
if ([string]::IsNullOrEmpty($remoteUserInput)) {
$remoteUser = "root"
} else {
$remoteUser = $remoteUserInput
}
$remotePassSecure = Read-Host "请输入登录密码" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($remotePassSecure)
$remotePass = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
if ([string]::IsNullOrEmpty($remotePass)) {
Write-Log -Level "ERROR" -Message "登录密码不能为空"
return $null
}
Write-Log -Level "INFO" -Message "已配置目标服务器: ${remoteUser}@${remoteHost}:${sshPort}"
return @{
IP = $remoteHost
User = $remoteUser
Pass = $remotePass
Port = $sshPort
Desc = "手动输入服务器"
}
}
elseif ($ServerList.ContainsKey($serverKey)) {
$server = $ServerList[$serverKey].Clone()
$server.Port = 22
Write-Log -Level "INFO" -Message "已选择 $($server.Desc) ($($server.IP))"
return $server
}
else {
Write-Log -Level "ERROR" -Message "编号 $serverKey 不存在,请重新运行脚本"
return $null
}
}
# ================================
# 测试 SSH 连接
# ================================
function Test-SSHConnection {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "测试 SSH 连接..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "echo CONNECTION_OK"
if (($result.ExitCode -ne 0) -or ($result.Output -notmatch "CONNECTION_OK")) {
Write-Log -Level "ERROR" -Message "SSH 连接失败!"
Write-Log -Level "ERROR" -Message "输出信息: $($result.Output)"
Write-Log -Level "ERROR" -Message "请检查: 1) IP地址是否正确 2) 端口是否正确 3) 密码是否正确 4) 网络是否可达"
return $false
}
Write-Log -Level "SUCCESS" -Message "SSH 连接测试通过"
return $true
}
# ================================
# 检测平台类型
# ================================
function Get-PlatformType {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "自动检测目标服务器平台类型..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "[ -d /data/services ] && echo 'NEW_PLATFORM' || echo 'OLD_PLATFORM'"
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '\S' }
$platformCheck = ($outputLines | Select-Object -Last 1).Trim()
if ($platformCheck -eq "NEW_PLATFORM") {
Write-Log -Level "SUCCESS" -Message "检测到 /data/services 目录存在,识别为【新统一平台】"
return "new"
}
else {
Write-Log -Level "SUCCESS" -Message "未检测到 /data/services 目录,识别为【传统平台】"
return "old"
}
}
# ================================
# 检测系统类型(容器类型)
# ================================
function Get-SystemType {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "自动检测系统类型(容器)..."
# 检测运行中的容器
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker ps --format '{{.Names}}' 2>/dev/null || echo 'DOCKER_ERROR'"
if ($result.Output -match "DOCKER_ERROR" -or $result.ExitCode -ne 0) {
Write-Log -Level "WARN" -Message "Docker 命令执行失败,可能未安装 Docker"
return @{
HasUjava = $false
HasUpython = $false
HasUpythonVoice = $false
Containers = @()
}
}
$containers = $result.Output -split "`n" | Where-Object { $_ -match '\S' } | ForEach-Object { $_.Trim() }
$systemInfo = @{
HasUjava = $false
HasUpython = $false
HasUpythonVoice = $false
Containers = $containers
UjavaContainer = $null
UpythonContainer = $null
UpythonVoiceContainer = $null
}
foreach ($container in $containers) {
if ($container -match '^ujava\d*$') {
$systemInfo.HasUjava = $true
$systemInfo.UjavaContainer = $container
Write-Log -Level "INFO" -Message " 检测到 ujava 容器: $container -> 会议预定系统"
}
elseif ($container -match '^upython\d*$' -and $container -notmatch 'voice') {
$systemInfo.HasUpython = $true
$systemInfo.UpythonContainer = $container
Write-Log -Level "INFO" -Message " 检测到 upython 容器: $container -> 运维集控系统"
}
elseif ($container -match '^upython_voice\d*$') {
$systemInfo.HasUpythonVoice = $true
$systemInfo.UpythonVoiceContainer = $container
Write-Log -Level "INFO" -Message " 检测到 upython_voice 容器: $container -> 转录系统"
}
}
if (-not $systemInfo.HasUjava -and -not $systemInfo.HasUpython -and -not $systemInfo.HasUpythonVoice) {
Write-Log -Level "WARN" -Message " 未检测到任何已知容器类型"
}
return $systemInfo
}
# ================================
# 检测 ujava 服务(支持容器内和宿主机)
# ================================
function Test-UjavaServices {
param(
[hashtable]$Server,
[string]$ContainerName = $null,
[string]$PlatformType = "new"
)
if ($ContainerName) {
Write-Host ""
Write-Log -Level "INFO" -Message "========== 检测 ujava 服务 (容器: $ContainerName) =========="
}
else {
Write-Host ""
Write-Log -Level "INFO" -Message "========== 检测 ujava 服务 (宿主机) =========="
}
$results = @()
foreach ($serviceName in $UjavaServices.Keys) {
$jarFileName = $UjavaServices[$serviceName]
# 构建检测命令
# 对于meeting2.0和meeting3.0,需要特殊处理,因为它们使用相同的jar文件名
$checkCmd = $null
$count = 0
if ($ContainerName) {
# 尝试在容器内检测
if ($serviceName -eq "meeting2.0") {
# meeting2.0 需要匹配路径中包含 java-meeting2.0
$checkCmd = "docker exec $ContainerName ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting2.0' | wc -l"
}
elseif ($serviceName -eq "meeting3.0") {
# meeting3.0 需要匹配路径中包含 java-meeting3.0
$checkCmd = "docker exec $ContainerName ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting3.0' | wc -l"
}
else {
# 其他服务直接匹配jar文件名
$checkCmd = "docker exec $ContainerName ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | wc -l"
}
}
else {
# 在宿主机检测
if ($serviceName -eq "meeting2.0") {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting2.0' | wc -l"
}
elseif ($serviceName -eq "meeting3.0") {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting3.0' | wc -l"
}
else {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | wc -l"
}
}
if ($checkCmd) {
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
try {
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '^\d+$' }
if ($outputLines) {
$count = [int]($outputLines | Select-Object -Last 1).Trim()
}
}
catch {
$count = 0
}
}
# 如果容器内检测失败,尝试在宿主机检测(仅当有容器时)
if ($count -eq 0 -and $ContainerName) {
if ($serviceName -eq "meeting2.0") {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting2.0' | wc -l"
}
elseif ($serviceName -eq "meeting3.0") {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | grep 'java-meeting3.0' | wc -l"
}
else {
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | wc -l"
}
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
try {
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '^\d+$' }
if ($outputLines) {
$count = [int]($outputLines | Select-Object -Last 1).Trim()
}
}
catch {
$count = 0
}
}
$status = if ($count -gt 0) { "运行中" } else { "未运行" }
$statusColor = if ($count -gt 0) { "SUCCESS" } else { "ERROR" }
$results += @{
Service = $serviceName
Pattern = $jarFileName
Status = $status
Running = ($count -gt 0)
}
$statusIcon = if ($count -gt 0) { "[OK]" } else { "[FAIL]" }
$location = if ($ContainerName) { "容器内" } else { "宿主机" }
Write-Log -Level $statusColor -Message " $statusIcon $serviceName ($jarFileName) [$location]: $status"
}
return $results
}
# ================================
# 检测 ujava 宿主机服务(extapi)
# ================================
function Test-UjavaHostServices {
param(
[hashtable]$Server
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 检测 ujava 宿主机服务 (extapi) =========="
$results = @()
foreach ($serviceName in $UjavaHostServices.Keys) {
$jarFileName = $UjavaHostServices[$serviceName]
# 在宿主机检查进程,使用jar文件名匹配(不依赖完整路径)
$checkCmd = "ps aux 2>/dev/null | grep -v grep | grep '$jarFileName' | wc -l"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
$count = 0
try {
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '^\d+$' }
if ($outputLines) {
$count = [int]($outputLines | Select-Object -Last 1).Trim()
}
}
catch {
$count = 0
}
$status = if ($count -gt 0) { "运行中" } else { "未运行" }
$statusColor = if ($count -gt 0) { "SUCCESS" } else { "ERROR" }
$results += @{
Service = $serviceName
Pattern = $jarFileName
Status = $status
Running = ($count -gt 0)
}
$statusIcon = if ($count -gt 0) { "[OK]" } else { "[FAIL]" }
Write-Log -Level $statusColor -Message " $statusIcon $serviceName ($jarFileName): $status"
}
return $results
}
# ================================
# 检测容器内端口服务
# ================================
function Test-ContainerPorts {
param(
[hashtable]$Server,
[string]$ContainerName,
[array]$PortList,
[string]$ServiceType
)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 检测 $ServiceType 容器内服务 ($ContainerName) =========="
$results = @()
# 获取容器内所有监听端口
$checkCmd = "docker exec $ContainerName netstat -tlnp 2>/dev/null || docker exec $ContainerName ss -tlnp 2>/dev/null"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $checkCmd
$netstatOutput = $result.Output
foreach ($portInfo in $PortList) {
$port = $portInfo.Port
$expectedProcess = $portInfo.Process
$description = $portInfo.Description
# 检查端口是否在监听
$portPattern = ":$port\s"
$isListening = $netstatOutput -match $portPattern
$status = if ($isListening) { "监听中" } else { "未监听" }
$statusColor = if ($isListening) { "SUCCESS" } else { "ERROR" }
$results += @{
Port = $port
Process = $expectedProcess
Description = $description
Status = $status
Listening = $isListening
}
$statusIcon = if ($isListening) { "[OK]" } else { "[FAIL]" }
Write-Log -Level $statusColor -Message " $statusIcon 端口 $port ($description): $status"
}
return $results
}
# ================================
# 生成检测报告
# ================================
function Show-HealthReport {
param(
[hashtable]$Server,
[string]$PlatformType,
[hashtable]$SystemInfo,
[array]$UjavaContainerResults,
[array]$UjavaHostResults,
[array]$UpythonResults,
[array]$UpythonVoiceResults
)
Write-Host ""
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host " 服务自检报告" -ForegroundColor Cyan
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host ""
# 基本信息
Write-Host "【基本信息】" -ForegroundColor Yellow
Write-Host " 服务器地址: $($Server.IP)"
Write-Host " 服务器描述: $($Server.Desc)"
Write-Host " 平台类型: $(if ($PlatformType -eq 'new') { '新统一平台' } else { '传统平台' })"
Write-Host " 检测时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host ""
# 系统类型
Write-Host "【系统类型识别】" -ForegroundColor Yellow
if ($SystemInfo.HasUjava) {
Write-Host " 会议预定系统 (ujava): 已部署 [$($SystemInfo.UjavaContainer)]" -ForegroundColor Green
}
if ($SystemInfo.HasUpython) {
Write-Host " 运维集控系统 (upython): 已部署 [$($SystemInfo.UpythonContainer)]" -ForegroundColor Green
}
if ($SystemInfo.HasUpythonVoice) {
Write-Host " 转录系统 (upython_voice): 已部署 [$($SystemInfo.UpythonVoiceContainer)]" -ForegroundColor Green
}
if (-not $SystemInfo.HasUjava -and -not $SystemInfo.HasUpython -and -not $SystemInfo.HasUpythonVoice) {
Write-Host " 未检测到已知系统容器" -ForegroundColor Yellow
}
Write-Host ""
# 统计信息
$totalServices = 0
$runningServices = 0
$failedServices = 0
# ujava 容器服务统计
if ($UjavaContainerResults -and $UjavaContainerResults.Count -gt 0) {
Write-Host "【ujava 容器内服务状态】" -ForegroundColor Yellow
foreach ($r in $UjavaContainerResults) {
$totalServices++
if ($r.Running) {
$runningServices++
Write-Host " [OK] $($r.Service): $($r.Status)" -ForegroundColor Green
}
else {
$failedServices++
Write-Host " [FAIL] $($r.Service): $($r.Status)" -ForegroundColor Red
}
}
Write-Host ""
}
# ujava 宿主机服务统计
if ($UjavaHostResults -and $UjavaHostResults.Count -gt 0) {
Write-Host "【ujava 宿主机服务状态】" -ForegroundColor Yellow
foreach ($r in $UjavaHostResults) {
$totalServices++
if ($r.Running) {
$runningServices++
Write-Host " [OK] $($r.Service): $($r.Status)" -ForegroundColor Green
}
else {
$failedServices++
Write-Host " [FAIL] $($r.Service): $($r.Status)" -ForegroundColor Red
}
}
Write-Host ""
}
# upython 服务统计
if ($UpythonResults -and $UpythonResults.Count -gt 0) {
Write-Host "【upython 容器服务状态】" -ForegroundColor Yellow
foreach ($r in $UpythonResults) {
$totalServices++
if ($r.Listening) {
$runningServices++
Write-Host " [OK] 端口 $($r.Port) ($($r.Description)): $($r.Status)" -ForegroundColor Green
}
else {
$failedServices++
Write-Host " [FAIL] 端口 $($r.Port) ($($r.Description)): $($r.Status)" -ForegroundColor Red
}
}
Write-Host ""
}
# upython_voice 服务统计
if ($UpythonVoiceResults -and $UpythonVoiceResults.Count -gt 0) {
Write-Host "【upython_voice 容器服务状态】" -ForegroundColor Yellow
foreach ($r in $UpythonVoiceResults) {
$totalServices++
if ($r.Listening) {
$runningServices++
Write-Host " [OK] 端口 $($r.Port) ($($r.Description)): $($r.Status)" -ForegroundColor Green
}
else {
$failedServices++
Write-Host " [FAIL] 端口 $($r.Port) ($($r.Description)): $($r.Status)" -ForegroundColor Red
}
}
Write-Host ""
}
# 总结
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host "【检测总结】" -ForegroundColor Yellow
Write-Host " 总服务数: $totalServices"
Write-Host " 正常运行: $runningServices" -ForegroundColor Green
Write-Host " 异常服务: $failedServices" -ForegroundColor $(if ($failedServices -gt 0) { "Red" } else { "Green" })
if ($failedServices -eq 0 -and $totalServices -gt 0) {
Write-Host ""
Write-Host " 所有服务运行正常!" -ForegroundColor Green
}
elseif ($failedServices -gt 0) {
Write-Host ""
Write-Host " 存在异常服务,请及时处理!" -ForegroundColor Red
}
elseif ($totalServices -eq 0) {
Write-Host ""
Write-Host " 未检测到任何服务" -ForegroundColor Yellow
}
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Host "日志文件: $LOG_FILE"
Write-Host ""
}
# ================================
# 主函数
# ================================
function Main {
Write-Host ""
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host " 服务自检工具 (Windows 版本)" -ForegroundColor Cyan
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host ""
Write-Log -Level "INFO" -Message "日志文件: $LOG_FILE"
Write-Host ""
# 检查依赖
if (-not (Test-Dependencies)) {
exit 1
}
Write-Host ""
# 选择服务器
$server = Select-Server
if ($null -eq $server) {
exit 2
}
Write-Host ""
# 测试 SSH 连接
if (-not (Test-SSHConnection -Server $server)) {
exit 3
}
Write-Host ""
# 检测平台类型
$platformType = Get-PlatformType -Server $server
Write-Host ""
# 检测系统类型
$systemInfo = Get-SystemType -Server $server
Write-Host ""
# 初始化结果变量
$ujavaContainerResults = @()
$ujavaHostResults = @()
$upythonResults = @()
$upythonVoiceResults = @()
# 根据检测到的系统类型进行服务检测
if ($platformType -eq "new") {
# 新统一平台检测
# ujava 检测
if ($systemInfo.HasUjava) {
# 先尝试在容器内检测
$ujavaContainerResults = Test-UjavaServices -Server $server -ContainerName $systemInfo.UjavaContainer -PlatformType $platformType
# 检测宿主机服务(extapi)
$ujavaHostResults = Test-UjavaHostServices -Server $server
}
else {
# 如果没有检测到ujava容器,尝试在宿主机检测(可能是传统部署方式)
Write-Log -Level "WARN" -Message "未检测到 ujava 容器,尝试在宿主机检测服务..."
$ujavaContainerResults = Test-UjavaServices -Server $server -ContainerName $null -PlatformType $platformType
$ujavaHostResults = Test-UjavaHostServices -Server $server
}
# upython 检测
if ($systemInfo.HasUpython) {
$upythonResults = Test-ContainerPorts -Server $server -ContainerName $systemInfo.UpythonContainer -PortList $UpythonPorts -ServiceType "upython"
}
# upython_voice 检测
if ($systemInfo.HasUpythonVoice) {
$upythonVoiceResults = Test-ContainerPorts -Server $server -ContainerName $systemInfo.UpythonVoiceContainer -PortList $UpythonVoicePorts -ServiceType "upython_voice"
}
}
else {
# 传统平台检测:直接在宿主机检测服务
Write-Log -Level "INFO" -Message "传统平台:在宿主机检测服务"
# ujava 服务检测(传统平台在宿主机运行)
$ujavaContainerResults = Test-UjavaServices -Server $server -ContainerName $null -PlatformType $platformType
$ujavaHostResults = Test-UjavaHostServices -Server $server
# upython 检测(如果有容器)
if ($systemInfo.HasUpython) {
$upythonResults = Test-ContainerPorts -Server $server -ContainerName $systemInfo.UpythonContainer -PortList $UpythonPorts -ServiceType "upython"
}
# upython_voice 检测(如果有容器)
if ($systemInfo.HasUpythonVoice) {
$upythonVoiceResults = Test-ContainerPorts -Server $server -ContainerName $systemInfo.UpythonVoiceContainer -PortList $UpythonVoicePorts -ServiceType "upython_voice"
}
}
# 生成检测报告
Show-HealthReport -Server $server -PlatformType $platformType -SystemInfo $systemInfo `
-UjavaContainerResults $ujavaContainerResults `
-UjavaHostResults $ujavaHostResults `
-UpythonResults $upythonResults `
-UpythonVoiceResults $upythonVoiceResults
}
# 执行主函数
Main
#Requires -Version 5.1
<#
.SYNOPSIS
远程容器更新脚本 (Windows 原生版本)
.DESCRIPTION
本脚本用于从 Windows 电脑远程升级 Linux 服务器上的容器版本。
功能特性:
1. 支持预设服务器列表和手动输入(IP/端口/用户名/密码)
2. 支持自定义 SSH 端口(默认 22)
3. 校验目标服务器是否为 x86 架构
4. 自动传输镜像文件和部署脚本
5. 自动停止远端旧容器
6. 自动检测目标服务器平台类型(检测 /data/services 目录)
7. 自动校验远端容器镜像版本是否已更新
8. 同步 EMQX 配置、数据、日志目录
9. 同步 Nginx 配置、HTML、证书
10. 自动递增容器编号
11. 调用远端部署脚本
12. 部署完成后自动清理远端镜像包和部署脚本
.PARAMETER RemoteDir
在目标服务器上存放镜像与部署脚本的目录,默认 /home/containerUpdate
.EXAMPLE
.\remote_update_win.ps1
.EXAMPLE
.\remote_update_win.ps1 -RemoteDir "/data/temp"
.NOTES
作者: 自动化运维团队
版本: 1.2.0
============================================================
依赖说明(零安装,开箱即用):
============================================================
本脚本需要 plink.exe 和 pscp.exe(PuTTY 工具)来实现自动密码认证。
原因:Windows 原生 SSH 不支持通过命令行传递密码
使用方式(任选其一):
方式1 (推荐,离线可用):
将 plink.exe 和 pscp.exe 放在脚本同目录下
脚本会自动检测并使用本地的可执行文件
下载地址(可在有网络的电脑下载后拷贝):
https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe
https://the.earth.li/~sgtatham/putty/latest/w64/pscp.exe
方式2: 已安装 PuTTY 到系统 PATH
脚本会自动检测系统中已安装的 plink/pscp
============================================================
#>
param(
[string]$RemoteDir = "/home/containerUpdate"
)
# 严格模式
Set-StrictMode -Version Latest
$ErrorActionPreference = "Continue"
# ================================
# 设置控制台编码为 UTF-8
# ================================
# 解决 Windows PowerShell 显示 Linux 服务器返回的中文乱码问题
# Windows 默认使用 GBK (代码页 936),Linux 使用 UTF-8
# 设置输入输出编码为 UTF-8,确保中文正确显示
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8"
# 设置 PowerShell 默认编码为 UTF-8
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'
$PSDefaultParameterValues['*:Encoding'] = 'utf8'
# ================================
# 全局配置
# ================================
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$REMOTE_ARCH_ALLOW_REGEX = '^(x86_64|amd64|i386|i686)$'
$SSH_TIMEOUT = 30
# PuTTY 工具路径(优先使用脚本同目录下的可执行文件)
# 这样可以实现离线使用,无需安装任何依赖
$script:PLINK_PATH = $null
$script:PSCP_PATH = $null
# ================================
# 日志审计配置
# ================================
# 日志文件存放在脚本同目录下的 logs 文件夹中
# 文件名格式: remote_update_YYYYMMDD_HHMMSS.log
$LOG_DIR = Join-Path $SCRIPT_DIR "logs"
$LOG_TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss"
$LOG_FILE = Join-Path $LOG_DIR "remote_update_$LOG_TIMESTAMP.log"
# 确保日志目录存在
if (-not (Test-Path $LOG_DIR)) {
New-Item -ItemType Directory -Path $LOG_DIR -Force | Out-Null
}
# 预设服务器列表
$ServerList = @{
"1" = @{
IP = "192.168.5.48"
User = "root"
Pass = "Ubains@123"
Desc = "标准版预定运维服务器"
}
"2" = @{
IP = "192.168.5.67"
User = "root"
Pass = "Ubains@123"
Desc = "阿曼项目预定服务器"
}
"3" = @{
IP = "192.168.5.47"
User = "root"
Pass = "Ubains@1234"
Desc = "标准版预定运维测试发布服务器"
}
}
# 容器与镜像映射配置
$ContainerOptions = @("ujava", "uemqx", "uredis", "upython", "unacos", "unginx")
$ContainerDescMap = @{
"ujava" = "Java 服务容器"
"uemqx" = "EMQX 消息队列容器"
"uredis" = "Redis 缓存容器"
"upython" = "Python 服务容器"
"unacos" = "Nacos 注册中心容器"
"unginx" = "Nginx 反向代理容器"
}
$ContainerImage = @{
"ujava" = "java1.8.0_472.tar.gz"
"uemqx" = "uemqx5.8.4.tar.gz"
"uredis" = "redis8.2.2.tar.gz"
"upython" = "python_v15.tar.gz"
"unacos" = "nacos-server-v2.5.2.tar.gz"
"unginx" = "nginx-1.29.3.tar.gz"
}
# 容器对应的 Docker 镜像名称(用于版本校验)
$ContainerDockerImage = @{
"ujava" = "139.9.60.86:5000/ujava:v6"
"uemqx" = "139.9.60.86:5000/uemqx:v2"
"uredis" = "139.9.60.86:5000/redis:v3"
"upython_new" = "139.9.60.86:5000/upython:v15"
"upython_old" = "139.9.60.86:5000/upython:v14"
"unacos" = "nacos-server:v2.5.2"
"unginx" = "nginx:1.29.3"
}
# 本地目录配置(用于同步)
$LOCAL_EMQX_DIR = "emqx"
$LOCAL_NGINX_DIR = "nginx"
# 部署脚本名称
$DEPLOY_SCRIPT = "container_update.sh"
# ================================
# 日志函数
# ================================
# 说明:
# 统一的日志输出函数,同时输出到控制台和日志文件
# 日志文件用于审计和问题排查
#
# 参数:
# - Level: 日志级别 (INFO/WARN/ERROR)
# - Message: 日志消息内容
#
# 日志文件位置:脚本目录/logs/remote_update_YYYYMMDD_HHMMSS.log
# ================================
function Write-Log {
param(
[Parameter(Mandatory=$true)]
[ValidateSet("INFO", "WARN", "ERROR")]
[string]$Level,
[Parameter(Mandatory=$true)]
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "[$timestamp] [$Level] $Message"
# 输出到控制台(带颜色)
$colorMap = @{
"INFO" = "Green"
"WARN" = "Yellow"
"ERROR" = "Red"
}
$color = $colorMap[$Level]
Write-Host $logLine -ForegroundColor $color
# 同时写入日志文件(用于审计)
try {
$logLine | Out-File -FilePath $LOG_FILE -Append -Encoding utf8
}
catch {
# 日志文件写入失败不影响主流程
}
}
# ================================
# 写入审计日志(记录关键操作)
# ================================
# 说明:
# 记录关键操作信息到日志文件,用于审计追踪
# 包括:操作类型、目标服务器、容器信息、操作结果等
# ================================
function Write-AuditLog {
param(
[string]$Action, # 操作类型:如 DEPLOY_START, DEPLOY_SUCCESS, DEPLOY_FAILED
[string]$ServerIP, # 目标服务器 IP
[string]$Container, # 容器名称
[string]$Details # 详细信息
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$auditLine = "[$timestamp] [AUDIT] Action=$Action | Server=$ServerIP | Container=$Container | Details=$Details"
try {
$auditLine | Out-File -FilePath $LOG_FILE -Append -Encoding utf8
}
catch {
# 日志文件写入失败不影响主流程
}
}
# ================================
# 检查依赖(Windows 内置 SSH)
# ================================
# 说明:
# Windows 原生 SSH 客户端不支持通过命令行参数或管道传递密码
# (SSH 直接从 TTY/终端读取密码,无法通过 stdin 重定向)
# 因此需要使用 plink/pscp (PuTTY 工具) 来实现自动密码认证
#
# 查找顺序:
# 1. 优先查找脚本同目录下的 plink.exe/pscp.exe(支持离线使用)
# 2. 其次查找系统 PATH 中的 plink/pscp(已安装 PuTTY)
# 3. 最后查找 sshpass(Git Bash/WSL/Cygwin 环境)
# ================================
function Test-Dependencies {
Write-Log -Level "INFO" -Message "检查系统依赖..."
# 检查 Windows 版本
$osVersion = [System.Environment]::OSVersion.Version
Write-Log -Level "INFO" -Message " Windows 版本: $($osVersion.Major).$($osVersion.Minor).$($osVersion.Build)"
# ================================
# 查找 plink.exe
# ================================
$hasPasswordTool = $false
# 1. 优先检查脚本同目录下的 plink.exe(离线模式)
$localPlinkPath = Join-Path $SCRIPT_DIR "plink.exe"
if (Test-Path $localPlinkPath) {
$script:PLINK_PATH = $localPlinkPath
Write-Log -Level "INFO" -Message " ✅ plink 已找到 (本地): $localPlinkPath"
$hasPasswordTool = $true
$script:PreferredSSHTool = "plink"
}
else {
# 2. 检查系统 PATH 中的 plink
try {
$systemPlink = Get-Command plink -ErrorAction Stop
$script:PLINK_PATH = $systemPlink.Source
Write-Log -Level "INFO" -Message " ✅ plink 已找到 (系统): $($script:PLINK_PATH)"
$hasPasswordTool = $true
$script:PreferredSSHTool = "plink"
}
catch {
Write-Log -Level "WARN" -Message " ⚠️ plink.exe 未找到"
}
}
# ================================
# 查找 pscp.exe
# ================================
if ($hasPasswordTool) {
# 1. 优先检查脚本同目录下的 pscp.exe
$localPscpPath = Join-Path $SCRIPT_DIR "pscp.exe"
if (Test-Path $localPscpPath) {
$script:PSCP_PATH = $localPscpPath
Write-Log -Level "INFO" -Message " ✅ pscp 已找到 (本地): $localPscpPath"
}
else {
# 2. 检查系统 PATH 中的 pscp
try {
$systemPscp = Get-Command pscp -ErrorAction Stop
$script:PSCP_PATH = $systemPscp.Source
Write-Log -Level "INFO" -Message " ✅ pscp 已找到 (系统): $($script:PSCP_PATH)"
}
catch {
Write-Log -Level "WARN" -Message " ⚠️ pscp.exe 未找到,文件传输可能失败"
}
}
}
# ================================
# 如果没有 plink,检查 sshpass
# ================================
if (-not $hasPasswordTool) {
try {
$sshpassPath = Get-Command sshpass -ErrorAction Stop
Write-Log -Level "INFO" -Message " ✅ sshpass 已找到: $($sshpassPath.Source)"
$hasPasswordTool = $true
$script:PreferredSSHTool = "sshpass"
}
catch {
Write-Log -Level "WARN" -Message " ⚠️ sshpass 未找到"
}
}
# ================================
# 如果没有任何密码认证工具,给出提示
# ================================
if (-not $hasPasswordTool) {
Write-Log -Level "ERROR" -Message ""
Write-Log -Level "ERROR" -Message " ❌ 未检测到密码认证工具"
Write-Log -Level "ERROR" -Message ""
Write-Log -Level "ERROR" -Message " Windows 原生 SSH 不支持自动密码认证,请按以下方式解决:"
Write-Log -Level "ERROR" -Message ""
Write-Log -Level "ERROR" -Message " 方式1 (推荐,离线可用):"
Write-Log -Level "ERROR" -Message " 将 plink.exe 和 pscp.exe 放在脚本同目录下"
Write-Log -Level "ERROR" -Message " 当前脚本目录: $SCRIPT_DIR"
Write-Log -Level "ERROR" -Message ""
Write-Log -Level "ERROR" -Message " 下载地址 (可在有网络的电脑下载后拷贝):"
Write-Log -Level "ERROR" -Message " plink.exe: https://the.earth.li/~sgtatham/putty/latest/w64/plink.exe"
Write-Log -Level "ERROR" -Message " pscp.exe: https://the.earth.li/~sgtatham/putty/latest/w64/pscp.exe"
Write-Log -Level "ERROR" -Message ""
Write-Log -Level "ERROR" -Message " 方式2: 安装 PuTTY 到系统"
Write-Log -Level "ERROR" -Message " 下载 MSI 安装包: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html"
Write-Log -Level "ERROR" -Message ""
return $false
}
Write-Log -Level "INFO" -Message "✅ 系统依赖检查通过 (使用 $script:PreferredSSHTool 进行密码认证)"
return $true
}
# ================================
# SSH 执行远程命令(使用密码)
# ================================
# 说明:
# 此函数用于通过 SSH 在远程服务器上执行命令
# 由于 Windows 原生 SSH 不支持通过 stdin 传递密码(SSH 直接从 TTY 读取),
# 因此必须使用 plink 或 sshpass 来实现自动密码认证
#
# 工具优先级:
# 1. plink (PuTTY) - 使用 $script:PLINK_PATH(可能是本地文件或系统命令)
# 2. sshpass - 通过环境变量 SSHPASS 传递密码
#
# 参数说明:
# - HostName: 目标服务器 IP 或主机名
# - User: SSH 登录用户名
# - Pass: SSH 登录密码
# - Port: SSH 端口,默认 22
# - Command: 要在远程服务器上执行的命令
#
# 返回值:
# 返回一个哈希表,包含:
# - Output: 命令执行的输出内容
# - ExitCode: 命令执行的退出码
#
# 注意:
# plink 首次连接新服务器时需要确认主机密钥,使用 -batch 模式会导致连接失败
# 解决方案:添加 echo y | 来自动确认,或使用 -hostkey 参数指定密钥
# ================================
function Invoke-SSHCommand {
param(
[string]$HostName,
[string]$User,
[string]$Pass,
[int]$Port = 22,
[string]$Command,
[switch]$Interactive
)
# 优先使用 plink(已在 Test-Dependencies 中检测并设置路径)
if ($script:PLINK_PATH -and (Test-Path $script:PLINK_PATH)) {
# 使用 plink 执行 SSH 命令
# 参数说明:
# -ssh: 使用 SSH 协议
# -P: 指定端口号(注意是大写 P)
# -l: 指定用户名
# -pw: 指定密码(plink 特有参数,Windows 原生 SSH 不支持)
# -batch: 禁用交互式提示,适合脚本自动化
#
# 注意:首次连接时 plink 需要确认主机密钥
# 使用 "echo y |" 管道自动回答 "y" 来接受主机密钥
# 这样可以避免 "Cannot confirm a host key in batch mode" 错误
# 先尝试用 batch 模式(如果主机密钥已缓存则会成功)
$plinkArgs = @(
"-ssh",
"-P", $Port,
"-l", $User,
"-pw", $Pass,
"-batch",
$HostName,
$Command
)
$result = & $script:PLINK_PATH @plinkArgs 2>&1
$exitCode = $LASTEXITCODE
# 如果失败且是因为主机密钥问题,则自动接受密钥后重试
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Cannot confirm")) {
Write-Log -Level "INFO" -Message " 首次连接,正在自动接受主机密钥..."
# 使用 cmd /c 和 echo y 来自动接受主机密钥
# plink 会询问 "Store key in cache? (y/n)",我们回答 y
$plinkArgsNoCache = @(
"-ssh",
"-P", $Port,
"-l", $User,
"-pw", $Pass,
$HostName,
$Command
)
# 通过 cmd 管道 echo y 来自动确认主机密钥
$cmdLine = "echo y | `"$($script:PLINK_PATH)`" -ssh -P $Port -l $User -pw `"$Pass`" $HostName `"$Command`""
$result = cmd /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
}
elseif ($script:PreferredSSHTool -eq "sshpass") {
# 使用 sshpass 执行 SSH 命令
# -e 参数表示从环境变量 SSHPASS 读取密码
# StrictHostKeyChecking=no 自动接受新主机密钥
$env:SSHPASS = $Pass
$result = & sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=$SSH_TIMEOUT -p $Port "$User@$HostName" $Command 2>&1
$exitCode = $LASTEXITCODE
$env:SSHPASS = $null
}
else {
# 没有可用的密码认证工具,返回错误
Write-Log -Level "ERROR" -Message "未找到 plink 或 sshpass,无法执行 SSH 命令"
return @{
Output = "ERROR: No password authentication tool available"
ExitCode = 1
}
}
return @{
Output = $result
ExitCode = $exitCode
}
}
# ================================
# SCP 文件传输
# ================================
# 说明:
# 此函数用于通过 SCP 将本地文件传输到远程服务器
# 与 SSH 命令类似,Windows 原生 SCP 不支持自动密码认证
# 因此需要使用 pscp (PuTTY) 或 sshpass
#
# 工具优先级:
# 1. pscp (PuTTY) - 使用 $script:PSCP_PATH(可能是本地文件或系统命令)
# 2. sshpass + scp - 通过环境变量传递密码
#
# 参数说明:
# - LocalPath: 本地文件路径
# - RemoteHost: 远程服务器 IP 或主机名
# - RemoteUser: SSH 登录用户名
# - RemotePass: SSH 登录密码
# - RemotePort: SSH 端口,默认 22
# - RemotePath: 远程目标路径
#
# 返回值:
# 返回一个哈希表,包含:
# - Output: 命令执行的输出内容
# - ExitCode: 命令执行的退出码(0 表示成功)
#
# 注意:
# pscp 首次连接新服务器时需要确认主机密钥
# 通过 echo y | 管道自动确认,避免 batch 模式下的连接失败
# ================================
function Send-SCPFile {
param(
[string]$LocalPath,
[string]$RemoteHost,
[string]$RemoteUser,
[string]$RemotePass,
[int]$RemotePort = 22,
[string]$RemotePath
)
# 优先使用 pscp(已在 Test-Dependencies 中检测并设置路径)
if ($script:PSCP_PATH -and (Test-Path $script:PSCP_PATH)) {
Write-Log -Level "INFO" -Message " 使用 pscp 传输文件..."
# 先尝试用 batch 模式(如果主机密钥已缓存则会成功)
$pscpArgs = @(
"-P", $RemotePort,
"-l", $RemoteUser,
"-pw", $RemotePass,
"-batch",
$LocalPath,
"${RemoteUser}@${RemoteHost}:${RemotePath}"
)
$result = & $script:PSCP_PATH @pscpArgs 2>&1
$exitCode = $LASTEXITCODE
# 如果失败且是因为主机密钥问题,则自动接受密钥后重试
if ($exitCode -ne 0 -and ($result -match "host key" -or $result -match "Cannot confirm")) {
Write-Log -Level "INFO" -Message " 首次连接,正在自动接受主机密钥..."
# 通过 cmd 管道 echo y 来自动确认主机密钥
$cmdLine = "echo y | `"$($script:PSCP_PATH)`" -P $RemotePort -l $RemoteUser -pw `"$RemotePass`" `"$LocalPath`" `"${RemoteUser}@${RemoteHost}:${RemotePath}`""
$result = cmd /c $cmdLine 2>&1
$exitCode = $LASTEXITCODE
}
}
elseif ($script:PreferredSSHTool -eq "sshpass") {
# 使用 sshpass + scp 传输文件
# StrictHostKeyChecking=no 自动接受新主机密钥
$env:SSHPASS = $RemotePass
$result = & sshpass -e scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P $RemotePort $LocalPath "${RemoteUser}@${RemoteHost}:${RemotePath}" 2>&1
$exitCode = $LASTEXITCODE
$env:SSHPASS = $null
}
else {
# 没有可用的密码认证工具,返回错误
Write-Log -Level "ERROR" -Message " ❌ 未检测到 pscp 或 sshpass,无法自动传输文件"
return @{
Output = "ERROR: No password authentication tool available"
ExitCode = 1
}
}
return @{
Output = $result
ExitCode = $exitCode
}
}
# ================================
# SCP 目录传输(递归)
# ================================
# 说明:
# 此函数用于通过 SCP 递归传输整个目录到远程服务器
# 与单文件传输类似,需要 pscp 或 sshpass 来实现自动密码认证
#
# 参数说明:
# - LocalPath: 本地目录路径
# - RemoteHost: 远程服务器 IP 或主机名
# - RemoteUser: SSH 登录用户名
# - RemotePass: SSH 登录密码
# - RemotePort: SSH 端口,默认 22
# - RemotePath: 远程目标路径
#
# 返回值:
# 返回一个哈希表,包含:
# - Output: 命令执行的输出内容
# - ExitCode: 命令执行的退出码(0 表示成功)
# ================================
function Send-SCPDirectory {
param(
[string]$LocalPath,
[string]$RemoteHost,
[string]$RemoteUser,
[string]$RemotePass,
[int]$RemotePort = 22,
[string]$RemotePath
)
# 优先使用 pscp(已在 Test-Dependencies 中检测并设置路径)
if ($script:PSCP_PATH -and (Test-Path $script:PSCP_PATH)) {
# 使用 pscp 递归传输目录
# -r 参数表示递归传输目录
$pscpArgs = @(
"-P", $RemotePort,
"-l", $RemoteUser,
"-pw", $RemotePass,
"-batch",
"-r",
$LocalPath,
"${RemoteUser}@${RemoteHost}:${RemotePath}"
)
Write-Log -Level "INFO" -Message " 使用 pscp 递归传输目录..."
# 使用完整路径调用 pscp(支持本地文件和系统命令)
$result = & $script:PSCP_PATH @pscpArgs 2>&1
$exitCode = $LASTEXITCODE
}
elseif ($script:PreferredSSHTool -eq "sshpass") {
# 使用 sshpass + scp 递归传输目录
$env:SSHPASS = $RemotePass
$result = & sshpass -e scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P $RemotePort -r $LocalPath "${RemoteUser}@${RemoteHost}:${RemotePath}" 2>&1
$exitCode = $LASTEXITCODE
$env:SSHPASS = $null
}
else {
# 没有可用的密码认证工具,返回错误
Write-Log -Level "ERROR" -Message " ❌ 未检测到 pscp 或 sshpass,无法自动传输目录"
return @{
Output = "ERROR: No password authentication tool available"
ExitCode = 1
}
}
return @{
Output = $result
ExitCode = $exitCode
}
}
# ================================
# 选择容器
# ================================
function Select-Container {
Write-Log -Level "INFO" -Message "可选择的容器类型:"
Write-Host ""
for ($i = 0; $i -lt $ContainerOptions.Count; $i++) {
$idx = $i + 1
$containerName = $ContainerOptions[$i]
$containerDesc = $ContainerDescMap[$containerName]
Write-Host " [$idx] $containerName - $containerDesc"
}
Write-Host ""
$containerKey = Read-Host "请输入容器编号 (1-6)"
if ($containerKey -notmatch '^[1-6]$') {
Write-Log -Level "ERROR" -Message "容器编号无效,请输入 1-6"
return $null
}
$selectedIndex = [int]$containerKey - 1
$selectedContainer = $ContainerOptions[$selectedIndex]
Write-Log -Level "INFO" -Message "已选择容器: $selectedContainer ($($ContainerDescMap[$selectedContainer]))"
return $selectedContainer
}
# ================================
# 选择服务器
# ================================
function Select-Server {
Write-Log -Level "INFO" -Message "可选择的目标服务器:"
Write-Host ""
foreach ($key in ($ServerList.Keys | Sort-Object)) {
$server = $ServerList[$key]
Write-Host " [$key] $($server.Desc) ($($server.IP) $($server.User))"
}
Write-Host " [0] 手动输入服务器信息"
Write-Host ""
$serverKey = Read-Host "请输入服务器编号"
if ($serverKey -eq "0") {
Write-Log -Level "INFO" -Message "进入手动输入模式"
Write-Host ""
$remoteHost = Read-Host "请输入目标服务器 IP 地址"
if ([string]::IsNullOrEmpty($remoteHost)) {
Write-Log -Level "ERROR" -Message "服务器 IP 地址不能为空"
return $null
}
$sshPortInput = Read-Host "请输入 SSH 端口号 [默认 22]"
if ([string]::IsNullOrEmpty($sshPortInput)) {
$sshPort = 22
} else {
$sshPort = [int]$sshPortInput
}
$remoteUserInput = Read-Host "请输入登录用户名 [默认 root]"
if ([string]::IsNullOrEmpty($remoteUserInput)) {
$remoteUser = "root"
} else {
$remoteUser = $remoteUserInput
}
$remotePassSecure = Read-Host "请输入登录密码" -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($remotePassSecure)
$remotePass = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
if ([string]::IsNullOrEmpty($remotePass)) {
Write-Log -Level "ERROR" -Message "登录密码不能为空"
return $null
}
Write-Log -Level "INFO" -Message "已配置目标服务器: ${remoteUser}@${remoteHost}:${sshPort}"
return @{
IP = $remoteHost
User = $remoteUser
Pass = $remotePass
Port = $sshPort
Desc = "手动输入服务器"
}
}
elseif ($ServerList.ContainsKey($serverKey)) {
$server = $ServerList[$serverKey].Clone()
$server.Port = 22
Write-Log -Level "INFO" -Message "已选择 $($server.Desc) ($($server.IP))"
return $server
}
else {
Write-Log -Level "ERROR" -Message "编号 $serverKey 不存在,请重新运行脚本"
return $null
}
}
# ================================
# 测试 SSH 连接
# ================================
function Test-SSHConnection {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "测试 SSH 连接..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "echo CONNECTION_OK"
if (($result.ExitCode -ne 0) -or ($result.Output -notmatch "CONNECTION_OK")) {
Write-Log -Level "ERROR" -Message "SSH 连接失败!"
Write-Log -Level "ERROR" -Message "输出信息: $($result.Output)"
Write-Log -Level "ERROR" -Message "请检查: 1) IP地址是否正确 2) 端口是否正确 3) 密码是否正确 4) 网络是否可达"
return $false
}
Write-Log -Level "INFO" -Message "✅ SSH 连接测试通过"
return $true
}
# ================================
# 校验远端架构
# ================================
function Test-RemoteArchitecture {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "开始校验远端架构 ($($Server.IP))"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "uname -m"
if ($result.ExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "无法获取远端架构,请检查网络或权限"
Write-Log -Level "ERROR" -Message "SSH 输出: $($result.Output)"
return $null
}
# 处理输出,获取最后一行(架构信息)
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '\S' }
$arch = ($outputLines | Select-Object -Last 1).Trim()
if ($arch -notmatch $REMOTE_ARCH_ALLOW_REGEX) {
Write-Log -Level "ERROR" -Message "远端架构为 $arch,非 x86,终止操作"
return $null
}
Write-Log -Level "INFO" -Message "✅ 远端架构 $arch 校验通过"
return $arch
}
# ================================
# 自动检测平台类型
# ================================
function Get-PlatformType {
param(
[hashtable]$Server
)
Write-Log -Level "INFO" -Message "自动检测目标服务器平台类型..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "[ -d /data/services ] && echo 'NEW_PLATFORM' || echo 'OLD_PLATFORM'"
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '\S' }
$platformCheck = ($outputLines | Select-Object -Last 1).Trim()
if ($platformCheck -eq "NEW_PLATFORM") {
Write-Log -Level "INFO" -Message "✅ 检测到 /data/services 目录存在,自动识别为新统一平台"
return @{
Type = "new"
Flag = "--new-platform"
}
}
elseif ($platformCheck -eq "OLD_PLATFORM") {
Write-Log -Level "INFO" -Message "✅ 未检测到 /data/services 目录,自动识别为传统平台"
return @{
Type = "old"
Flag = ""
}
}
else {
Write-Log -Level "WARN" -Message "⚠️ 自动检测平台类型失败,切换为手动确认模式"
$platformInput = Read-Host "目标服务器是否为新统一平台? (y/n)"
switch ($platformInput.ToLower()) {
"y" {
Write-Log -Level "INFO" -Message "标记为新统一平台"
return @{
Type = "new"
Flag = "--new-platform"
}
}
"n" {
Write-Log -Level "INFO" -Message "标记为传统平台"
return @{
Type = "old"
Flag = ""
}
}
default {
Write-Log -Level "ERROR" -Message "输入无效,请输入 y 或 n"
return $null
}
}
}
}
# ================================
# 校验远端镜像版本
# ================================
function Test-RemoteImageVersion {
param(
[hashtable]$Server,
[string]$ContainerPrefix,
[string]$PlatformType
)
# 根据容器类型和平台类型获取期望的镜像名称
if ($ContainerPrefix -eq "upython") {
if ($PlatformType -eq "new") {
$expectedImage = $ContainerDockerImage["upython_new"]
} else {
$expectedImage = $ContainerDockerImage["upython_old"]
}
} else {
$expectedImage = $ContainerDockerImage[$ContainerPrefix]
}
if ([string]::IsNullOrEmpty($expectedImage)) {
Write-Log -Level "WARN" -Message "未配置容器 $ContainerPrefix 的镜像版本信息,跳过版本校验"
return $true
}
Write-Log -Level "INFO" -Message "检查远端服务器镜像版本..."
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker images --format '{{.Repository}}:{{.Tag}}'"
$remoteImages = $result.Output
$escapedImage = [regex]::Escape($expectedImage)
if ($remoteImages -match $escapedImage) {
Write-Log -Level "WARN" -Message "⚠️ 检测到远端服务器已存在目标镜像: $expectedImage"
Write-Log -Level "WARN" -Message "⚠️ 目标服务器可能已完成更新"
# 检查是否有相关容器在运行
$containerCheckResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker ps --format '{{.Names}}' | grep -E '^${ContainerPrefix}([0-9]+)?$' | head -n1"
$runningContainer = $containerCheckResult.Output.Trim()
if (-not [string]::IsNullOrEmpty($runningContainer)) {
# 获取运行容器使用的镜像
$imageCheckResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker inspect --format '{{.Config.Image}}' '$runningContainer'"
$containerImage = $imageCheckResult.Output.Trim()
if ($containerImage -eq $expectedImage) {
Write-Log -Level "WARN" -Message "⚠️ 检测到容器 $runningContainer 正在使用目标镜像 $expectedImage"
Write-Log -Level "WARN" -Message "⚠️ 目标服务器该容器已是最新版本,无需更新"
$continueInput = Read-Host "是否仍要继续更新? (y/n)"
switch ($continueInput.ToLower()) {
"y" {
Write-Log -Level "INFO" -Message "用户选择继续更新"
return $true
}
"n" {
Write-Log -Level "INFO" -Message "用户选择跳过更新"
return $false
}
default {
Write-Log -Level "ERROR" -Message "输入无效,终止操作"
return $false
}
}
}
}
$continueInput = Read-Host "远端已有目标镜像,是否仍要继续更新? (y/n)"
switch ($continueInput.ToLower()) {
"y" {
Write-Log -Level "INFO" -Message "用户选择继续更新"
return $true
}
"n" {
Write-Log -Level "INFO" -Message "用户选择跳过更新"
return $false
}
default {
Write-Log -Level "ERROR" -Message "输入无效,终止操作"
return $false
}
}
}
else {
Write-Log -Level "INFO" -Message "✅ 远端服务器尚未安装目标镜像 $expectedImage,继续更新流程"
return $true
}
}
# ================================
# 同步 EMQX 目录
# ================================
function Sync-EmqxAssets {
param(
[hashtable]$Server,
[string]$RemoteTargetDir,
[string]$PlatformLabel
)
$localEmqxPath = Join-Path $SCRIPT_DIR $LOCAL_EMQX_DIR
if (-not (Test-Path $localEmqxPath)) {
Write-Log -Level "WARN" -Message "本地 EMQX 目录不存在: $localEmqxPath,跳过同步"
return $true
}
Write-Log -Level "INFO" -Message "准备同步 EMQX 目录 ($localEmqxPath -> $($Server.IP):$RemoteTargetDir)"
# 创建远端目录
$null = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "mkdir -p '$RemoteTargetDir'"
# 备份远端目录
$backupCmd = "set -e; TARGET_DIR='$RemoteTargetDir'; if [ -d `"`$TARGET_DIR`" ] && [ -n `"`$(ls -A `$TARGET_DIR 2>/dev/null)`" ]; then ts=`$(date +%Y%m%d_%H%M%S); backup=`"`${TARGET_DIR}_backup_`${ts}`"; cp -r `"`$TARGET_DIR`" `"`$backup`"; echo `$backup; fi"
$backupResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $backupCmd
$backupPath = ($backupResult.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -Last 1)
if ($backupPath) { $backupPath = $backupPath.Trim() }
if (-not [string]::IsNullOrEmpty($backupPath) -and $backupPath -match "backup") {
Write-Log -Level "INFO" -Message "远端目录已备份到 $backupPath"
}
else {
Write-Log -Level "INFO" -Message "远端目录为空或不存在,跳过备份"
}
# 使用 SCP 同步目录
Write-Log -Level "INFO" -Message "使用 SCP 同步 EMQX 目录"
$scpResult = Send-SCPDirectory -LocalPath $localEmqxPath -RemoteHost $Server.IP -RemoteUser $Server.User -RemotePass $Server.Pass -RemotePort $Server.Port -RemotePath $RemoteTargetDir
if ($scpResult.ExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "EMQX 目录同步失败: $($scpResult.Output)"
return $false
}
# 设置远端配置文件权限
$null = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "if [ -d '$RemoteTargetDir/config' ]; then cd '$RemoteTargetDir/config' && ls *.conf >/dev/null 2>&1 && chmod +x *.conf || true; fi"
Write-Log -Level "INFO" -Message "✅ EMQX 目录同步完成 ($PlatformLabel 平台)"
return $true
}
# ================================
# 同步 Nginx 目录
# ================================
function Sync-NginxAssets {
param(
[hashtable]$Server,
[string]$RemoteTargetDir,
[string]$PlatformLabel
)
$localNginxPath = Join-Path $SCRIPT_DIR $LOCAL_NGINX_DIR
if (-not (Test-Path $localNginxPath)) {
Write-Log -Level "WARN" -Message "本地 Nginx 目录不存在: $localNginxPath,跳过同步"
return $true
}
Write-Log -Level "INFO" -Message "准备同步 Nginx 目录 ($localNginxPath -> $($Server.IP):$RemoteTargetDir)"
# 创建远端目录
$null = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "mkdir -p '$RemoteTargetDir'"
# 备份远端目录
$backupCmd = "set -e; TARGET_DIR='$RemoteTargetDir'; if [ -d `"`$TARGET_DIR`" ] && [ -n `"`$(ls -A `$TARGET_DIR 2>/dev/null)`" ]; then ts=`$(date +%Y%m%d_%H%M%S); backup=`"`${TARGET_DIR}_backup_`${ts}`"; cp -r `"`$TARGET_DIR`" `"`$backup`"; echo `$backup; fi"
$backupResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $backupCmd
$backupPath = ($backupResult.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -Last 1)
if ($backupPath) { $backupPath = $backupPath.Trim() }
if (-not [string]::IsNullOrEmpty($backupPath) -and $backupPath -match "backup") {
Write-Log -Level "INFO" -Message "远端 Nginx 目录已备份到 $backupPath"
}
else {
Write-Log -Level "INFO" -Message "远端 Nginx 目录为空或不存在,跳过备份"
}
# 使用 SCP 同步目录
Write-Log -Level "INFO" -Message "使用 SCP 同步 Nginx 目录"
$scpResult = Send-SCPDirectory -LocalPath $localNginxPath -RemoteHost $Server.IP -RemoteUser $Server.User -RemotePass $Server.Pass -RemotePort $Server.Port -RemotePath $RemoteTargetDir
if ($scpResult.ExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "Nginx 目录同步失败: $($scpResult.Output)"
return $false
}
Write-Log -Level "INFO" -Message "✅ Nginx 目录同步完成 ($PlatformLabel 平台)"
return $true
}
# ================================
# 查找并停止远端旧容器
# ================================
function Stop-RemoteContainer {
param(
[hashtable]$Server,
[string]$ContainerPrefix
)
Write-Log -Level "INFO" -Message "查找并停止远端旧容器"
$result = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker ps --format '{{.Names}}' | grep -E '^${ContainerPrefix}([0-9]+)?$' | sort -V | tail -n1"
$outputLines = $result.Output -split "`n" | Where-Object { $_ -match '\S' -and $_ -match "^${ContainerPrefix}" }
$currentContainer = if ($outputLines) { ($outputLines | Select-Object -Last 1).Trim() } else { $null }
if (-not [string]::IsNullOrEmpty($currentContainer)) {
Write-Log -Level "INFO" -Message "检测到旧容器 $currentContainer,执行停止"
$null = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command "docker stop '$currentContainer'"
Write-Log -Level "INFO" -Message "✅ 旧容器 $currentContainer 已停止"
return $currentContainer
}
else {
Write-Log -Level "INFO" -Message "未找到匹配 $ContainerPrefix${ContainerPrefix}[数字] 的运行容器"
return $null
}
}
# ================================
# 确定新容器名称(自动递增)
# ================================
function Get-NewContainerName {
param(
[string]$CurrentContainer,
[string]$ContainerPrefix
)
if (-not [string]::IsNullOrEmpty($CurrentContainer)) {
# 提取容器编号
if ($CurrentContainer -match "${ContainerPrefix}(\d+)$") {
$lastNum = [int]$Matches[1]
}
else {
$lastNum = 0
}
$nextNum = $lastNum + 1
}
else {
$nextNum = 1
}
$newContainer = "${ContainerPrefix}${nextNum}"
Write-Log -Level "INFO" -Message "新容器名称确定为 $newContainer"
return $newContainer
}
# ================================
# 清理远端镜像包和部署脚本
# ================================
function Clear-RemoteFiles {
param(
[hashtable]$Server,
[string]$RemoteImagePath,
[string]$RemoteTargetDir,
[string]$DeployName
)
Write-Log -Level "INFO" -Message "开始清理远端镜像包..."
$cleanupCmd = @"
set -e
# 清理镜像包
if [ -f '$RemoteImagePath' ]; then
rm -f '$RemoteImagePath'
echo '已删除镜像包: $RemoteImagePath'
fi
# 清理部署脚本
if [ -f '$RemoteTargetDir/$DeployName' ]; then
rm -f '$RemoteTargetDir/$DeployName'
echo '已删除部署脚本: $RemoteTargetDir/$DeployName'
fi
# 如果远端目录为空,则删除目录
if [ -d '$RemoteTargetDir' ] && [ -z "`$(ls -A '$RemoteTargetDir')" ]; then
rmdir '$RemoteTargetDir'
echo '已删除空目录: $RemoteTargetDir'
fi
"@
$cleanupResult = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $cleanupCmd
if ($cleanupResult.ExitCode -eq 0) {
Write-Log -Level "INFO" -Message "✅ 远端镜像包清理完成"
$cleanupResult.Output -split "`n" | Where-Object { $_ -match '已删除' } | ForEach-Object {
Write-Log -Level "INFO" -Message " $_"
}
}
else {
Write-Log -Level "WARN" -Message "⚠️ 远端镜像包清理过程中出现警告,但不影响部署结果"
}
}
# ================================
# 主函数
# ================================
function Main {
Write-Host ""
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host " 远程容器更新工具 (Windows 原生版本)" -ForegroundColor Cyan
Write-Host "==================================================================" -ForegroundColor Cyan
Write-Host ""
# 记录脚本启动
Write-AuditLog -Action "SCRIPT_START" -ServerIP "N/A" -Container "N/A" -Details "脚本启动,日志文件: $LOG_FILE"
Write-Log -Level "INFO" -Message "日志文件: $LOG_FILE"
# 检查依赖
if (-not (Test-Dependencies)) {
Write-AuditLog -Action "DEPENDENCY_CHECK_FAILED" -ServerIP "N/A" -Container "N/A" -Details "依赖检查失败"
exit 1
}
Write-Host ""
# 检查部署脚本是否存在
$deployScriptPath = Join-Path $SCRIPT_DIR $DEPLOY_SCRIPT
if (-not (Test-Path $deployScriptPath)) {
Write-Log -Level "ERROR" -Message "部署脚本 $DEPLOY_SCRIPT 不存在"
Write-Log -Level "ERROR" -Message "请确保 $deployScriptPath 文件存在"
exit 3
}
Write-Log -Level "INFO" -Message "✅ 部署脚本已找到: $deployScriptPath"
Write-Host ""
# 选择容器
$containerPrefix = Select-Container
if ($null -eq $containerPrefix) {
exit 9
}
Write-Host ""
# 获取镜像文件
$imageFile = $ContainerImage[$containerPrefix]
if ([string]::IsNullOrEmpty($imageFile)) {
Write-Log -Level "ERROR" -Message "容器 $containerPrefix 未配置镜像映射"
exit 10
}
# 检查镜像文件是否存在
$localImagePath = Join-Path $SCRIPT_DIR $imageFile
$imageName = $imageFile
if (-not (Test-Path $localImagePath)) {
Write-Log -Level "ERROR" -Message "镜像文件 $localImagePath 不存在"
Write-Log -Level "ERROR" -Message "请将镜像文件放置于脚本目录后重试"
exit 2
}
$imageSize = (Get-Item $localImagePath).Length / 1MB
Write-Log -Level "INFO" -Message "✅ 镜像文件: $localImagePath (大小: $([math]::Round($imageSize, 2)) MB)"
Write-Host ""
# 选择服务器
$server = Select-Server
if ($null -eq $server) {
exit 7
}
Write-Host ""
# 测试 SSH 连接
Write-AuditLog -Action "SSH_TEST_START" -ServerIP $server.IP -Container "N/A" -Details "开始测试SSH连接"
if (-not (Test-SSHConnection -Server $server)) {
Write-AuditLog -Action "SSH_TEST_FAILED" -ServerIP $server.IP -Container "N/A" -Details "SSH连接测试失败"
exit 4
}
Write-AuditLog -Action "SSH_TEST_SUCCESS" -ServerIP $server.IP -Container "N/A" -Details "SSH连接测试成功"
Write-Host ""
# 校验远端架构
$arch = Test-RemoteArchitecture -Server $server
if ($null -eq $arch) {
exit 5
}
Write-Host ""
# 自动检测平台类型
$platform = Get-PlatformType -Server $server
if ($null -eq $platform) {
exit 6
}
Write-Host ""
# 检查 Nacos 和 Nginx 平台限制
if (($containerPrefix -eq "unacos" -or $containerPrefix -eq "unginx") -and $platform.Type -eq "old") {
Write-Log -Level "ERROR" -Message "$containerPrefix 仅支持新统一平台,无法在传统平台部署"
exit 20
}
# 校验远端镜像版本
if (-not (Test-RemoteImageVersion -Server $server -ContainerPrefix $containerPrefix -PlatformType $platform.Type)) {
Write-Log -Level "INFO" -Message "用户取消更新,流程结束"
exit 0
}
Write-Host ""
# 创建远端目录
Write-Log -Level "INFO" -Message "创建远端目录 $RemoteDir"
$null = Invoke-SSHCommand -HostName $server.IP -User $server.User -Pass $server.Pass -Port $server.Port -Command "mkdir -p '$RemoteDir'"
# 传输镜像与部署脚本到远端目录
Write-Log -Level "INFO" -Message "传输镜像与部署脚本到远端目录"
# 传输镜像文件
Write-Log -Level "INFO" -Message "正在传输镜像文件 (可能需要几分钟)..."
Write-AuditLog -Action "IMAGE_TRANSFER_START" -ServerIP $server.IP -Container $containerPrefix -Details "开始传输镜像文件: $imageName"
$scpImageResult = Send-SCPFile -LocalPath $localImagePath -RemoteHost $server.IP -RemoteUser $server.User -RemotePass $server.Pass -RemotePort $server.Port -RemotePath "$RemoteDir/"
if ($scpImageResult.ExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "镜像文件传输失败: $($scpImageResult.Output)"
Write-AuditLog -Action "IMAGE_TRANSFER_FAILED" -ServerIP $server.IP -Container $containerPrefix -Details "镜像传输失败: $($scpImageResult.Output)"
exit 11
}
Write-Log -Level "INFO" -Message "✅ 镜像文件传输完成"
Write-AuditLog -Action "IMAGE_TRANSFER_SUCCESS" -ServerIP $server.IP -Container $containerPrefix -Details "镜像传输成功: $imageName"
# 传输部署脚本
$scpScriptResult = Send-SCPFile -LocalPath $deployScriptPath -RemoteHost $server.IP -RemoteUser $server.User -RemotePass $server.Pass -RemotePort $server.Port -RemotePath "$RemoteDir/"
if ($scpScriptResult.ExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "部署脚本传输失败: $($scpScriptResult.Output)"
exit 12
}
Write-Log -Level "INFO" -Message "✅ 部署脚本传输完成"
Write-Host ""
# 查找并停止远端旧容器
$currentContainer = Stop-RemoteContainer -Server $server -ContainerPrefix $containerPrefix
Write-Host ""
# 根据容器类型同步相关文件
switch ($containerPrefix) {
"uemqx" {
if ($platform.Type -eq "new") {
$remoteEmqxDir = "/data/middleware/emqx"
} else {
$remoteEmqxDir = "/var/www/emqx"
}
$null = Sync-EmqxAssets -Server $server -RemoteTargetDir $remoteEmqxDir -PlatformLabel $platform.Type
Write-Host ""
}
"upython" {
Write-Log -Level "INFO" -Message "Python 容器暂不执行文件同步,仅部署容器"
}
"unginx" {
if ($platform.Type -eq "new") {
$remoteNginxDir = "/data/middleware/nginx"
$null = Sync-NginxAssets -Server $server -RemoteTargetDir $remoteNginxDir -PlatformLabel $platform.Type
Write-Host ""
}
else {
Write-Log -Level "ERROR" -Message "Nginx 容器仅支持新统一平台,请重新选择"
exit 20
}
}
}
# 确定新容器名称
$newContainer = Get-NewContainerName -CurrentContainer $currentContainer -ContainerPrefix $containerPrefix
# 确定远端镜像路径
$remoteImagePath = "$RemoteDir/$imageName"
# 构建远端执行命令
if (-not [string]::IsNullOrEmpty($platform.Flag)) {
$platformTail = " '$($platform.Flag)'"
} else {
$platformTail = ""
}
$remoteCmd = "set -euo pipefail; cd '$RemoteDir'; sed -i 's/\r$//' '$DEPLOY_SCRIPT' 2>/dev/null || true; chmod +x '$DEPLOY_SCRIPT'; ./'$DEPLOY_SCRIPT' '$newContainer' '$remoteImagePath'$platformTail"
# 执行远端部署脚本
Write-Host ""
Write-Log -Level "INFO" -Message "开始执行远端部署脚本"
Write-AuditLog -Action "DEPLOY_START" -ServerIP $server.IP -Container $newContainer -Details "开始部署容器,平台类型: $($platform.Type)"
Write-Host "==================================================================" -ForegroundColor Cyan
# 使用已检测到的 plink 路径(支持本地文件和系统命令)
if ($script:PLINK_PATH -and (Test-Path $script:PLINK_PATH)) {
# 使用 plink 执行远程命令(带 TTY)
# -t 参数请求伪终端,用于显示部署脚本的实时输出
$plinkArgs = @(
"-ssh",
"-P", $server.Port,
"-l", $server.User,
"-pw", $server.Pass,
"-t",
$server.IP,
$remoteCmd
)
# 使用完整路径调用 plink
& $script:PLINK_PATH @plinkArgs
$deployExitCode = $LASTEXITCODE
}
else {
# 回退到 Invoke-SSHCommand(使用 sshpass)
$result = Invoke-SSHCommand -HostName $server.IP -User $server.User -Pass $server.Pass -Port $server.Port -Command $remoteCmd
Write-Host $result.Output
$deployExitCode = $result.ExitCode
}
Write-Host "==================================================================" -ForegroundColor Cyan
if ($deployExitCode -ne 0) {
Write-Log -Level "ERROR" -Message "远端部署执行失败"
Write-AuditLog -Action "DEPLOY_FAILED" -ServerIP $server.IP -Container $newContainer -Details "部署失败,退出码: $deployExitCode"
exit 13
}
Write-Log -Level "INFO" -Message "✅ 远端部署执行完成"
Write-AuditLog -Action "DEPLOY_SUCCESS" -ServerIP $server.IP -Container $newContainer -Details "部署成功"
Write-Host ""
# 清理远端镜像包和部署脚本
Clear-RemoteFiles -Server $server -RemoteImagePath $remoteImagePath -RemoteTargetDir $RemoteDir -DeployName $DEPLOY_SCRIPT
# 记录部署完成摘要
$summaryDetails = "容器=$newContainer, 镜像=$imageName, 平台=$($platform.Type)"
Write-AuditLog -Action "SCRIPT_END" -ServerIP $server.IP -Container $newContainer -Details "更新流程完成: $summaryDetails"
Write-Host ""
Write-Host "==================================================================" -ForegroundColor Green
Write-Host " ✅ 远程容器更新流程结束!" -ForegroundColor Green
Write-Host "==================================================================" -ForegroundColor Green
Write-Host ""
Write-Host "更新摘要:" -ForegroundColor Cyan
Write-Host " 目标服务器: $($server.IP) ($($server.Desc))"
Write-Host " 容器类型: $containerPrefix ($($ContainerDescMap[$containerPrefix]))"
Write-Host " 新容器名: $newContainer"
Write-Host " 平台类型: $($platform.Type)"
Write-Host " 日志文件: $LOG_FILE"
Write-Host ""
}
# 执行主函数
Main
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论