package runtime import ( "context" "fmt" "strings" "git.cloudinit.dev/coreci/orca/internal/sshpush" ) // vmidFor returns a deterministic 5-digit VMID derived from the alloc // ID (hash(alloc.ID) % 99999 + 1). Used by both pveVMRuntime and // pveCTRuntime — VM and container IDs share the same numeric space on // a Proxmox node, but the namespace is sparse (one alloc = one VMID) // so collisions are rare in practice. func vmidFor(alloc *Alloc) int { id := allocIDHash(alloc.ID) if id < 100 { id += 100 } return id } // PveVMRuntime implements Runtime for the "pve-vm" one_of using the // `qm` tool over SSH on a Proxmox peer (extends REQ-076). The transport // is injected. // // Lifecycle: // // - Prepare: `qm create --memory --cores --scsi0 ` // - Start: `qm start ` // - Stop: `qm shutdown ` (graceful) then `qm stop ` (force) // - Status: `qm status ` type PveVMRuntime struct { transport *sshpush.Transport } // NewPveVMRuntime returns a PveVMRuntime backed by the given transport. func NewPveVMRuntime(t *sshpush.Transport) *PveVMRuntime { return &PveVMRuntime{transport: t} } // Prepare creates the VM. Memory/Cores default to 512MB / 1 if the // spec's Runtime block omits them; the disk is the spec's image field // (a Proxmox storage path like local:vmdir/disk.qcow2). func (v *PveVMRuntime) Prepare(ctx context.Context, alloc *Alloc) error { if alloc == nil || alloc.Spec == nil || alloc.Spec.Runtime == nil { return fmt.Errorf("pve-vm: nil alloc/spec/runtime") } vmid := vmidFor(alloc) image := alloc.Spec.Runtime.Image if image == "" { return fmt.Errorf("pve-vm: alloc %s has no disk (Runtime.Image)", alloc.ID) } mb := 512 cores := 1 cmd := fmt.Sprintf("qm create %d --memory %d --cores %d --scsi0 %q", vmid, mb, cores, image) if _, err := v.transport.Exec(ctx, alloc.Node, cmd); err != nil { return fmt.Errorf("pve-vm: create: %w", err) } return nil } // Start boots the VM. func (v *PveVMRuntime) Start(ctx context.Context, alloc *Alloc) (int, error) { vmid := vmidFor(alloc) if _, err := v.transport.Exec(ctx, alloc.Node, fmt.Sprintf("qm start %d", vmid)); err != nil { return 0, fmt.Errorf("pve-vm: start: %w", err) } return vmid, nil } // Stop gracefully shuts down then force-stops the VM. func (v *PveVMRuntime) Stop(ctx context.Context, alloc *Alloc) error { vmid := vmidFor(alloc) if _, err := v.transport.Exec(ctx, alloc.Node, fmt.Sprintf("qm shutdown %d", vmid)); err != nil { // Best-effort graceful; fall through to force stop. } if _, err := v.transport.Exec(ctx, alloc.Node, fmt.Sprintf("qm stop %d", vmid)); err != nil { return fmt.Errorf("pve-vm: stop: %w", err) } return nil } // Status reports the VM state from `qm status`. func (v *PveVMRuntime) Status(ctx context.Context, alloc *Alloc) (State, error) { vmid := vmidFor(alloc) out, err := v.transport.Exec(ctx, alloc.Node, fmt.Sprintf("qm status %d", vmid)) if err != nil { return StateFailed, fmt.Errorf("pve-vm: status: %w", err) } s := strings.ToLower(string(out)) switch { case strings.Contains(s, "running"): return StateRunning, nil case strings.Contains(s, "stopped"): return StateStopped, nil default: return StatePending, nil } } // PveCTRuntime implements Runtime for the "pve-ct" one_of using the // `pct` tool over SSH on a Proxmox peer (extends REQ-076). // // Lifecycle: // // - Prepare: `pct create