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

feat(server-health-check): 增强服务器健康检查脚本功能

- 更新脚本版本号从 1.0.1 到 1.0.3
- 为容器信息函数添加 PrintDetails 开关参数
- 扩展容器信息收集功能,增加镜像、端口、IP、挂载点、大小等详细信息
- 修复 PowerShell 中 Go template 变量转义问题
- 重构容器信息显示逻辑,支持详细模式和摘要模式
- 为文件下载功能添加超时控制和更好的错误处理
- 改进 pscp 下载过程,添加临时文件处理和进程管理
- 优化主机密钥接受机制,提高首次连接成功率
- 改进文件下载成功判定逻辑,基于文件存在性和大小判断
- 在健康报告中启用容器详细信息输出模式
上级 1e7638b4
......@@ -64,7 +64,7 @@ $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$SSH_TIMEOUT = 30
# 脚本版本号(用于日志与报告)
$SCRIPT_VERSION = "1.0.1"
$SCRIPT_VERSION = "1.0.3"
# PuTTY 工具路径
$script:PLINK_PATH = $null
......@@ -1597,7 +1597,8 @@ function Repair-ExternalMeetingService {
# ================================
function Test-ContainerInformation {
param(
[hashtable]$Server
[hashtable]$Server,
[switch]$PrintDetails # ✅ 新增:是否在控制台打印完整明细
)
Write-Host ""
......@@ -1648,24 +1649,116 @@ function Test-ContainerInformation {
}
}
# 2. 容器信息导出排版:先运行,再未运行,中间加分隔线(写入日志,最终导出到 md)
Write-Log -Level "INFO" -Message "----- 运行中的容器 -----"
foreach ($c in $runningContainers) {
Write-Log -Level "INFO" -Message (" [RUNNING] {0} ({1}) - {2}" -f $c.Name, $c.Id, $c.Status)
}
Write-Log -Level "INFO" -Message "----------------------------------------"
Write-Log -Level "INFO" -Message "----- 未运行的容器 -----"
foreach ($c in $stoppedContainers) {
Write-Log -Level "INFO" -Message (" [STOPPED] {0} ({1}) - {2}" -f $c.Name, $c.Id, $c.Status)
# 2.1 生成“逐容器明细”(用于报告 Markdown)
$containerDetailsMd = New-Object System.Collections.Generic.List[string]
# 容器总数/运行/停止
$containerDetailsMd.Add(("- 容器总数: {0}" -f ($runningContainers.Count + $stoppedContainers.Count)))
$containerDetailsMd.Add(("- 运行中: {0}" -f $runningContainers.Count))
$containerDetailsMd.Add(("- 已停止: {0}" -f $stoppedContainers.Count))
# 逐个容器补充信息(镜像/端口/IP/挂载/大小等)
$allContainers = @()
$allContainers += $runningContainers
$allContainers += $stoppedContainers
foreach ($c in $allContainers) {
$name = $c.Name
# 镜像
$imgCmd = "docker inspect -f '{{.Config.Image}}' $name 2>/dev/null"
$imgRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $imgCmd
$img = (($imgRes.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -First 1) -as [string])
if ([string]::IsNullOrWhiteSpace($img)) { $img = "-" }
# 状态/健康/重启策略/重启次数
$stateCmd = "docker inspect -f '{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}-{{end}}|{{.HostConfig.RestartPolicy.Name}}|{{.RestartCount}}' $name 2>/dev/null"
$stateRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $stateCmd
$stateLine = (($stateRes.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -First 1) -as [string])
$st = "-"; $health = "-"; $restart = "-"; $restartCount = "0"
if ($stateLine -and $stateLine -match '\|') {
$parts = $stateLine.Trim() -split '\|', 4
if ($parts.Count -ge 1) { $st = $parts[0] }
if ($parts.Count -ge 2) { $health = $parts[1] }
if ($parts.Count -ge 3) { $restart = $parts[2] }
if ($parts.Count -ge 4) { $restartCount = $parts[3] }
}
# IP(可能多个网络,拼成第一个非空)
# ✅ 修复:Go template 的 $k/$v 在 PowerShell 双引号内会被当成变量,必须转义为 `$k/`$v
$ipCmd = "docker inspect -f '{{range `$k,`$v := .NetworkSettings.Networks}}{{if `$v.IPAddress}}{{`$v.IPAddress}} {{end}}{{end}}' $name 2>/dev/null"
$ipRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $ipCmd
$ip = (($ipRes.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -First 1) -as [string])
$ip = ($ip -replace "`r","").Trim()
if ([string]::IsNullOrWhiteSpace($ip)) { $ip = "-" }
# 端口
$portCmd = "docker port $name 2>/dev/null | tr '\n' ',' | sed 's/,$//'"
$portRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $portCmd
$ports = (($portRes.Output -split "`n" | Select-Object -First 1) -as [string])
if ([string]::IsNullOrWhiteSpace($ports)) { $ports = "-" }
# 网络(network:ip)
# ✅ 修复:同上,转义 Go template 的 $k/$v
$netCmd = "docker inspect -f '{{range `$k,`$v := .NetworkSettings.Networks}}{{`$k}}:{{if `$v.IPAddress}}{{`$v.IPAddress}}{{else}}-{{end}} {{end}}' $name 2>/dev/null"
$netRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $netCmd
$nets = (($netRes.Output -split "`n" | Where-Object { $_ -match '\S' } | Select-Object -First 1) -as [string])
$nets = ($nets -replace "`r","").Trim()
if ([string]::IsNullOrWhiteSpace($nets)) { $nets = "-" }
# 挂载
$mntCmd = "docker inspect -f '{{range .Mounts}}{{if .Source}}{{.Source}}{{else}}-{{end}}:{{.Destination}}({{.Mode}}{{if .RW}},rw{{else}},ro{{end}}); {{end}}' $name 2>/dev/null"
$mntRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $mntCmd
$mounts = (($mntRes.Output -split "`n" | Select-Object -First 1) -as [string])
$mounts = ($mounts -replace "`r","").Trim()
if ([string]::IsNullOrWhiteSpace($mounts)) { $mounts = "-" }
# 大小(rw / virtual),注意不同 docker 版本输出差异
$sizeCmd = "docker ps -a --size --filter ""name=^/$name$"" --format ""{{.Size}}"" 2>/dev/null | head -n 1"
$sizeRes = Invoke-SSHCommand -HostName $Server.IP -User $Server.User -Pass $Server.Pass -Port $Server.Port -Command $sizeCmd
$sizeLine = (($sizeRes.Output -split "`n" | Select-Object -First 1) -as [string])
$sizeLine = ($sizeLine -replace "`r","").Trim()
$sizeRw = "-"; $sizeRoot = "-"
if (-not [string]::IsNullOrWhiteSpace($sizeLine)) {
# 例如: "12.3MB (virtual 1.23GB)"
$sizeRw = ($sizeLine -split '\s+\(',2)[0]
if ($sizeLine -match '\(virtual\s+([^)]+)\)') { $sizeRoot = $Matches[1] }
}
$icon = if ($st -eq "running" -or $c.Status -match '^Up') { "✅" } else { "❌" }
$containerDetailsMd.Add(("- {0} 名称: {1} | 镜像: {2} | 状态: {3} | 健康: {4} | 重启: {5}/{6} | IP: {7}" -f $icon, $name, $img, $st, $health, $restart, $restartCount, $ip))
$containerDetailsMd.Add((" - 端口: {0}" -f $ports))
$containerDetailsMd.Add((" - 网络: {0}" -f $nets))
$containerDetailsMd.Add((" - 挂载: {0}" -f $mounts))
$containerDetailsMd.Add((" - 大小: rw={0}, root={1}" -f $sizeRw, $sizeRoot))
$containerDetailsMd.Add("---")
}
# ✅ 2.2 运行时打印(控制台/日志)
Write-Log -Level "INFO" -Message ("[Docker] 容器总数: {0} | 运行中: {1} | 已停止: {2}" -f `
($runningContainers.Count + $stoppedContainers.Count), $runningContainers.Count, $stoppedContainers.Count)
if ($PrintDetails) {
# 打印完整明细(含端口/网络/挂载/大小)
foreach ($line in $containerDetailsMd) {
Write-Log -Level "INFO" -Message ("[Docker] {0}" -f $line)
}
} else {
# 默认只打印每个容器的“摘要第一行”(不刷屏)
foreach ($line in $containerDetailsMd) {
if ($line -match '^- (✅|❌) 名称:') {
Write-Log -Level "INFO" -Message ("[Docker] {0}" -f $line)
}
}
Write-Log -Level "INFO" -Message "[Docker] (提示) 如需打印端口/挂载/大小等完整明细,请在调用处加 -PrintDetails"
}
# 把详细信息作为一条“报告专用记录”塞进 results,供 Show-HealthReport 输出
$results += @{
Check = "容器信息收集"
Status = "完成"
Details = "运行中: $($runningContainers.Count),未运行: $($stoppedContainers.Count)"
Success = $true
Check = "容器详情"
Status = "完成"
Details = ($containerDetailsMd -join "`n")
Success = $true
}
# =====================================================================
......@@ -2808,39 +2901,22 @@ function Show-HealthReport {
Write-Host "【容器信息】未发现容器或容器运行时不可用" -ForegroundColor Yellow
$md += "- 未发现容器或容器运行时不可用"
} else {
# 这里的 ContainerInfo 实际就是 Test-ContainerInformation 返回的 $results,
# 其中只包含一条汇总记录和若干其他检查记录。
# 我们只按 Status 文本做一个简单统计展示。
$total = $ContainerInfo.Count
# 粗略认为 Status 含“运行中”或 “Up” 就算运行;这取决于你写入 Details 的习惯
$runningCount = ($ContainerInfo | Where-Object {
($_ -is [hashtable] -or $_ -is [pscustomobject]) -and $_.Status -match '完成|运行中|Up'
}).Count
$stoppedCount = $total - $runningCount
Write-Host "【容器信息】记录条数: $total" -ForegroundColor Yellow
$md += "- 记录条数: $total"
foreach ($c in $ContainerInfo) {
# 兼容 hashtable / pscustomobject
$name = $null
$status = $null
if ($c -is [hashtable]) {
if ($c.ContainsKey('Check')) { $name = $c['Check'] }
if ($c.ContainsKey('Status')) { $status = $c['Status'] }
# ✅ 优先输出“容器详情”(如果 Test-ContainerInformation 已生成)
$detailItem = $ContainerInfo | Where-Object { $_.Check -eq "容器详情" } | Select-Object -First 1
if ($detailItem -and $detailItem.Details) {
$md += ($detailItem.Details -split "`n")
} else {
$name = $c.Check
$status = $c.Status
# fallback:保持你原来的简单汇总输出
$total = $ContainerInfo.Count
$md += "- 记录条数: $total"
foreach ($c in $ContainerInfo) {
$name = $c.Check
$status = $c.Status
if (-not $name -and -not $status) { continue }
$icon = if ($status -match '完成|正常|运行中|Up') { "✅" } else { "⚠️" }
$md += ("- {0} {1}: {2}" -f $icon, $name, $status)
}
}
if (-not $name -and -not $status) { continue }
$icon = if ($status -match '完成|正常|运行中|Up') { "✅" } else { "⚠️" }
# 这里避免在双引号内出现 $name: 这样的形式,使用 -f 拼接
$line = ("- {0} {1}: {2}" -f $icon, $name, $status)
$md += $line
Write-Host " $line"
}
}
$md += ""
......@@ -3269,7 +3345,8 @@ function Download-RemoteFile {
param(
[Parameter(Mandatory=$true)] [hashtable]$Server,
[Parameter(Mandatory=$true)] [string]$RemotePath,
[Parameter(Mandatory=$true)] [string]$LocalPath
[Parameter(Mandatory=$true)] [string]$LocalPath,
[int]$TimeoutSeconds = 600
)
if (-not $script:PSCP_PATH -or -not (Test-Path $script:PSCP_PATH)) {
......@@ -3278,9 +3355,7 @@ function Download-RemoteFile {
}
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
if (-not (Test-Path $localDir)) { New-Item -ItemType Directory -Path $localDir -Force | Out-Null }
$args = @(
"-scp",
......@@ -3292,26 +3367,114 @@ function Download-RemoteFile {
$LocalPath
)
Write-Log -Level "INFO" -Message ("[DL] pscp 下载: {0} {1}" -f $script:PSCP_PATH, ($args -join ' '))
Write-Log -Level "INFO" -Message ("[DL] pscp 下载(超时 {0}s): {1} {2}" -f $TimeoutSeconds, $script:PSCP_PATH, ($args -join ' '))
$out = & $script:PSCP_PATH @args 2>&1
$code = $LASTEXITCODE
# 用临时文件接 stdout/stderr,避免卡住无输出时看起来“没打印”
$tmpOut = Join-Path $env:TEMP ("pscp_out_{0}.log" -f ([guid]::NewGuid().ToString("N")))
$tmpErr = Join-Path $env:TEMP ("pscp_err_{0}.log" -f ([guid]::NewGuid().ToString("N")))
if ($code -ne 0 -and ($out -match "host key" -or $out -match "Cannot confirm")) {
$cmdLine = "echo y | `"$($script:PSCP_PATH)`" -scp -batch -P $($Server.Port) -l $($Server.User) -pw `"$($Server.Pass)`" `"$($Server.User)@$($Server.IP):$RemotePath`" `"$LocalPath`""
Write-Log -Level "WARN" -Message "[DL] 首次连接,接受主机密钥并重试: $cmdLine"
$out = cmd /c $cmdLine 2>&1
$code = $LASTEXITCODE
}
try {
$p = Start-Process -FilePath $script:PSCP_PATH `
-ArgumentList $args `
-NoNewWindow `
-PassThru `
-RedirectStandardOutput $tmpOut `
-RedirectStandardError $tmpErr
if ($code -eq 0 -and (Test-Path $LocalPath)) {
$size = (Get-Item $LocalPath).Length
Write-Log -Level "SUCCESS" -Message "[DL] 下载成功 ($([math]::Round($size/1024,2)) KB): $LocalPath"
return @{ Success = $true; Size = $size; Output = $out }
} else {
$oneLine = ($out -join " ") -replace '\s+', ' '
Write-Log -Level "ERROR" -Message "[DL] 下载失败 ExitCode=$code, 输出: $oneLine"
return @{ Success = $false; ExitCode = $code; Output = $out }
$ok = $p.WaitForExit($TimeoutSeconds * 1000)
if (-not $ok) {
try { $p.Kill() } catch {}
Write-Log -Level "ERROR" -Message ("[DL] 下载超时,已终止 pscp (>{0}s): {1}" -f $TimeoutSeconds, $RemotePath)
$outTxt = (Get-Content -Path $tmpOut -ErrorAction SilentlyContinue | Out-String).Trim()
$errTxt = (Get-Content -Path $tmpErr -ErrorAction SilentlyContinue | Out-String).Trim()
if ($outTxt) {
$one = ($outTxt -replace '\s+',' ').Trim()
$n = [math]::Min(500, $one.Length)
Write-Log -Level "ERROR" -Message ("[DL] pscp stdout: {0}" -f $one.Substring(0, $n))
}
if ($errTxt) {
$one = ($errTxt -replace '\s+',' ').Trim()
$n = [math]::Min(500, $one.Length)
Write-Log -Level "ERROR" -Message ("[DL] pscp stderr: {0}" -f $one.Substring(0, $n))
}
return @{ Success = $false; ExitCode = -1; Reason = "timeout" }
}
$code = $p.ExitCode
$outTxt = (Get-Content -Path $tmpOut -ErrorAction SilentlyContinue | Out-String)
$errTxt = (Get-Content -Path $tmpErr -ErrorAction SilentlyContinue | Out-String)
$all = (($outTxt + "`n" + $errTxt) -replace "`r","").Trim()
# host key:第一次连接需要 y,pscp 在 -batch 下会失败,这里自动接受并重试一次
if ($code -ne 0 -and $all -match "host key|Cannot confirm") {
$cmdLine = "echo y | `"$($script:PSCP_PATH)`" -scp -batch -P $($Server.Port) -l $($Server.User) -pw `"$($Server.Pass)`" `"$($Server.User)@$($Server.IP):$RemotePath`" `"$LocalPath`""
Write-Log -Level "WARN" -Message "[DL] 首次连接主机密钥提示,自动接受并重试一次"
$all2 = cmd /c $cmdLine 2>&1
$code2 = $LASTEXITCODE
if ($code2 -eq 0 -and (Test-Path $LocalPath)) {
$size = (Get-Item $LocalPath).Length
Write-Log -Level "SUCCESS" -Message "[DL] 下载成功 ($([math]::Round($size/1024,2)) KB): $LocalPath"
return @{ Success = $true; Size = $size; Output = $all2 }
} else {
$oneLine = (($all2 -join " ") -replace '\s+',' ').Trim()
Write-Log -Level "ERROR" -Message "[DL] 下载失败(重试) ExitCode=$code2, 输出: $oneLine"
return @{ Success = $false; ExitCode = $code2; Output = $all2 }
}
}
$code = $null
try { $code = $p.ExitCode } catch { $code = $null }
$outTxt = (Get-Content -Path $tmpOut -ErrorAction SilentlyContinue | Out-String)
$errTxt = (Get-Content -Path $tmpErr -ErrorAction SilentlyContinue | Out-String)
$all = (($outTxt + "`n" + $errTxt) -replace "`r","").Trim()
# ✅ 关键:以“文件是否存在且大小>0”作为成功判定(pscp 会把进度输出到 stderr,不代表失败)
$fileOk = $false
$size = 0
if (Test-Path -LiteralPath $LocalPath) {
try {
$size = (Get-Item -LiteralPath $LocalPath).Length
if ($size -gt 0) { $fileOk = $true }
} catch { $fileOk = $false }
}
# host key:第一次连接需要 y(只有在明确失败且输出包含 host key 时才走)
if (-not $fileOk -and $all -match "host key|Cannot confirm") {
$cmdLine = "echo y | `"$($script:PSCP_PATH)`" -scp -batch -P $($Server.Port) -l $($Server.User) -pw `"$($Server.Pass)`" `"$($Server.User)@$($Server.IP):$RemotePath`" `"$LocalPath`""
Write-Log -Level "WARN" -Message "[DL] 首次连接主机密钥提示,自动接受并重试一次"
$all2 = cmd /c $cmdLine 2>&1
$code2 = $LASTEXITCODE
if ((Test-Path -LiteralPath $LocalPath) -and ((Get-Item -LiteralPath $LocalPath).Length -gt 0)) {
$size2 = (Get-Item -LiteralPath $LocalPath).Length
Write-Log -Level "SUCCESS" -Message "[DL] 下载成功 ($([math]::Round($size2/1024,2)) KB): $LocalPath"
return @{ Success = $true; Size = $size2; Output = $all2 }
} else {
$oneLine = (($all2 -join " ") -replace '\s+',' ').Trim()
Write-Log -Level "ERROR" -Message "[DL] 下载失败(重试) ExitCode=$code2, 输出: $oneLine"
return @{ Success = $false; ExitCode = $code2; Output = $all2 }
}
}
if ($fileOk) {
Write-Log -Level "SUCCESS" -Message "[DL] 下载成功 ($([math]::Round($size/1024,2)) KB): $LocalPath"
return @{ Success = $true; Size = $size; ExitCode = $code; Output = $all }
} else {
$oneLine = ($all -replace '\s+',' ').Trim()
if ($oneLine.Length -gt 800) { $oneLine = $oneLine.Substring(0,800) + "..." }
Write-Log -Level "ERROR" -Message ("[DL] 下载失败 ExitCode={0}, 输出: {1}" -f $code, $oneLine)
return @{ Success = $false; ExitCode = $code; Output = $all }
}
}
finally {
Remove-Item -Path $tmpOut,$tmpErr -Force -ErrorAction SilentlyContinue | Out-Null
}
}
......@@ -3804,7 +3967,7 @@ function Main {
# 容器信息收集(加入到自检报告)
Write-Host ""
Write-Log -Level "INFO" -Message "========== 容器信息(报告) =========="
$containerInfo = Test-ContainerInformation -Server $server
$containerInfo = Test-ContainerInformation -Server $server -PrintDetails
if (-not $containerInfo -or $containerInfo.Count -eq 0) {
}
$md += ""
......
# 代码规范:
## 代码注释:
所有代码都需要标注上注释信息,并且以中文体现。
\ No newline at end of file
所有代码都需要标注上注释信息,并且以中文体现。
## 代码影响:
新版本代码不能影响原有版本实现功能代码。
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论