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
This commit is contained in:
362
pkg/qemu/profile.go
Normal file
362
pkg/qemu/profile.go
Normal file
@@ -0,0 +1,362 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user