功能:扩展云管资源运维、账号授权及监控管理
This commit is contained in:
1 parent
eef485306f
commit
6134d49c16
87 files changed
+29498
-126
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -25,7 +26,7 @@ func (s *Server) overview(c *gin.Context) {
|
||||
Sessions int `db:"sessions"`
|
||||
Failed int `db:"failed"`
|
||||
}
|
||||
err := s.DB.GetContext(c.Request.Context(), &counts, `SELECT (SELECT count(*) FROM terminals) terminals,(SELECT count(*) FROM terminals WHERE last_seen>now()-interval '90 seconds') online,(SELECT count(*) FROM desktops) desktops,(SELECT count(*) FROM desktops WHERE power_state='running') running,(SELECT count(*) FROM terminals WHERE session_state='connected' AND session_desktop_id IS NOT NULL AND last_seen>now()-interval '90 seconds') sessions,(SELECT count(*) FROM tasks WHERE status IN ('failed','timeout') AND created_at>now()-interval '24 hours') failed`)
|
||||
err := s.DB.GetContext(c.Request.Context(), &counts, `SELECT (SELECT count(*) FROM terminals) terminals,(SELECT count(*) FROM terminals WHERE enabled AND last_seen>now()-interval '90 seconds') online,(SELECT count(*) FROM desktops) desktops,(SELECT count(*) FROM desktops WHERE power_state='running') running,(SELECT count(*) FROM terminals WHERE enabled AND session_state='connected' AND session_desktop_id IS NOT NULL AND last_seen>now()-interval '90 seconds') sessions,(SELECT count(*) FROM tasks WHERE status IN ('failed','timeout') AND created_at>now()-interval '24 hours') failed`)
|
||||
if err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
@@ -33,14 +34,15 @@ func (s *Server) overview(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"terminalCount": counts.Terminals, "onlineTerminals": counts.Online, "desktopCount": counts.Desktops, "runningDesktops": counts.Running, "activeSessions": counts.Sessions, "failedTasks": counts.Failed, "mode": s.Config.Mode})
|
||||
}
|
||||
func (s *Server) settings(c *gin.Context) {
|
||||
pve := s.currentPVE()
|
||||
provider := "unconfigured"
|
||||
if s.Config.Mode == "demo" {
|
||||
provider = "demo"
|
||||
}
|
||||
if s.PVE != nil {
|
||||
if pve != nil {
|
||||
provider = "proxmox"
|
||||
}
|
||||
c.JSON(200, gin.H{"mode": s.Config.Mode, "computeProvider": provider, "pveConfigured": s.PVE != nil, "terminalTransport": "https-poll", "capabilities": []string{"auth", "resources", "desktops", "terminals", "bindings", "tasks", "audit"}})
|
||||
c.JSON(200, gin.H{"mode": s.Config.Mode, "computeProvider": provider, "pveConfigured": pve != nil, "terminalTransport": "https-poll", "capabilities": []string{"terminal-system-metrics", "gpu-inventory", "desktop-batches", "desktop-snapshots", "pve-connection", "desktop-hardware", "login-rate-limit", "csv-imports", "session-history", "monitor-alerts", "administrators", "password", "auth", "resources", "desktops", "templates", "pools", "clone", "desktop-connection", "terminals", "terminal-lifecycle", "terminal-policies", "users", "batch-bindings", "bindings", "tasks", "audit"}})
|
||||
}
|
||||
func (s *Server) resources(c *gin.Context) {
|
||||
items := []model.Resource{}
|
||||
@@ -70,7 +72,7 @@ func (s *Server) desktop(c *gin.Context) {
|
||||
}
|
||||
func (s *Server) terminals(c *gin.Context) {
|
||||
items := []model.Terminal{}
|
||||
err := s.DB.SelectContext(c.Request.Context(), &items, `SELECT id,sn,name,group_name,CASE WHEN last_seen>now()-interval '90 seconds' THEN 'online' WHEN status='unregistered' THEN 'unregistered' ELSE 'offline' END status,agent_version,last_seen,CASE WHEN last_seen>now()-interval '90 seconds' THEN session_desktop_id ELSE NULL END session_desktop_id,session_state,config_version,fps,rtt FROM terminals WHERE (name ILIKE $1 OR sn ILIKE $1) ORDER BY name`, "%"+c.Query("keyword")+"%")
|
||||
err := s.DB.SelectContext(c.Request.Context(), &items, terminalListSQL+` WHERE (name ILIKE $1 OR sn ILIKE $1) ORDER BY name`, "%"+c.Query("keyword")+"%")
|
||||
if err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
@@ -135,6 +137,9 @@ func (s *Server) bindings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if !s.lockAuthorization(c, tx) {
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if err = tx.GetContext(ctx, &user, "SELECT * FROM users WHERE id=$1 FOR UPDATE", c.Param("id")); err != nil {
|
||||
s.dbError(c, err)
|
||||
@@ -171,6 +176,10 @@ func (s *Server) bindings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, "DELETE FROM connection_tickets WHERE consumed_at IS NULL AND (terminal_id=$1 OR terminal_id=$2)", user.TerminalID, in.TerminalID); err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, "INSERT INTO audit_logs VALUES($1,$2,'update_bindings',$3,'success',$4,now())", store.ID("audit_"), s.admin(c).Username, user.ID, c.GetString("requestId"))
|
||||
if err == nil {
|
||||
err = tx.Commit()
|
||||
@@ -211,11 +220,12 @@ func (s *Server) audits(c *gin.Context) {
|
||||
list(c, items)
|
||||
}
|
||||
func (s *Server) discoverVMs(c *gin.Context) {
|
||||
if s.PVE == nil {
|
||||
pve := s.currentPVE()
|
||||
if pve == nil {
|
||||
s.fail(c, 503, "pve_unconfigured", "尚未配置 Proxmox 接入,演示环境不发现真实 VM")
|
||||
return
|
||||
}
|
||||
items, err := s.PVE.Resources(c.Request.Context())
|
||||
items, err := pve.Resources(c.Request.Context())
|
||||
if err != nil {
|
||||
s.fail(c, 502, "pve_unavailable", "Proxmox 查询失败,请检查连接、证书和访问权限")
|
||||
return
|
||||
@@ -229,13 +239,15 @@ func (s *Server) discoverVMs(c *gin.Context) {
|
||||
list(c, vms)
|
||||
}
|
||||
func (s *Server) syncResources(c *gin.Context) {
|
||||
if s.PVE == nil {
|
||||
pve := s.currentPVE()
|
||||
if pve == nil {
|
||||
s.fail(c, 503, "pve_unconfigured", "请通过服务端环境变量配置 Proxmox 接入")
|
||||
return
|
||||
}
|
||||
s.submitTask(c, "resource", "sync", "cluster", "Proxmox 集群")
|
||||
}
|
||||
func (s *Server) importDesktop(c *gin.Context) {
|
||||
pve := s.currentPVE()
|
||||
var in struct {
|
||||
Name string `json:"name" binding:"required,max=80"`
|
||||
OS string `json:"os" binding:"required,oneof=Windows Ubuntu 麒麟"`
|
||||
@@ -249,7 +261,7 @@ func (s *Server) importDesktop(c *gin.Context) {
|
||||
if !s.decode(c, &in) {
|
||||
return
|
||||
}
|
||||
if s.PVE == nil {
|
||||
if pve == nil {
|
||||
s.fail(c, 503, "pve_unconfigured", "真实 VM 纳管需要先配置 Proxmox")
|
||||
return
|
||||
}
|
||||
@@ -261,7 +273,7 @@ func (s *Server) importDesktop(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
power, err := s.PVE.Power(c.Request.Context(), in.Node, in.VMID)
|
||||
power, err := pve.Power(c.Request.Context(), in.Node, in.VMID)
|
||||
if err != nil {
|
||||
s.fail(c, 502, "pve_unavailable", "无法验证所选 VM,请检查节点和 VMID")
|
||||
return
|
||||
@@ -290,6 +302,7 @@ func (s *Server) importDesktop(c *gin.Context) {
|
||||
s.desktop(c)
|
||||
}
|
||||
func (s *Server) desktopAction(c *gin.Context) {
|
||||
pve := s.currentPVE()
|
||||
var in struct {
|
||||
Action string `json:"action" binding:"required,oneof=start shutdown reboot"`
|
||||
}
|
||||
@@ -305,10 +318,19 @@ func (s *Server) desktopAction(c *gin.Context) {
|
||||
s.fail(c, 409, "demo_disabled", "正式模式不执行演示桌面任务")
|
||||
return
|
||||
}
|
||||
if d.Provider == "proxmox" && s.PVE == nil {
|
||||
if d.Provider == "proxmox" && pve == nil {
|
||||
s.fail(c, 503, "pve_unconfigured", "Proxmox 尚未配置")
|
||||
return
|
||||
}
|
||||
unresolved, err := maintenanceBlocked(c.Request.Context(), s.DB, d.ID, "", false)
|
||||
if err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
}
|
||||
if unresolved {
|
||||
s.fail(c, 409, "hardware_unconfirmed", "请先核对结果未确认的桌面维护任务")
|
||||
return
|
||||
}
|
||||
s.submitTask(c, "desktop", in.Action, d.ID, d.Name)
|
||||
}
|
||||
func (s *Server) terminalCommand(c *gin.Context) {
|
||||
@@ -353,6 +375,33 @@ func (s *Server) submitTask(c *gin.Context, kind, action, targetID, targetName s
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if kind == "desktop" {
|
||||
var id string
|
||||
if err = tx.GetContext(ctx, &id, "SELECT id FROM desktops WHERE id=$1 FOR UPDATE", targetID); err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
}
|
||||
unresolved, err := maintenanceBlocked(ctx, tx, targetID, "", false)
|
||||
if err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
}
|
||||
if unresolved {
|
||||
s.fail(c, 409, "hardware_unconfirmed", "请先核对未确认的桌面维护任务")
|
||||
return
|
||||
}
|
||||
}
|
||||
if kind == "terminal" {
|
||||
var enabled bool
|
||||
if err = tx.GetContext(ctx, &enabled, "SELECT enabled FROM terminals WHERE id=$1 FOR UPDATE", targetID); err != nil {
|
||||
s.dbError(c, err)
|
||||
return
|
||||
}
|
||||
if !enabled {
|
||||
s.fail(c, 409, "terminal_disabled", "终端已停用,请重新激活后操作")
|
||||
return
|
||||
}
|
||||
}
|
||||
var task model.Task
|
||||
err = tx.GetContext(ctx, &task, `INSERT INTO tasks(id,kind,action,target_id,target_name,actor,status,message,idempotency_key) VALUES($1,$2,$3,$4,$5,$6,'queued','已提交,等待执行回执',$7) RETURNING *`, store.ID("task_"), kind, action, targetID, targetName, actor, key)
|
||||
if err != nil {
|
||||
@@ -381,6 +430,12 @@ func (s *Server) complete(ctx context.Context, t model.Task, status, message str
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
if err = s.completeTx(ctx, tx, t, status, message); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
func (s *Server) completeTx(ctx context.Context, tx *sqlx.Tx, t model.Task, status, message string) error {
|
||||
result, err := tx.ExecContext(ctx, "UPDATE tasks SET status=$1,message=$2,updated_at=now() WHERE id=$3 AND status IN ('running','queued')", status, message, t.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -395,7 +450,7 @@ func (s *Server) complete(ctx context.Context, t model.Task, status, message str
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit()
|
||||
return nil
|
||||
}
|
||||
func ratio(a, b float64) float64 {
|
||||
if b <= 0 {
|
||||
@@ -404,7 +459,14 @@ func ratio(a, b float64) float64 {
|
||||
return a / b
|
||||
}
|
||||
func (s *Server) sync(ctx context.Context) error {
|
||||
items, err := s.PVE.Resources(ctx)
|
||||
if err := s.RefreshCompute(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
pve := s.currentPVE()
|
||||
if pve == nil {
|
||||
return nil
|
||||
}
|
||||
items, err := pve.Resources(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -421,6 +483,9 @@ func (s *Server) sync(ctx context.Context) error {
|
||||
if _, err = tx.ExecContext(ctx, `INSERT INTO resources VALUES($1,$1,$2,$3,$4,$5,'proxmox',now()) ON CONFLICT(id) DO UPDATE SET status=$2,cpu=$3,memory=$4,storage=$5,provider='proxmox',updated_at=now()`, r.Node, r.Status, r.CPU, ratio(r.Memory, r.MaxMemory), ratio(r.Disk, r.MaxDisk)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = tx.ExecContext(ctx, `INSERT INTO resource_samples(resource_id,cpu,memory,storage) VALUES($1,CASE WHEN $7 THEN $2::double precision ELSE NULL END,CASE WHEN $7 AND $3::double precision>0 THEN $4::double precision ELSE NULL END,CASE WHEN $7 AND $5::double precision>0 THEN $6::double precision ELSE NULL END)`, r.Node, r.CPU, r.MaxMemory, ratio(r.Memory, r.MaxMemory), r.MaxDisk, ratio(r.Disk, r.MaxDisk), r.Status == "online"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if r.Type == "qemu" {
|
||||
if _, err = tx.ExecContext(ctx, "UPDATE desktops SET power_state=$1,updated_at=now() WHERE provider='proxmox' AND node=$2 AND vmid=$3", r.Status, r.Node, r.VMID); err != nil {
|
||||
@@ -432,26 +497,81 @@ func (s *Server) sync(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *Server) RunWorker(ctx context.Context) {
|
||||
// Hold a session lock so only one worker can resume PVE tasks across API replicas.
|
||||
conn, err := s.DB.Connx(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
for {
|
||||
var acquired bool
|
||||
if err = conn.GetContext(ctx, &acquired, "SELECT pg_try_advisory_lock(7651092027)"); err != nil {
|
||||
return
|
||||
}
|
||||
if acquired {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
defer conn.ExecContext(context.Background(), "SELECT pg_advisory_unlock(7651092027)")
|
||||
workerCtx, stop := context.WithCancel(ctx)
|
||||
provisionDone := make(chan struct{}, 1)
|
||||
provisionBusy := false
|
||||
defer func() {
|
||||
stop()
|
||||
if provisionBusy {
|
||||
<-provisionDone
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(300 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-provisionDone:
|
||||
provisionBusy = false
|
||||
case <-ticker.C:
|
||||
if err = conn.PingContext(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
// Unknown outcomes are never automatically retried after a worker restart.
|
||||
var expired []model.Task
|
||||
if s.DB.SelectContext(ctx, &expired, "SELECT * FROM tasks WHERE status IN ('queued','running') AND created_at<now()-interval '2 minutes'") == nil {
|
||||
if s.DB.SelectContext(ctx, &expired, "SELECT * FROM tasks WHERE status IN ('queued','running') AND ((kind NOT IN ('provision','hardware','snapshot','batch-desktop') AND created_at<now()-interval '2 minutes') OR (kind='batch-desktop' AND ((status='queued' AND created_at<now()-interval '2 hours') OR (status='running' AND updated_at<now()-interval '2 minutes'))) OR (kind IN ('provision','hardware','snapshot') AND updated_at<now()-interval '35 minutes'))") == nil {
|
||||
for _, t := range expired {
|
||||
if t.Kind == "batch-desktop" {
|
||||
s.finishBatchPower(t, "", "expired", "")
|
||||
continue
|
||||
}
|
||||
if t.Kind == "snapshot" {
|
||||
s.finishSnapshot(t, "", "expired", "")
|
||||
continue
|
||||
}
|
||||
if t.Kind == "hardware" {
|
||||
s.finishHardware(t, "", "expired", "")
|
||||
continue
|
||||
}
|
||||
if t.Kind == "provision" {
|
||||
_, _ = s.DB.ExecContext(ctx, "UPDATE provisioning_jobs SET state='uncertain' WHERE task_id=$1 AND state='reserved'", t.ID)
|
||||
}
|
||||
_ = s.complete(ctx, t, "timeout", "结果未确认,请核对设备或 PVE 实际状态;不会自动重放")
|
||||
}
|
||||
}
|
||||
var t model.Task
|
||||
err := s.DB.GetContext(ctx, &t, `UPDATE tasks SET status='running',updated_at=now(),message='正在执行,等待最终回执' WHERE id=(SELECT id FROM tasks WHERE status='queued' AND kind!='terminal' ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *`)
|
||||
err := s.DB.GetContext(ctx, &t, `UPDATE tasks SET status='running',updated_at=now(),message='正在执行,等待最终回执' WHERE id=(SELECT id FROM tasks WHERE ((status='queued' AND kind!='terminal') OR (kind='provision' AND status='running' AND provider_ref!='') OR (kind IN ('hardware','snapshot','batch-desktop') AND status='running')) AND ($1 OR kind NOT IN ('provision','hardware','snapshot','batch-desktop')) ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT 1) RETURNING *`, !provisionBusy)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
s.execute(ctx, t)
|
||||
if t.Kind == "provision" || t.Kind == "hardware" || t.Kind == "snapshot" || t.Kind == "batch-desktop" {
|
||||
provisionBusy = true
|
||||
go func(task model.Task) { s.execute(workerCtx, task); provisionDone <- struct{}{} }(t)
|
||||
} else {
|
||||
s.execute(ctx, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,6 +594,27 @@ func (s *Server) RunSync(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
func (s *Server) execute(parent context.Context, t model.Task) {
|
||||
if t.Kind == "batch-desktop" {
|
||||
s.executeBatchPower(parent, t)
|
||||
return
|
||||
}
|
||||
if t.Kind == "snapshot" {
|
||||
s.executeSnapshot(parent, t)
|
||||
return
|
||||
}
|
||||
if t.Kind == "hardware" {
|
||||
s.executeHardware(parent, t)
|
||||
return
|
||||
}
|
||||
if err := s.RefreshCompute(parent); err != nil {
|
||||
_ = s.complete(parent, t, "failed", "计算平台配置无法加载,未提交操作")
|
||||
return
|
||||
}
|
||||
pve := s.currentPVE()
|
||||
if t.Kind == "provision" {
|
||||
s.executeProvision(parent, t)
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(parent, 90*time.Second)
|
||||
defer cancel()
|
||||
var err error
|
||||
@@ -503,11 +644,11 @@ func (s *Server) execute(parent context.Context, t model.Task) {
|
||||
}
|
||||
}
|
||||
} else if err == nil {
|
||||
if s.PVE == nil {
|
||||
if pve == nil {
|
||||
err = errors.New("PVE unconfigured")
|
||||
} else {
|
||||
var upid string
|
||||
upid, err = s.PVE.Action(ctx, d.Node, d.VMID, t.Action)
|
||||
upid, err = pve.Action(ctx, d.Node, d.VMID, t.Action)
|
||||
uncertain = true
|
||||
var httpError *proxmox.HTTPError
|
||||
if errors.As(err, &httpError) && httpError.Status >= 400 && httpError.Status < 500 {
|
||||
@@ -517,11 +658,11 @@ func (s *Server) execute(parent context.Context, t model.Task) {
|
||||
_, err = s.DB.ExecContext(ctx, "UPDATE tasks SET provider_ref=$1 WHERE id=$2", upid, t.ID)
|
||||
}
|
||||
if err == nil {
|
||||
err = s.PVE.Wait(ctx, d.Node, upid, time.Second)
|
||||
err = pve.Wait(ctx, d.Node, upid, time.Second)
|
||||
}
|
||||
if err == nil {
|
||||
var power string
|
||||
power, err = s.PVE.Power(ctx, d.Node, d.VMID)
|
||||
power, err = pve.Power(ctx, d.Node, d.VMID)
|
||||
if err == nil {
|
||||
expected := "running"
|
||||
if t.Action == "shutdown" {
|
||||
|
||||
Reference in new issue
Block a user