// 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) }