Files
volt/pkg/qemu/profile.go
Karl Clinger 81ad0b597c Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI:
- Container runtime (systemd-nspawn)
- VoltVisor VMs (Neutron Stardust / QEMU)
- Stellarium CAS (content-addressed storage)
- ORAS Registry
- GitOps integration
- Landlock LSM security
- Compose orchestration
- Mesh networking

Copyright (c) Armored Gates LLC. All rights reserved.
Licensed under AGPSL v5.0
2026-03-21 00:31:12 -05:00

363 lines
10 KiB
Go

// Package qemu manages QEMU build profiles for the Volt hybrid platform.
//
// Each profile is a purpose-built QEMU compilation stored in Stellarium CAS,
// containing only the binary, shared libraries, and firmware needed for a
// specific use case. This maximizes CAS deduplication across workloads.
//
// Profiles:
// - kvm-linux: Headless Linux KVM (virtio-only, no TCG, no display)
// - kvm-uefi: Windows/UEFI KVM (VNC, USB, TPM, OVMF)
// - emulate-x86: x86 TCG emulation (legacy OS, SCADA, nested)
// - emulate-foreign: Foreign arch TCG (ARM, RISC-V, MIPS, PPC)
package qemu
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
)
// Profile identifies a QEMU build profile.
type Profile string
const (
ProfileKVMLinux Profile = "kvm-linux"
ProfileKVMUEFI Profile = "kvm-uefi"
ProfileEmulateX86 Profile = "emulate-x86"
ProfileEmulateForeign Profile = "emulate-foreign"
)
// ValidProfiles is the set of recognized QEMU build profiles.
var ValidProfiles = []Profile{
ProfileKVMLinux,
ProfileKVMUEFI,
ProfileEmulateX86,
ProfileEmulateForeign,
}
// ProfileManifest describes a CAS-ingested QEMU profile.
// This matches the format produced by `volt cas build`.
type ProfileManifest struct {
Name string `json:"name"`
CreatedAt string `json:"created_at"`
Objects map[string]string `json:"objects"`
// Optional fields from the build manifest (if included as an object)
Profile string `json:"profile,omitempty"`
QEMUVer string `json:"qemu_version,omitempty"`
BuildDate string `json:"build_date,omitempty"`
BuildHost string `json:"build_host,omitempty"`
Arch string `json:"arch,omitempty"`
TotalBytes int64 `json:"total_bytes,omitempty"`
}
// CountFiles returns the number of binaries, libraries, and firmware files.
func (m *ProfileManifest) CountFiles() (binaries, libraries, firmware int) {
for path := range m.Objects {
switch {
case strings.HasPrefix(path, "bin/"):
binaries++
case strings.HasPrefix(path, "lib/"):
libraries++
case strings.HasPrefix(path, "firmware/"):
firmware++
}
}
return
}
// ResolvedProfile contains paths to an assembled QEMU profile ready for use.
type ResolvedProfile struct {
Profile Profile
BinaryPath string // Path to qemu-system-* binary
FirmwareDir string // Path to firmware directory (-L flag)
LibDir string // Path to shared libraries (LD_LIBRARY_PATH)
Arch string // Target architecture (x86_64, aarch64, etc.)
}
// ProfileDir is the base directory for assembled QEMU profiles.
const ProfileDir = "/var/lib/volt/qemu"
// CASRefsDir is where CAS manifests live.
const CASRefsDir = "/var/lib/volt/cas/refs"
// IsValid returns true if the profile is a recognized QEMU build profile.
func (p Profile) IsValid() bool {
for _, v := range ValidProfiles {
if p == v {
return true
}
}
return false
}
// NeedsTCG returns true if the profile uses TCG (software emulation).
func (p Profile) NeedsTCG() bool {
return p == ProfileEmulateX86 || p == ProfileEmulateForeign
}
// NeedsKVM returns true if the profile requires /dev/kvm.
func (p Profile) NeedsKVM() bool {
return p == ProfileKVMLinux || p == ProfileKVMUEFI
}
// DefaultBinaryName returns the expected QEMU binary name for the profile.
func (p Profile) DefaultBinaryName(guestArch string) string {
if guestArch == "" {
guestArch = "x86_64"
}
return fmt.Sprintf("qemu-system-%s", guestArch)
}
// AccelFlag returns the -accel flag value for this profile.
func (p Profile) AccelFlag() string {
if p.NeedsKVM() {
return "kvm"
}
return "tcg"
}
// SelectProfile chooses the best QEMU profile for a workload mode and guest OS.
func SelectProfile(mode string, guestArch string, guestOS string) Profile {
switch {
case mode == "hybrid-emulated":
if guestArch != "" && guestArch != "x86_64" && guestArch != "i386" {
return ProfileEmulateForeign
}
return ProfileEmulateX86
case mode == "hybrid-kvm":
if guestOS == "windows" || guestOS == "uefi" {
return ProfileKVMUEFI
}
return ProfileKVMLinux
default:
// Fallback: if KVM is available, use it; otherwise emulate
if KVMAvailable() {
return ProfileKVMLinux
}
return ProfileEmulateX86
}
}
// KVMAvailable checks if /dev/kvm exists and is accessible.
func KVMAvailable() bool {
info, err := os.Stat("/dev/kvm")
if err != nil {
return false
}
return info.Mode()&os.ModeCharDevice != 0
}
// FindCASRef finds the CAS manifest ref for a QEMU profile.
// Returns the ref path (e.g., "/var/lib/volt/cas/refs/kvm-linux-8e1e73bc.json")
// or empty string if not found.
func FindCASRef(profile Profile) string {
prefix := string(profile) + "-"
entries, err := os.ReadDir(CASRefsDir)
if err != nil {
return ""
}
for _, e := range entries {
if strings.HasPrefix(e.Name(), prefix) && strings.HasSuffix(e.Name(), ".json") {
return filepath.Join(CASRefsDir, e.Name())
}
}
return ""
}
// LoadManifest reads and parses a QEMU profile manifest from CAS.
func LoadManifest(refPath string) (*ProfileManifest, error) {
data, err := os.ReadFile(refPath)
if err != nil {
return nil, fmt.Errorf("read manifest: %w", err)
}
var m ProfileManifest
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("parse manifest: %w", err)
}
return &m, nil
}
// Resolve assembles a QEMU profile from CAS into ProfileDir and returns
// the resolved paths. If already assembled, returns existing paths.
func Resolve(profile Profile, guestArch string) (*ResolvedProfile, error) {
if !profile.IsValid() {
return nil, fmt.Errorf("invalid QEMU profile: %s", profile)
}
if guestArch == "" {
guestArch = "x86_64"
}
profileDir := filepath.Join(ProfileDir, string(profile))
binPath := filepath.Join(profileDir, "bin", profile.DefaultBinaryName(guestArch))
fwDir := filepath.Join(profileDir, "firmware")
libDir := filepath.Join(profileDir, "lib")
// Check if already assembled
if _, err := os.Stat(binPath); err == nil {
return &ResolvedProfile{
Profile: profile,
BinaryPath: binPath,
FirmwareDir: fwDir,
LibDir: libDir,
Arch: guestArch,
}, nil
}
// Find CAS ref
ref := FindCASRef(profile)
if ref == "" {
return nil, fmt.Errorf("QEMU profile %q not found in CAS (run: volt qemu pull %s)", profile, profile)
}
// Assemble from CAS (TinyVol hard-link assembly)
// This reuses the same CAS→TinyVol pipeline as workload rootfs assembly
if err := assembleFromCAS(ref, profileDir); err != nil {
return nil, fmt.Errorf("assemble QEMU profile %s: %w", profile, err)
}
// Verify binary exists after assembly
if _, err := os.Stat(binPath); err != nil {
return nil, fmt.Errorf("QEMU binary not found after assembly: %s", binPath)
}
// Make binary executable
os.Chmod(binPath, 0755)
return &ResolvedProfile{
Profile: profile,
BinaryPath: binPath,
FirmwareDir: fwDir,
LibDir: libDir,
Arch: guestArch,
}, nil
}
// assembleFromCAS reads a CAS manifest and hard-links all objects into targetDir.
func assembleFromCAS(refPath, targetDir string) error {
manifest, err := LoadManifest(refPath)
if err != nil {
return err
}
// Create directory structure
for _, subdir := range []string{"bin", "lib", "firmware"} {
if err := os.MkdirAll(filepath.Join(targetDir, subdir), 0755); err != nil {
return fmt.Errorf("mkdir %s: %w", subdir, err)
}
}
// Hard-link each object from CAS store
casObjectsDir := "/var/lib/volt/cas/objects"
for relPath, hash := range manifest.Objects {
srcObj := filepath.Join(casObjectsDir, hash)
dstPath := filepath.Join(targetDir, relPath)
// Ensure parent dir exists
os.MkdirAll(filepath.Dir(dstPath), 0755)
// Hard-link (or copy if cross-device)
if err := os.Link(srcObj, dstPath); err != nil {
// Fallback to copy if hard link fails (e.g., cross-device)
if err := copyFile(srcObj, dstPath); err != nil {
return fmt.Errorf("link/copy %s → %s: %w", hash[:12], relPath, err)
}
}
}
return nil
}
// copyFile copies src to dst, preserving permissions.
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, data, 0644)
}
// BuildQEMUArgs constructs the QEMU command-line arguments for a workload.
func (r *ResolvedProfile) BuildQEMUArgs(name string, rootfsDir string, memory int, cpus int) []string {
if memory <= 0 {
memory = 256
}
if cpus <= 0 {
cpus = 1
}
args := []string{
"-name", fmt.Sprintf("volt-%s", name),
"-machine", fmt.Sprintf("q35,accel=%s", r.Profile.AccelFlag()),
"-m", fmt.Sprintf("%d", memory),
"-smp", fmt.Sprintf("%d", cpus),
"-nographic",
"-no-reboot",
"-serial", "mon:stdio",
"-net", "none",
"-L", r.FirmwareDir,
}
// CPU model
if r.Profile.NeedsTCG() {
args = append(args, "-cpu", "qemu64")
} else {
args = append(args, "-cpu", "host")
}
// 9p virtio filesystem for rootfs (CAS-assembled)
if rootfsDir != "" {
args = append(args,
"-fsdev", fmt.Sprintf("local,id=rootdev,path=%s,security_model=none,readonly=on", rootfsDir),
"-device", "virtio-9p-pci,fsdev=rootdev,mount_tag=rootfs",
)
}
return args
}
// EnvVars returns environment variables needed to run the QEMU binary
// (primarily LD_LIBRARY_PATH for the profile's shared libraries).
func (r *ResolvedProfile) EnvVars() []string {
return []string{
fmt.Sprintf("LD_LIBRARY_PATH=%s", r.LibDir),
}
}
// SystemdUnitContent generates a systemd service unit for a QEMU workload.
func (r *ResolvedProfile) SystemdUnitContent(name string, rootfsDir string, kernelPath string, memory int, cpus int) string {
qemuArgs := r.BuildQEMUArgs(name, rootfsDir, memory, cpus)
// Add kernel boot if specified
if kernelPath != "" {
qemuArgs = append(qemuArgs,
"-kernel", kernelPath,
"-append", "root=rootfs rootfstype=9p rootflags=trans=virtio,version=9p2000.L console=ttyS0 panic=1",
)
}
argStr := strings.Join(qemuArgs, " \\\n ")
return fmt.Sprintf(`[Unit]
Description=Volt VM: %s (QEMU %s)
After=network.target
[Service]
Type=simple
Environment=LD_LIBRARY_PATH=%s
ExecStart=%s \
%s
KillMode=mixed
TimeoutStopSec=30
Restart=no
[Install]
WantedBy=multi-user.target
`, name, r.Profile, r.LibDir, r.BinaryPath, argStr)
}