功能:完善云管助手与GPU故障恢复核对

This commit is contained in:
songyiming committed 2026-11-13 19:08:40 +08:00
1 parent b6026f9759
commit 9f7b01e4db
42 files changed
+3576 -120

No files matched your search

@@ -0,0 +1,121 @@
package server
import (
"context"
"database/sql"
"net"
"strings"
"time"
"github.com/gin-gonic/gin"
"vdi-cloud/platform/internal/model"
)
type desktopReadinessCheck struct {
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message"`
}
type desktopReadiness struct {
DesktopID string `json:"desktopId"`
ObservedAt time.Time `json:"observedAt"`
Ready bool `json:"ready"`
PowerState string `json:"powerState"`
ServiceState string `json:"serviceState"`
SetupRequired bool `json:"setupRequired"`
Checks []desktopReadinessCheck `json:"checks"`
}
// checkDesktopReadiness verifies the parts of the provisioning contract that
// the platform can observe without logging into the guest OS. It never starts
// or stops a VM and never changes guest configuration.
func (s *Server) checkDesktopReadiness(c *gin.Context) {
ctx := c.Request.Context()
var desktop model.Desktop
if err := s.DB.GetContext(ctx, &desktop, "SELECT * FROM desktops WHERE id=$1", c.Param("id")); err != nil {
if err == sql.ErrNoRows {
s.fail(c, 404, "desktop_not_found", "桌面不存在")
} else {
s.dbError(c, err)
}
return
}
result := desktopReadiness{
DesktopID: desktop.ID, ObservedAt: time.Now().UTC(), Ready: false,
PowerState: desktop.PowerState, ServiceState: desktop.ServiceState,
SetupRequired: desktop.SetupRequired, Checks: []desktopReadinessCheck{},
}
add := func(name, status, message string) {
result.Checks = append(result.Checks, desktopReadinessCheck{Name: name, Status: status, Message: message})
}
if desktop.Provider != "proxmox" {
add("provider", "fail", "桌面不是 Proxmox 资源,无法执行真实连接探测")
c.JSON(200, result)
return
}
if desktop.Endpoint == "" {
add("endpoint", "fail", "尚未配置桌面 IP 或主机名")
} else {
add("endpoint", "pass", "已配置桌面连接地址")
}
if desktop.SetupRequired {
add("initialization", "fail", "尚未确认系统身份、硬件编码、Sunshine 和终端配对")
} else {
add("initialization", "pass", "已完成平台初始化确认")
}
pve := s.currentPVE()
if pve == nil {
add("power", "unknown", "尚未配置 Proxmox,无法读取真实电源状态")
} else {
power, err := pve.Power(ctx, desktop.Node, desktop.VMID)
if err != nil {
result.PowerState = "unknown"
add("power", "unknown", "无法读取 Proxmox 实际电源状态")
} else {
result.PowerState = power
if power == "running" {
add("power", "pass", "虚拟机正在运行")
} else {
add("power", "fail", "虚拟机未运行,请先开机")
}
}
}
serviceReady := false
if desktop.Endpoint != "" && result.PowerState == "running" {
serviceReady = sunshineReady(ctx, desktop.Endpoint)
if serviceReady {
result.ServiceState = "ready"
add("sunshine", "pass", "Sunshine serverinfo 已响应")
} else {
result.ServiceState = "unknown"
add("sunshine", "fail", "Sunshine 未响应,请检查服务、网络和硬件编码配置")
}
} else {
add("sunshine", "unknown", "虚拟机未运行或尚未配置连接地址,暂不探测 Sunshine")
}
result.Ready = !desktop.SetupRequired && result.PowerState == "running" && serviceReady
if _, err := s.DB.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state=$2,updated_at=now() WHERE id=$3", result.PowerState, result.ServiceState, desktop.ID); err != nil {
s.dbError(c, err)
return
}
c.JSON(200, result)
}
// sunshineReadyAt is split out so the real probe remains fixed to Sunshine's
// serverinfo port while tests can use an isolated local listener.
func sunshineReadyAt(ctx context.Context, endpoint, port string) bool {
host := endpoint
if parsed, _, err := net.SplitHostPort(endpoint); err == nil {
host = parsed
}
if strings.ContainsAny(host, "/\\@?#") || host == "" {
return false
}
return sunshineHTTPProbe(ctx, host, port)
}