192 lines
4.7 KiB
Go
192 lines
4.7 KiB
Go
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
|
|
}
|