Files
vdi-cloud/platform/internal/server/provision_worker.go
T

204 lines
7.0 KiB
Go

package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"time"
"github.com/gin-gonic/gin"
"vdi-cloud/platform/internal/model"
"vdi-cloud/platform/internal/store"
"vdi-cloud/platform/pkg/proxmox"
)
// Reconciliation only polls a known UPID. It never submits another clone request.
func (s *Server) reconcileProvision(c *gin.Context) {
ctx := c.Request.Context()
tx, err := s.DB.BeginTxx(ctx, nil)
if err != nil {
s.dbError(c, err)
return
}
defer tx.Rollback()
var task model.Task
if err = tx.GetContext(ctx, &task, "SELECT * FROM tasks WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
if task.Kind != "provision" || task.Status != "timeout" || task.ProviderRef == "" {
s.fail(c, 409, "reconcile_unavailable", "仅可核对有 PVE 任务标识且结果未确认的克隆;其他情况请在 PVE 人工核查")
return
}
if _, err = tx.ExecContext(ctx, "UPDATE provisioning_jobs SET state='reserved' WHERE task_id=$1 AND state='uncertain'", task.ID); err != nil {
s.dbError(c, err)
return
}
err = tx.GetContext(ctx, &task, "UPDATE tasks SET status='running',message='正在重新核对原 PVE 任务,不会再次克隆',updated_at=now() WHERE id=$1 RETURNING *", task.ID)
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
s.log(c, "reconcile_clone", task.ID, "submitted")
c.JSON(202, task)
}
func (s *Server) validateTemplate(c *gin.Context) {
pve := s.currentPVE()
var template model.ImageTemplate
ctx := c.Request.Context()
if err := s.DB.GetContext(ctx, &template, "SELECT * FROM image_templates WHERE id=$1", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
if pve == nil {
s.fail(c, 503, "pve_unconfigured", "尚未配置 Proxmox")
return
}
config, err := pve.VMConfig(ctx, template.Node, template.VMID)
if err != nil {
s.fail(c, 502, "pve_unavailable", "无法读取模板配置")
return
}
if _, err = proxmox.TemplateDisks(config); err != nil {
s.fail(c, 409, "invalid_template", err.Error())
return
}
digest, _ := config["digest"].(string)
if digest == "" {
s.fail(c, 502, "invalid_template", "模板配置摘要缺失")
return
}
if err = s.DB.GetContext(ctx, &template, "UPDATE image_templates SET digest=$1,validated_at=now() WHERE id=$2 RETURNING *", digest, template.ID); err != nil {
s.dbError(c, err)
return
}
s.log(c, "validate_template", template.ID, "success")
c.JSON(200, template)
}
func (s *Server) executeProvision(parent context.Context, task model.Task) {
if err := s.RefreshCompute(parent); err != nil {
_ = s.complete(parent, task, "failed", "计算平台配置无法加载,未提交操作")
return
}
pve := s.currentPVE()
ctx, cancel := context.WithTimeout(parent, 30*time.Minute)
defer cancel()
var job provisioningJob
err := s.DB.GetContext(ctx, &job, "SELECT * FROM provisioning_jobs WHERE task_id=$1", task.ID)
var in cloneInput
if err == nil {
err = json.Unmarshal([]byte(job.RequestJSON), &in)
}
var template model.ImageTemplate
if err == nil {
err = s.DB.GetContext(ctx, &template, "SELECT * FROM image_templates WHERE id=$1", job.TemplateID)
}
uncertain := task.ProviderRef != ""
if err == nil && pve == nil {
err = errors.New("Proxmox 尚未配置")
}
if err == nil && task.ProviderRef == "" {
_, err = s.clonePreflight(ctx, in)
if err == nil && in.PoolID != nil {
var enabled bool
err = s.DB.GetContext(ctx, &enabled, "SELECT enabled FROM desktop_pools WHERE id=$1", *in.PoolID)
if err == nil && !enabled {
err = errors.New("桌面池已停用")
}
}
if err == nil {
// After a POST could have reached PVE, never submit it again automatically.
uncertain = true
task.ProviderRef, err = pve.Clone(ctx, template.Node, template.VMID, in.VMID, in.Full, in.Storage)
var rejected *proxmox.HTTPError
if errors.As(err, &rejected) && rejected.Status >= 400 && rejected.Status < 500 {
uncertain = false
}
if err == nil {
_, err = s.DB.ExecContext(ctx, "UPDATE tasks SET provider_ref=$1,updated_at=now() WHERE id=$2", task.ProviderRef, task.ID)
}
}
}
if err == nil {
err = pve.Wait(ctx, template.Node, task.ProviderRef, time.Second)
}
var power string
if err == nil {
var config map[string]any
config, err = pve.VMConfig(ctx, template.Node, in.VMID)
if err == nil && (config["name"] != fmt.Sprintf("vdi-%d", in.VMID) || fmt.Sprint(config["template"]) == "1" || config["lock"] != nil) {
err = errors.New("克隆目标配置尚未确认,请在 PVE 核对")
}
if err == nil {
power, err = pve.Power(ctx, template.Node, in.VMID)
}
if err == nil && power != "running" && power != "stopped" {
err = errors.New("目标虚拟机状态未确认")
}
}
if err == nil {
err = s.commitProvision(ctx, task, job, template, in, power)
}
if err == nil {
return
}
log.Printf("provision execution error taskId=%s error=%v", task.ID, err)
final, done := context.WithTimeout(context.Background(), 5*time.Second)
defer done()
state, status, message := "failed", "failed", "创建未执行:"+err.Error()
if uncertain {
state = "uncertain"
status = "timeout"
message = "创建结果未确认,请在 PVE 核对目标 VMID 和任务;不会自动重复克隆"
}
if errors.Is(err, proxmox.ErrTaskFailed) {
status = "failed"
message = "PVE 克隆任务失败,请核对目标 VMID 是否有残留资源;未自动删除或重试"
}
_, _ = s.DB.ExecContext(final, "UPDATE provisioning_jobs SET state=$1 WHERE task_id=$2 AND state='reserved'", state, task.ID)
_ = s.complete(final, task, status, message)
}
func (s *Server) commitProvision(ctx context.Context, task model.Task, job provisioningJob, template model.ImageTemplate, in cloneInput, power string) error {
tx, err := s.DB.BeginTxx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if err = catalogLock(ctx, tx); err != nil {
return err
}
var poolName string
if job.PoolID != nil {
if err = tx.GetContext(ctx, &poolName, "SELECT name FROM desktop_pools WHERE id=$1", *job.PoolID); err != nil {
return err
}
}
_, err = tx.ExecContext(ctx, `INSERT INTO desktops(id,name,os,node,vmid,pool,pool_id,power_state,provider,setup_required) VALUES($1,$2,$3,$4,$5,$6,$7,$8,'proxmox',true)`, job.DesktopID, in.Name, template.OS, template.Node, in.VMID, poolName, job.PoolID, power)
if err != nil {
return err
}
if _, err = tx.ExecContext(ctx, "UPDATE provisioning_jobs SET state='created' WHERE task_id=$1", task.ID); err != nil {
return err
}
result, err := tx.ExecContext(ctx, "UPDATE tasks SET status='success',message='VM 克隆完成并已纳管;尚需配置系统身份、IP、Sunshine 与终端配对',updated_at=now() WHERE id=$1 AND status='running'", task.ID)
if err != nil {
return err
}
n, _ := result.RowsAffected()
if n != 1 {
return errors.New("创建任务状态已变化,保留预留资源等待核对")
}
_, err = tx.ExecContext(ctx, "INSERT INTO audit_logs VALUES($1,$2,'clone',$3,'success',$4,now())", store.ID("audit_"), task.Actor, task.TargetName, task.ID)
if err != nil {
return err
}
return tx.Commit()
}