go-devops/container/hypervisor.go
Claude 392ad68047
feat: extract devops packages from core/go
Build system, release automation, SDK generation, Ansible executor,
LinuxKit dev environments, container runtime, deployment, infra
metrics, and developer toolkit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 15:21:39 +00:00

273 lines
7.3 KiB
Go

package container
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// Hypervisor defines the interface for VM hypervisors.
type Hypervisor interface {
// Name returns the name of the hypervisor.
Name() string
// Available checks if the hypervisor is available on the system.
Available() bool
// BuildCommand builds the command to run a VM with the given options.
BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error)
}
// HypervisorOptions contains options for running a VM.
type HypervisorOptions struct {
// Memory in MB.
Memory int
// CPUs count.
CPUs int
// LogFile path for output.
LogFile string
// SSHPort for SSH access.
SSHPort int
// Ports maps host ports to guest ports.
Ports map[int]int
// Volumes maps host paths to guest paths (9p shares).
Volumes map[string]string
// Detach runs in background (nographic mode).
Detach bool
}
// QemuHypervisor implements Hypervisor for QEMU.
type QemuHypervisor struct {
// Binary is the path to the qemu binary (defaults to qemu-system-x86_64).
Binary string
}
// NewQemuHypervisor creates a new QEMU hypervisor instance.
func NewQemuHypervisor() *QemuHypervisor {
return &QemuHypervisor{
Binary: "qemu-system-x86_64",
}
}
// Name returns the hypervisor name.
func (q *QemuHypervisor) Name() string {
return "qemu"
}
// Available checks if QEMU is installed and accessible.
func (q *QemuHypervisor) Available() bool {
_, err := exec.LookPath(q.Binary)
return err == nil
}
// BuildCommand creates the QEMU command for running a VM.
func (q *QemuHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
format := DetectImageFormat(image)
if format == FormatUnknown {
return nil, fmt.Errorf("unknown image format: %s", image)
}
args := []string{
"-m", fmt.Sprintf("%d", opts.Memory),
"-smp", fmt.Sprintf("%d", opts.CPUs),
"-enable-kvm",
}
// Add the image based on format
switch format {
case FormatISO:
args = append(args, "-cdrom", image)
args = append(args, "-boot", "d")
case FormatQCOW2:
args = append(args, "-drive", fmt.Sprintf("file=%s,format=qcow2", image))
case FormatVMDK:
args = append(args, "-drive", fmt.Sprintf("file=%s,format=vmdk", image))
case FormatRaw:
args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw", image))
}
// Always run in nographic mode for container-like behavior
args = append(args, "-nographic")
// Add serial console for log output
args = append(args, "-serial", "stdio")
// Network with port forwarding
netdev := "user,id=net0"
if opts.SSHPort > 0 {
netdev += fmt.Sprintf(",hostfwd=tcp::%d-:22", opts.SSHPort)
}
for hostPort, guestPort := range opts.Ports {
netdev += fmt.Sprintf(",hostfwd=tcp::%d-:%d", hostPort, guestPort)
}
args = append(args, "-netdev", netdev)
args = append(args, "-device", "virtio-net-pci,netdev=net0")
// Add 9p shares for volumes
shareID := 0
for hostPath, guestPath := range opts.Volumes {
tag := fmt.Sprintf("share%d", shareID)
args = append(args,
"-fsdev", fmt.Sprintf("local,id=%s,path=%s,security_model=none", tag, hostPath),
"-device", fmt.Sprintf("virtio-9p-pci,fsdev=%s,mount_tag=%s", tag, filepath.Base(guestPath)),
)
shareID++
}
// Check if KVM is available on Linux, remove -enable-kvm if not
if runtime.GOOS != "linux" || !isKVMAvailable() {
// Remove -enable-kvm from args
newArgs := make([]string, 0, len(args))
for _, arg := range args {
if arg != "-enable-kvm" {
newArgs = append(newArgs, arg)
}
}
args = newArgs
// On macOS, use HVF acceleration if available
if runtime.GOOS == "darwin" {
args = append(args, "-accel", "hvf")
}
}
cmd := exec.CommandContext(ctx, q.Binary, args...)
return cmd, nil
}
// isKVMAvailable checks if KVM is available on the system.
func isKVMAvailable() bool {
_, err := os.Stat("/dev/kvm")
return err == nil
}
// HyperkitHypervisor implements Hypervisor for macOS Hyperkit.
type HyperkitHypervisor struct {
// Binary is the path to the hyperkit binary.
Binary string
}
// NewHyperkitHypervisor creates a new Hyperkit hypervisor instance.
func NewHyperkitHypervisor() *HyperkitHypervisor {
return &HyperkitHypervisor{
Binary: "hyperkit",
}
}
// Name returns the hypervisor name.
func (h *HyperkitHypervisor) Name() string {
return "hyperkit"
}
// Available checks if Hyperkit is installed and accessible.
func (h *HyperkitHypervisor) Available() bool {
if runtime.GOOS != "darwin" {
return false
}
_, err := exec.LookPath(h.Binary)
return err == nil
}
// BuildCommand creates the Hyperkit command for running a VM.
func (h *HyperkitHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
format := DetectImageFormat(image)
if format == FormatUnknown {
return nil, fmt.Errorf("unknown image format: %s", image)
}
args := []string{
"-m", fmt.Sprintf("%dM", opts.Memory),
"-c", fmt.Sprintf("%d", opts.CPUs),
"-A", // ACPI
"-u", // Unlimited console output
"-s", "0:0,hostbridge",
"-s", "31,lpc",
"-l", "com1,stdio", // Serial console
}
// Add PCI slot for disk (slot 2)
switch format {
case FormatISO:
args = append(args, "-s", fmt.Sprintf("2:0,ahci-cd,%s", image))
case FormatQCOW2, FormatVMDK, FormatRaw:
args = append(args, "-s", fmt.Sprintf("2:0,virtio-blk,%s", image))
}
// Network with port forwarding (slot 3)
netArgs := "virtio-net"
if opts.SSHPort > 0 || len(opts.Ports) > 0 {
// Hyperkit uses slirp for user networking with port forwarding
portForwards := make([]string, 0)
if opts.SSHPort > 0 {
portForwards = append(portForwards, fmt.Sprintf("tcp:%d:22", opts.SSHPort))
}
for hostPort, guestPort := range opts.Ports {
portForwards = append(portForwards, fmt.Sprintf("tcp:%d:%d", hostPort, guestPort))
}
if len(portForwards) > 0 {
netArgs += "," + strings.Join(portForwards, ",")
}
}
args = append(args, "-s", "3:0,"+netArgs)
cmd := exec.CommandContext(ctx, h.Binary, args...)
return cmd, nil
}
// DetectImageFormat determines the image format from its file extension.
func DetectImageFormat(path string) ImageFormat {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".iso":
return FormatISO
case ".qcow2":
return FormatQCOW2
case ".vmdk":
return FormatVMDK
case ".raw", ".img":
return FormatRaw
default:
return FormatUnknown
}
}
// DetectHypervisor returns the best available hypervisor for the current platform.
func DetectHypervisor() (Hypervisor, error) {
// On macOS, prefer Hyperkit if available, fall back to QEMU
if runtime.GOOS == "darwin" {
hk := NewHyperkitHypervisor()
if hk.Available() {
return hk, nil
}
}
// Try QEMU on all platforms
qemu := NewQemuHypervisor()
if qemu.Available() {
return qemu, nil
}
return nil, fmt.Errorf("no hypervisor available: install qemu or hyperkit (macOS)")
}
// GetHypervisor returns a specific hypervisor by name.
func GetHypervisor(name string) (Hypervisor, error) {
switch strings.ToLower(name) {
case "qemu":
h := NewQemuHypervisor()
if !h.Available() {
return nil, fmt.Errorf("qemu is not available")
}
return h, nil
case "hyperkit":
h := NewHyperkitHypervisor()
if !h.Available() {
return nil, fmt.Errorf("hyperkit is not available (requires macOS)")
}
return h, nil
default:
return nil, fmt.Errorf("unknown hypervisor: %s", name)
}
}