872ffcaf25
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---
178 lines
5.6 KiB
Go
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
|
|
}
|
|
}
|