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

204 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}