Files
orca/internal/runtime/pve.go
T
Jon Chery 872ffcaf25 feat(P07a/b/c): runtime abstraction — 5 backends (process/podman/wasm/pve-vm/pve-ct), C-01 satisfied (REQ-078)
P07a/b/c — Runtime abstraction interface + 5 implementations.

Runtime interface (internal/runtime/runtime.go, REQ-078):
- Runtime interface { Prepare, Start, Stop, Status }. Alloc struct carries
  Runtime field (changeable on migration per R-004). Registry keyed by
  runtime.one_of. DefaultRegistry(transport) registers all 5.

Process (internal/runtime/process.go):
- ProcessRuntime wraps os/exec (LOCAL testing only; production uses systemd
  emitter). SIGTERM grace 10s then SIGKILL.

Podman (internal/runtime/podman.go):
- PodmanRuntime via sshpush.Transport. podman pull/run/stop/rm/inspect.

Wasm (internal/runtime/wasm.go, gate C-01 SATISFIED):
- WasmRuntime uses wasmtime CLI (apt-installed on peer) via SSH exec. NO CGO
  — does NOT import bytecodealliance/wasmtime-go. CGO_ENABLED=0 build
  passes. D-002 cross-compile story preserved. D-187 recorded.

PVE (internal/runtime/pve.go):
- PveVMRuntime (qm create/start/stop/status) + PveCTRuntime (pct
  create/start/stop/status) via sshpush.Transport. VMID = hash(alloc.ID)%99999.

C-01 evaluation: internal/runtime/C01_WASMTIME_CGO_EVAL.md. Auto-decision
(full autonomy): wasmtime remains primary; CLI-via-SSH avoids CGO entirely.
D-187 in PROJECT.md.

23 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent.
92.7% coverage on internal/runtime.

---ci---
project: orca
phase: P07a/b/c
milestone: v0.9
status: execute
---/ci---
2026-08-05 18:31:25 +00:00

178 lines
5.6 KiB
Go

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 <vmid> --memory <mb> --cores <n> --scsi0 <disk>`
// - Start: `qm start <vmid>`
// - Stop: `qm shutdown <vmid>` (graceful) then `qm stop <vmid>` (force)
// - Status: `qm status <vmid>`
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 <vmid> <template> --memory <mb> --cores <n>`
// - Start: `pct start <vmid>`
// - Stop: `pct shutdown <vmid>` then `pct stop <vmid>`
// - Status: `pct status <vmid>`
type PveCTRuntime struct {
transport *sshpush.Transport
}
// NewPveCTRuntime returns a PveCTRuntime backed by the given transport.
func NewPveCTRuntime(t *sshpush.Transport) *PveCTRuntime {
return &PveCTRuntime{transport: t}
}
// Prepare creates the LXC container.
func (c *PveCTRuntime) Prepare(ctx context.Context, alloc *Alloc) error {
if alloc == nil || alloc.Spec == nil || alloc.Spec.Runtime == nil {
return fmt.Errorf("pve-ct: nil alloc/spec/runtime")
}
vmid := vmidFor(alloc)
template := alloc.Spec.Runtime.Image
if template == "" {
return fmt.Errorf("pve-ct: alloc %s has no template (Runtime.Image)", alloc.ID)
}
mb := 512
cores := 1
cmd := fmt.Sprintf("pct create %d %q --memory %d --cores %d", vmid, template, mb, cores)
if _, err := c.transport.Exec(ctx, alloc.Node, cmd); err != nil {
return fmt.Errorf("pve-ct: create: %w", err)
}
return nil
}
// Start boots the container.
func (c *PveCTRuntime) Start(ctx context.Context, alloc *Alloc) (int, error) {
vmid := vmidFor(alloc)
if _, err := c.transport.Exec(ctx, alloc.Node, fmt.Sprintf("pct start %d", vmid)); err != nil {
return 0, fmt.Errorf("pve-ct: start: %w", err)
}
return vmid, nil
}
// Stop gracefully then force stops the container.
func (c *PveCTRuntime) Stop(ctx context.Context, alloc *Alloc) error {
vmid := vmidFor(alloc)
if _, err := c.transport.Exec(ctx, alloc.Node, fmt.Sprintf("pct shutdown %d", vmid)); err != nil {
// best-effort
}
if _, err := c.transport.Exec(ctx, alloc.Node, fmt.Sprintf("pct stop %d", vmid)); err != nil {
return fmt.Errorf("pve-ct: stop: %w", err)
}
return nil
}
// Status reports the container state from `pct status`.
func (c *PveCTRuntime) Status(ctx context.Context, alloc *Alloc) (State, error) {
vmid := vmidFor(alloc)
out, err := c.transport.Exec(ctx, alloc.Node, fmt.Sprintf("pct status %d", vmid))
if err != nil {
return StateFailed, fmt.Errorf("pve-ct: 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
}
}