204 lines
7.0 KiB
Go
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()
|
||
}
|