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

238 lines
7.8 KiB
Go

package server
import (
"context"
"errors"
"github.com/gin-gonic/gin"
"github.com/jmoiron/sqlx"
"time"
"vdi-cloud/platform/internal/model"
"vdi-cloud/platform/pkg/proxmox"
)
type powerBatchJob struct {
Stage string `db:"stage"`
Fingerprint string `db:"fingerprint"`
}
func (s *Server) claimBatchPower(ctx context.Context, t model.Task) (bool, error) {
tx, err := s.DB.BeginTxx(ctx, nil)
if err != nil {
return false, err
}
defer tx.Rollback()
var id string
if err = tx.GetContext(ctx, &id, "SELECT id FROM desktops WHERE id=$1 FOR UPDATE", t.TargetID); err != nil {
return false, err
}
r, err := tx.ExecContext(ctx, `UPDATE power_batch_items SET stage='submitting' WHERE task_id=$1 AND stage='prepared' AND EXISTS(SELECT 1 FROM tasks WHERE id=$1 AND kind='batch-desktop' AND status IN ('queued','running'))`, t.ID)
if err != nil {
return false, err
}
n, _ := r.RowsAffected()
if n != 1 {
return false, nil
}
err = tx.Commit()
return err == nil, err
}
func (s *Server) confirmBatchPower(ctx context.Context, db sqlx.ExtContext, t model.Task) error {
var d model.Desktop
if err := sqlx.GetContext(ctx, db, &d, "SELECT * FROM desktops WHERE id=$1", t.TargetID); err != nil {
return err
}
power := d.PowerState
if d.Provider == "proxmox" {
pve := s.currentPVE()
if pve == nil || t.ProviderRef == "" {
return errors.New("missing provider task")
}
task, err := pve.Task(ctx, d.Node, t.ProviderRef)
if err != nil {
return err
}
if task.Status != "stopped" || task.ExitStatus != "OK" {
return errors.New("original power task not successful")
}
power, err = pve.Power(ctx, d.Node, d.VMID)
if err != nil {
return err
}
} else if d.Provider != "demo" || s.Config.Mode != "demo" {
return errors.New("unsupported provider")
}
expected := "running"
if t.Action == "shutdown" {
expected = "stopped"
}
if power != expected {
return errors.New("actual power differs")
}
return nil
}
func (s *Server) executeBatchPower(parent context.Context, t model.Task) {
ctx, cancel := context.WithTimeout(parent, 90*time.Second)
defer cancel()
var job powerBatchJob
err := s.DB.GetContext(ctx, &job, "SELECT stage,fingerprint FROM power_batch_items WHERE task_id=$1", t.ID)
submitted := job.Stage == "submitting" || job.Stage == "uncertain"
if err == nil {
err = s.RefreshCompute(ctx)
}
var d model.Desktop
if err == nil && !submitted {
var item powerBatchCheckItem
d, item, err = s.powerBatchPreflight(ctx, s.DB, t.TargetID, t.Action, t.ID)
if err == nil && (!item.Available || powerFingerprint(d, item.PowerState) != job.Fingerprint) {
err = errors.New("power preflight changed")
}
if err == nil {
var claimed bool
claimed, err = s.claimBatchPower(ctx, t)
if err == nil && !claimed {
return
}
if err == nil {
submitted = true
if d.Provider == "demo" {
power := "running"
if t.Action == "shutdown" {
power = "stopped"
}
_, err = s.DB.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state='unknown',updated_at=now() WHERE id=$2", power, d.ID)
} else {
t.ProviderRef, err = s.currentPVE().Action(ctx, d.Node, d.VMID, t.Action)
var rejected *proxmox.HTTPError
if errors.As(err, &rejected) && rejected.Status >= 400 && rejected.Status < 500 {
submitted = false
}
if err == nil {
_, err = s.DB.ExecContext(ctx, "UPDATE tasks SET provider_ref=$1,updated_at=now() WHERE id=$2", t.ProviderRef, t.ID)
}
}
}
}
}
if err == nil {
err = s.DB.GetContext(ctx, &d, "SELECT * FROM desktops WHERE id=$1", t.TargetID)
if err == nil && d.Provider == "proxmox" {
if t.ProviderRef == "" {
err = errors.New("missing original UPID")
} else {
err = s.currentPVE().Wait(ctx, d.Node, t.ProviderRef, time.Second)
}
}
}
if err == nil {
err = s.confirmBatchPower(ctx, s.DB, t)
}
status, stage, message := "success", "completed", "电源任务和实际状态已确认;连接服务需另行确认"
if err != nil {
status, stage, message = "failed", "failed", "执行前状态发生变化或 PVE 拒绝请求,未发送有效修改"
if submitted {
status, stage, message = "timeout", "uncertain", "批量电源结果未确认,已阻止后续修改和新连接;只核对原任务,不会自动重发"
}
}
s.finishBatchPower(t, status, stage, message)
}
func (s *Server) finishBatchPower(t model.Task, status, stage, message string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := s.DB.BeginTxx(ctx, nil)
if err != nil {
return
}
defer tx.Rollback()
var id, current, jobStage string
if err = tx.GetContext(ctx, &id, "SELECT id FROM desktops WHERE id=$1 FOR UPDATE", t.TargetID); err != nil {
return
}
if err = tx.GetContext(ctx, &current, "SELECT status FROM tasks WHERE id=$1 FOR UPDATE", t.ID); err != nil || current != "queued" && current != "running" {
return
}
if err = tx.GetContext(ctx, &jobStage, "SELECT stage FROM power_batch_items WHERE task_id=$1 FOR UPDATE", t.ID); err != nil {
return
}
if stage == "expired" {
status, stage, message = "failed", "failed", "批量电源任务提交前过期,未发送修改"
if jobStage != "prepared" {
status, stage, message = "timeout", "uncertain", "原批量电源任务结果未确认,不会自动重发"
}
}
_, err = tx.ExecContext(ctx, "UPDATE power_batch_items SET stage=$1 WHERE task_id=$2", stage, t.ID)
if err == nil && status == "success" {
power := "running"
if t.Action == "shutdown" {
power = "stopped"
}
_, err = tx.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state='unknown',updated_at=now() WHERE id=$2", power, t.TargetID)
}
if err == nil {
err = s.completeTx(ctx, tx, t, status, message)
}
if err == nil {
_ = tx.Commit()
}
}
func (s *Server) reconcileBatchPower(c *gin.Context) {
tx, actor := s.accountTransaction(c, false)
if tx == nil {
return
}
defer tx.Rollback()
if actor.Role != "super_admin" && actor.Role != "operator" {
s.fail(c, 403, "forbidden", "当前账号不能核对电源任务")
return
}
ctx := c.Request.Context()
var t model.Task
if err := tx.GetContext(ctx, &t, "SELECT * FROM tasks WHERE id=$1 AND kind='batch-desktop'", c.Param("id")); err != nil {
s.dbError(c, err)
return
}
var id string
if err := tx.GetContext(ctx, &id, "SELECT id FROM desktops WHERE id=$1 FOR UPDATE", t.TargetID); err != nil {
s.dbError(c, err)
return
}
if err := tx.GetContext(ctx, &t, "SELECT * FROM tasks WHERE id=$1 FOR UPDATE", t.ID); err != nil {
s.dbError(c, err)
return
}
var stage string
if err := tx.GetContext(ctx, &stage, "SELECT stage FROM power_batch_items WHERE task_id=$1 FOR UPDATE", t.ID); err != nil {
s.dbError(c, err)
return
}
if t.Status != "timeout" || stage != "uncertain" {
s.fail(c, 409, "reconcile_unavailable", "仅可核对结果未确认的批量电源任务")
return
}
if err := s.confirmBatchPower(ctx, tx, t); err != nil {
s.fail(c, 409, "power_unconfirmed", "原 PVE 任务和实际电源状态尚未确认;未重新发送修改")
return
}
_, err := tx.ExecContext(ctx, "UPDATE power_batch_items SET stage='completed' WHERE task_id=$1", t.ID)
if err == nil {
_, err = tx.ExecContext(ctx, "UPDATE tasks SET status='success',message='只读核对原任务和实际电源状态通过,未重新发送修改',updated_at=now() WHERE id=$1", t.ID)
}
if err == nil {
power := "running"
if t.Action == "shutdown" {
power = "stopped"
}
_, err = tx.ExecContext(ctx, "UPDATE desktops SET power_state=$1,service_state='unknown',updated_at=now() WHERE id=$2", power, t.TargetID)
}
if err == nil {
err = accountAudit(ctx, tx, actor.Username, "reconcile_batch_power", t.ID, c.GetString("requestId"))
}
if err == nil {
err = tx.Commit()
}
if err != nil {
s.dbError(c, err)
return
}
c.JSON(200, gin.H{"accepted": true})
}