Skip to content

Commit 4ab69ee

Browse files
committed
Add Windows hypervisor primitives
1 parent 4860317 commit 4ab69ee

13 files changed

Lines changed: 497 additions & 14 deletions

File tree

.github/workflows/test.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,23 +79,29 @@ jobs:
7979
! command -v mkfs.ext4 &> /dev/null || \
8080
! command -v iptables &> /dev/null || \
8181
! command -v qemu-system-x86_64 &> /dev/null || \
82-
! qemu-system-x86_64 --version >/dev/null 2>&1; then
82+
! qemu-system-x86_64 --version >/dev/null 2>&1 || \
83+
! command -v qemu-img &> /dev/null || \
84+
! command -v swtpm &> /dev/null || \
85+
! test -f /usr/share/OVMF/OVMF_CODE_4M.secboot.fd || \
86+
! test -f /usr/share/OVMF/OVMF_VARS_4M.ms.fd; then
8387
apt_update_with_retry
84-
timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables qemu-system-x86 qemu-utils
88+
timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables ovmf qemu-system-x86 qemu-utils swtpm
8589
fi
8690
go mod download
8791
8892
- name: Verify Linux test toolchain
8993
run: |
9094
set -euo pipefail
9195
TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"
92-
for bin in mkfs.erofs mkfs.ext4 iptables qemu-system-x86_64; do
96+
for bin in mkfs.erofs mkfs.ext4 iptables qemu-img qemu-system-x86_64 swtpm; do
9397
if ! sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin' >/dev/null"; then
9498
echo "missing required binary under sudo PATH: $bin"
9599
exit 1
96100
fi
97101
sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin'"
98102
done
103+
test -f /usr/share/OVMF/OVMF_CODE_4M.secboot.fd
104+
test -f /usr/share/OVMF/OVMF_VARS_4M.ms.fd
99105
100106
# Slash-command runs are maintainer-approved and need authenticated pulls
101107
# for images that are not covered by the prewarm cache.

lib/hypervisor/cloudhypervisor/process.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ func NewStarter() *Starter {
7272
// Verify Starter implements the interface
7373
var _ hypervisor.VMStarter = (*Starter)(nil)
7474

75-
func (s *Starter) ValidateConfig(hypervisor.VMConfig) error { return nil }
75+
func (s *Starter) ValidateConfig(config hypervisor.VMConfig) error {
76+
return hypervisor.ValidateDirectRawConfig("cloud-hypervisor", config)
77+
}
7678

7779
// SocketName returns the socket filename for Cloud Hypervisor.
7880
func (s *Starter) SocketName() string {
@@ -108,6 +110,9 @@ func (s *Starter) ResolveVersion(p *paths.Paths, requested string) (string, erro
108110
// StartVM launches Cloud Hypervisor, configures the VM, and boots it.
109111
// Returns the process ID and a Hypervisor client for subsequent operations.
110112
func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) {
113+
if err := s.ValidateConfig(config); err != nil {
114+
return 0, nil, fmt.Errorf("validate cloud-hypervisor config: %w", err)
115+
}
111116
log := logger.FromContext(ctx)
112117

113118
// Validate version

lib/hypervisor/config.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ type VMConfig struct {
2626
// PCI device passthrough (GPU, etc.)
2727
PCIDevices []string
2828

29-
// Boot configuration
29+
// Boot configuration. Empty BootMode preserves the existing direct-kernel
30+
// behavior for Linux callers.
31+
BootMode BootMode
32+
Firmware *FirmwareConfig
33+
TPM *TPMConfig
34+
3035
KernelPath string
3136
InitrdPath string
3237
KernelArgs string
@@ -54,9 +59,54 @@ type CPUTopology struct {
5459
Packages int
5560
}
5661

62+
type BootMode string
63+
64+
const (
65+
BootModeDirect BootMode = "direct"
66+
BootModeUEFI BootMode = "uefi"
67+
)
68+
69+
// EffectiveBootMode preserves direct Linux kernel boot for existing callers.
70+
func (c VMConfig) EffectiveBootMode() BootMode {
71+
if c.BootMode == "" {
72+
return BootModeDirect
73+
}
74+
return c.BootMode
75+
}
76+
77+
// FirmwareConfig describes UEFI firmware files. CodePath is immutable firmware;
78+
// VarsPath is per-instance writable variable storage.
79+
type FirmwareConfig struct {
80+
CodePath string
81+
VarsPath string
82+
SecureBoot bool
83+
}
84+
85+
// TPMConfig describes a per-instance software TPM 2.0 endpoint.
86+
type TPMConfig struct {
87+
SocketPath string
88+
StateDir string
89+
}
90+
91+
type DiskFormat string
92+
93+
const (
94+
DiskFormatRaw DiskFormat = "raw"
95+
DiskFormatQCOW2 DiskFormat = "qcow2"
96+
)
97+
98+
// EffectiveFormat preserves raw disks for existing callers.
99+
func (d DiskConfig) EffectiveFormat() DiskFormat {
100+
if d.Format == "" {
101+
return DiskFormatRaw
102+
}
103+
return d.Format
104+
}
105+
57106
// DiskConfig represents a disk attached to the VM
58107
type DiskConfig struct {
59108
Path string
109+
Format DiskFormat
60110
Readonly bool
61111
IOBps int64 // Sustained I/O rate limit in bytes/sec (0 = unlimited)
62112
IOBurstBps int64 // Burst I/O rate in bytes/sec (0 = same as IOBps)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package hypervisor
2+
3+
import "fmt"
4+
5+
// ValidateBootConfig validates boot and disk fields shared by hypervisor backends.
6+
func ValidateBootConfig(cfg VMConfig) error {
7+
switch cfg.EffectiveBootMode() {
8+
case BootModeDirect:
9+
if cfg.Firmware != nil {
10+
return fmt.Errorf("direct boot cannot specify firmware")
11+
}
12+
if cfg.TPM != nil {
13+
return fmt.Errorf("direct boot cannot specify a TPM")
14+
}
15+
case BootModeUEFI:
16+
if cfg.Firmware == nil {
17+
return fmt.Errorf("UEFI boot requires firmware")
18+
}
19+
if cfg.Firmware.CodePath == "" || cfg.Firmware.VarsPath == "" {
20+
return fmt.Errorf("UEFI boot requires firmware code and variable storage paths")
21+
}
22+
if cfg.KernelPath != "" || cfg.InitrdPath != "" || cfg.KernelArgs != "" {
23+
return fmt.Errorf("UEFI boot cannot specify a direct kernel, initrd, or kernel arguments")
24+
}
25+
if cfg.TPM != nil && (cfg.TPM.SocketPath == "" || cfg.TPM.StateDir == "") {
26+
return fmt.Errorf("TPM requires socket and state directory paths")
27+
}
28+
default:
29+
return fmt.Errorf("unsupported boot mode %q", cfg.BootMode)
30+
}
31+
32+
for i, disk := range cfg.Disks {
33+
switch disk.EffectiveFormat() {
34+
case DiskFormatRaw, DiskFormatQCOW2:
35+
default:
36+
return fmt.Errorf("disk %d has unsupported format %q", i, disk.Format)
37+
}
38+
}
39+
return nil
40+
}
41+
42+
// ValidateDirectRawConfig preserves the Linux-only contract of backends that
43+
// do not implement firmware boot or qcow2 disks.
44+
func ValidateDirectRawConfig(backend string, cfg VMConfig) error {
45+
if err := ValidateBootConfig(cfg); err != nil {
46+
return err
47+
}
48+
if cfg.EffectiveBootMode() != BootModeDirect {
49+
return fmt.Errorf("%s does not support %s boot", backend, cfg.EffectiveBootMode())
50+
}
51+
for i, disk := range cfg.Disks {
52+
if disk.EffectiveFormat() != DiskFormatRaw {
53+
return fmt.Errorf("%s does not support disk %d format %q", backend, i, disk.EffectiveFormat())
54+
}
55+
}
56+
return nil
57+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package hypervisor
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestValidateBootConfigPreservesDirectRawDefaults(t *testing.T) {
11+
cfg := VMConfig{
12+
KernelPath: "/kernel",
13+
Disks: []DiskConfig{{Path: "/rootfs"}},
14+
}
15+
require.NoError(t, ValidateBootConfig(cfg))
16+
assert.Equal(t, BootModeDirect, cfg.EffectiveBootMode())
17+
assert.Equal(t, DiskFormatRaw, cfg.Disks[0].EffectiveFormat())
18+
}
19+
20+
func TestValidateBootConfigUEFI(t *testing.T) {
21+
valid := VMConfig{
22+
BootMode: BootModeUEFI,
23+
Firmware: &FirmwareConfig{CodePath: "/ovmf/code", VarsPath: "/instance/vars"},
24+
TPM: &TPMConfig{SocketPath: "/instance/swtpm.sock", StateDir: "/instance/tpm"},
25+
Disks: []DiskConfig{{Path: "/instance/disk", Format: DiskFormatQCOW2}},
26+
}
27+
require.NoError(t, ValidateBootConfig(valid))
28+
29+
tests := []struct {
30+
name string
31+
cfg VMConfig
32+
}{
33+
{name: "missing firmware", cfg: VMConfig{BootMode: BootModeUEFI}},
34+
{name: "direct kernel", cfg: VMConfig{BootMode: BootModeUEFI, Firmware: valid.Firmware, KernelPath: "/kernel"}},
35+
{name: "incomplete TPM", cfg: VMConfig{BootMode: BootModeUEFI, Firmware: valid.Firmware, TPM: &TPMConfig{StateDir: "/state"}}},
36+
{name: "unknown disk", cfg: VMConfig{Disks: []DiskConfig{{Path: "/disk", Format: "vhdx"}}}},
37+
}
38+
for _, tt := range tests {
39+
t.Run(tt.name, func(t *testing.T) {
40+
assert.Error(t, ValidateBootConfig(tt.cfg))
41+
})
42+
}
43+
}
44+
45+
func TestValidateDirectRawConfigRejectsFirmwareAndQCOW2(t *testing.T) {
46+
uefi := VMConfig{BootMode: BootModeUEFI, Firmware: &FirmwareConfig{CodePath: "/code", VarsPath: "/vars"}}
47+
assert.ErrorContains(t, ValidateDirectRawConfig("backend", uefi), "does not support uefi boot")
48+
49+
qcow := VMConfig{Disks: []DiskConfig{{Path: "/disk", Format: DiskFormatQCOW2}}}
50+
assert.ErrorContains(t, ValidateDirectRawConfig("backend", qcow), "does not support disk 0 format")
51+
}

lib/hypervisor/firecracker/process.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ func WithUFFDClient(client UFFDClient) StarterOption {
5858

5959
var _ hypervisor.VMStarter = (*Starter)(nil)
6060

61-
func (s *Starter) ValidateConfig(hypervisor.VMConfig) error { return nil }
61+
func (s *Starter) ValidateConfig(config hypervisor.VMConfig) error {
62+
return hypervisor.ValidateDirectRawConfig("firecracker", config)
63+
}
6264

6365
func (s *Starter) SocketName() string {
6466
return "fc.sock"
@@ -90,6 +92,9 @@ func (s *Starter) ResolveVersion(p *paths.Paths, requested string) (string, erro
9092
}
9193

9294
func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) {
95+
if err := s.ValidateConfig(config); err != nil {
96+
return 0, nil, fmt.Errorf("validate firecracker config: %w", err)
97+
}
9398
processCtx, processSpan := hypervisor.StartProcessSpan(ctx, hypervisor.TypeFirecracker)
9499
pid, err := s.startProcess(processCtx, p, version, socketPath)
95100
hypervisor.FinishTraceSpan(processSpan, err)

lib/hypervisor/qemu/config.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
2020
microvm := machine == MachineTypeMicroVM
2121

2222
// Machine type with KVM acceleration (arch-specific when omitted).
23-
args = append(args, "-machine", string(machine)+",accel=kvm")
23+
machineArg := string(machine) + ",accel=kvm"
24+
if cfg.Firmware != nil && cfg.Firmware.SecureBoot {
25+
machineArg += ",smm=on"
26+
}
27+
args = append(args, "-machine", machineArg)
2428
if microvm {
2529
// Do not allow a host qemu.conf to add devices outside microvm's
2630
// documented eight virtio-mmio-device limit.
@@ -51,6 +55,18 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
5155
args = append(args, "-device", strings.Join(balloonOpts, ","))
5256
}
5357

58+
// Firmware boot. The code image is shared and immutable; variable storage is
59+
// a per-instance writable copy.
60+
if cfg.EffectiveBootMode() == hypervisor.BootModeUEFI {
61+
args = append(args,
62+
"-drive", fmt.Sprintf("if=pflash,format=raw,unit=0,file=%s,readonly=on", cfg.Firmware.CodePath),
63+
"-drive", fmt.Sprintf("if=pflash,format=raw,unit=1,file=%s", cfg.Firmware.VarsPath),
64+
)
65+
if cfg.Firmware.SecureBoot {
66+
args = append(args, "-global", "driver=cfi.pflash01,property=secure,value=on")
67+
}
68+
}
69+
5470
// Kernel and initrd
5571
if cfg.KernelPath != "" {
5672
args = append(args, "-kernel", cfg.KernelPath)
@@ -64,7 +80,7 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
6480

6581
// Disk configuration
6682
for i, disk := range cfg.Disks {
67-
driveOpts := fmt.Sprintf("file=%s,format=raw,if=none,id=drive%d", disk.Path, i)
83+
driveOpts := fmt.Sprintf("file=%s,format=%s,if=none,id=drive%d", disk.Path, disk.EffectiveFormat(), i)
6884
if disk.Readonly {
6985
// Disable host-side file locking for shared readonly bases so multiple
7086
// VMs can boot concurrently from the same image without lock contention.
@@ -80,6 +96,15 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string {
8096
args = append(args, "-device", fmt.Sprintf("%s,drive=drive%d", virtioDevice(microvm, "virtio-blk"), i))
8197
}
8298

99+
// Software TPM 2.0. The swtpm process is started by Starter before QEMU.
100+
if cfg.TPM != nil {
101+
args = append(args,
102+
"-chardev", fmt.Sprintf("socket,id=chrtpm,path=%s", cfg.TPM.SocketPath),
103+
"-tpmdev", "emulator,id=tpm0,chardev=chrtpm",
104+
"-device", "tpm-crb,tpmdev=tpm0",
105+
)
106+
}
107+
83108
// Network configuration
84109
for i, net := range cfg.Networks {
85110
netdevOpts := fmt.Sprintf("tap,id=net%d,ifname=%s,script=no,downscript=no", i, net.TAPDevice)

lib/hypervisor/qemu/config_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,35 @@ func TestBuildArgs_Disks(t *testing.T) {
8181
assert.Contains(t, args, "virtio-blk-pci,drive=drive1")
8282
}
8383

84+
func TestBuildArgs_UEFISecureBootTPMAndQCOW2(t *testing.T) {
85+
cfg := hypervisor.VMConfig{
86+
VCPUs: 2,
87+
MemoryBytes: 1024 * 1024 * 1024,
88+
BootMode: hypervisor.BootModeUEFI,
89+
Firmware: &hypervisor.FirmwareConfig{
90+
CodePath: "/firmware/OVMF_CODE.fd",
91+
VarsPath: "/instance/OVMF_VARS.fd",
92+
SecureBoot: true,
93+
},
94+
TPM: &hypervisor.TPMConfig{
95+
SocketPath: "/instance/swtpm.sock",
96+
StateDir: "/instance/tpm",
97+
},
98+
Disks: []hypervisor.DiskConfig{{Path: "/instance/windows.qcow2", Format: hypervisor.DiskFormatQCOW2}},
99+
}
100+
101+
args := buildArgs(cfg, MachineTypeQ35)
102+
assert.Contains(t, args, "q35,accel=kvm,smm=on")
103+
assert.Contains(t, args, "if=pflash,format=raw,unit=0,file=/firmware/OVMF_CODE.fd,readonly=on")
104+
assert.Contains(t, args, "if=pflash,format=raw,unit=1,file=/instance/OVMF_VARS.fd")
105+
assert.Contains(t, args, "driver=cfi.pflash01,property=secure,value=on")
106+
assert.Contains(t, args, "file=/instance/windows.qcow2,format=qcow2,if=none,id=drive0")
107+
assert.Contains(t, args, "socket,id=chrtpm,path=/instance/swtpm.sock")
108+
assert.Contains(t, args, "emulator,id=tpm0,chardev=chrtpm")
109+
assert.Contains(t, args, "tpm-crb,tpmdev=tpm0")
110+
assert.NotContains(t, args, "-kernel")
111+
}
112+
84113
func TestBuildArgs_Network(t *testing.T) {
85114
cfg := hypervisor.VMConfig{
86115
VCPUs: 1,
@@ -187,6 +216,16 @@ func TestBuildArgs_MicroVM(t *testing.T) {
187216
}
188217
}
189218

219+
func TestProfilesValidateFirmwareAndDiskFormats(t *testing.T) {
220+
uefi := hypervisor.VMConfig{
221+
BootMode: hypervisor.BootModeUEFI,
222+
Firmware: &hypervisor.FirmwareConfig{CodePath: "/code", VarsPath: "/vars"},
223+
Disks: []hypervisor.DiskConfig{{Path: "/disk", Format: hypervisor.DiskFormatQCOW2}},
224+
}
225+
assert.NoError(t, StandardProfile{}.validateConfig(uefi))
226+
assert.ErrorContains(t, MicroVMProfile{}.validateConfig(uefi), "does not support uefi boot")
227+
}
228+
190229
func TestBuildArgs_GuestMemoryBalloon(t *testing.T) {
191230
cfg := hypervisor.VMConfig{
192231
VCPUs: 1,

0 commit comments

Comments
 (0)