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
363 lines
10 KiB
Go
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)
|
|
}
|