122 lines
3.9 KiB
Go
122 lines
3.9 KiB
Go
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)
|
|
}
|