package vm import ( "context" "database/sql" "encoding/json" "fmt" "net/http" "forgejo.digital-droplets.de/philschlo/proxui/platform/cluster" "forgejo.digital-droplets.de/philschlo/proxui/platform/jobs" "forgejo.digital-droplets.de/philschlo/proxui/platform/proxmox" "proxui/backend/internal/auth" "proxui/backend/internal/rbac" ) type ClusterRepository interface { GetCluster(ctx context.Context, id string) (cluster.Cluster, bool, error) } type PowerClient interface { PowerVM(ctx context.Context, node string, vmid int, action string) (string, error) } type PowerClientFactory func(cluster.Cluster) (PowerClient, error) type TaskEnqueuer interface { EnqueueProxmoxTaskPoll(ctx context.Context, payload jobs.ProxmoxTaskPollPayload) error } type PowerAuditWriter interface { WriteVMPowerAudit(ctx context.Context, event VMPowerAuditEvent) error } type VMPowerAuditEvent struct { TenantID string ProfileID string Action string VMID string ClusterID string Node string UPID string } type PowerDependencies struct { Clusters ClusterRepository ClientFactory PowerClientFactory Tasks TaskEnqueuer Audit PowerAuditWriter } func (h Handler) StartVM(w http.ResponseWriter, r *http.Request) { h.powerVM(w, r, "start", "running") } func (h Handler) StopVM(w http.ResponseWriter, r *http.Request) { h.powerVM(w, r, "stop", "stopped") } func (h Handler) RebootVM(w http.ResponseWriter, r *http.Request) { h.powerVM(w, r, "reboot", "running") } func (h Handler) powerVM(w http.ResponseWriter, r *http.Request, action string, successStatus string) { if h.power.Clusters == nil || h.power.ClientFactory == nil || h.power.Tasks == nil || h.power.Audit == nil { writeError(w, http.StatusServiceUnavailable, "vm_power_not_configured") return } principal, ok := auth.PrincipalFromRequest(r) if !ok { writeError(w, http.StatusUnauthorized, "unauthorized") return } vmID := r.PathValue("vmID") if vmID == "" { writeError(w, http.StatusBadRequest, "vm_id_required") return } vm, found, err := h.repository.GetVM(r.Context(), principal.Subject, vmID) if err != nil { writeError(w, http.StatusInternalServerError, "vm_get_failed") return } if !found { writeError(w, http.StatusNotFound, "vm_not_found") return } if !rbac.Can(rbac.Role(vm.MembershipRole), rbac.ActionVMPower) { writeError(w, http.StatusForbidden, "forbidden") return } cluster, found, err := h.power.Clusters.GetCluster(r.Context(), vm.ClusterID) if err != nil { writeError(w, http.StatusInternalServerError, "cluster_get_failed") return } if !found { writeError(w, http.StatusBadGateway, "cluster_not_found") return } client, err := h.power.ClientFactory(cluster) if err != nil { writeError(w, http.StatusBadGateway, "proxmox_client_failed") return } upid, err := client.PowerVM(r.Context(), vm.Node, vm.ProxmoxVMID, action) if err != nil { writeError(w, http.StatusBadGateway, "proxmox_power_failed") return } auditAction := "vm.power." + action if err := h.power.Tasks.EnqueueProxmoxTaskPoll(r.Context(), jobs.ProxmoxTaskPollPayload{ ClusterID: vm.ClusterID, Node: vm.Node, UPID: upid, TargetType: "vm", TargetID: vm.ID, TenantID: vm.TenantID, ProfileID: principal.Subject, Action: auditAction, SuccessStatus: successStatus, }); err != nil { writeError(w, http.StatusInternalServerError, "task_enqueue_failed") return } if err := h.power.Audit.WriteVMPowerAudit(r.Context(), VMPowerAuditEvent{ TenantID: vm.TenantID, ProfileID: principal.Subject, Action: auditAction, VMID: vm.ID, ClusterID: vm.ClusterID, Node: vm.Node, UPID: upid, }); err != nil { writeError(w, http.StatusInternalServerError, "audit_write_failed") return } writeJSON(w, http.StatusAccepted, map[string]string{ "upid": upid, "status": "queued", }) } func DefaultPowerClientFactory(cluster cluster.Cluster) (PowerClient, error) { return proxmox.NewClient(cluster) } type SQLPowerAuditWriter struct { db *sql.DB } func NewSQLPowerAuditWriter(db *sql.DB) SQLPowerAuditWriter { return SQLPowerAuditWriter{db: db} } func (w SQLPowerAuditWriter) WriteVMPowerAudit(ctx context.Context, event VMPowerAuditEvent) error { metadata, err := json.Marshal(map[string]any{ "cluster_id": event.ClusterID, "node": event.Node, "upid": event.UPID, }) if err != nil { return err } _, err = w.db.ExecContext(ctx, ` insert into public.audit_log ( tenant_id, profile_id, action, target_type, target_id, metadata ) values ($1, $2, $3, 'vm', $4, $5::jsonb) `, event.TenantID, event.ProfileID, event.Action, event.VMID, string(metadata)) if err != nil { return fmt.Errorf("insert power audit: %w", err) } return nil }