diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 261148f24..caf47c339 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,23 +79,57 @@ jobs: ! command -v mkfs.ext4 &> /dev/null || \ ! command -v iptables &> /dev/null || \ ! command -v qemu-system-x86_64 &> /dev/null || \ - ! qemu-system-x86_64 --version >/dev/null 2>&1; then + ! qemu-system-x86_64 --version >/dev/null 2>&1 || \ + ! command -v qemu-img &> /dev/null || \ + ! command -v swtpm &> /dev/null || \ + ! test -d /usr/share/OVMF; then apt_update_with_retry - timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables qemu-system-x86 qemu-utils + timeout 300s sudo apt-get install -y erofs-utils e2fsprogs iptables ovmf qemu-system-x86 qemu-utils swtpm fi + if test -f /usr/share/OVMF/OVMF_CODE_4M.secboot.fd && test -f /usr/share/OVMF/OVMF_VARS_4M.ms.fd; then + ovmf_code=/usr/share/OVMF/OVMF_CODE_4M.secboot.fd + ovmf_vars=/usr/share/OVMF/OVMF_VARS_4M.ms.fd + elif test -f /usr/share/OVMF/OVMF_CODE.secboot.fd && test -f /usr/share/OVMF/OVMF_VARS.ms.fd; then + ovmf_code=/usr/share/OVMF/OVMF_CODE.secboot.fd + ovmf_vars=/usr/share/OVMF/OVMF_VARS.ms.fd + else + echo "Secure Boot OVMF firmware with Microsoft-enrolled variables is unavailable" >&2 + exit 1 + fi + echo "HYPEMAN_WINDOWS_OVMF_CODE=$ovmf_code" >> "$GITHUB_ENV" + echo "HYPEMAN_WINDOWS_OVMF_VARS=$ovmf_vars" >> "$GITHUB_ENV" go mod download - name: Verify Linux test toolchain run: | set -euo pipefail TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" - for bin in mkfs.erofs mkfs.ext4 iptables qemu-system-x86_64; do + for bin in mkfs.erofs mkfs.ext4 iptables qemu-img qemu-system-x86_64 swtpm; do if ! sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin' >/dev/null"; then echo "missing required binary under sudo PATH: $bin" exit 1 fi sudo env "PATH=$TEST_PATH" bash -lc "command -v '$bin'" done + test -f "$HYPEMAN_WINDOWS_OVMF_CODE" + test -f "$HYPEMAN_WINDOWS_OVMF_VARS" + + - name: Test Windows hypervisor primitives + run: | + TEST_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" + for attempt in 1 2 3; do + if sudo env \ + "PATH=$TEST_PATH" \ + "CI=true" \ + "HYPEMAN_RUN_WINDOWS_CONFIG_INTEGRATION=1" \ + "HYPEMAN_WINDOWS_OVMF_CODE=$HYPEMAN_WINDOWS_OVMF_CODE" \ + "HYPEMAN_WINDOWS_OVMF_VARS=$HYPEMAN_WINDOWS_OVMF_VARS" \ + go test -count=1 -run '^TestWindowsConfigIntegration$' -timeout 2m ./lib/hypervisor/qemu; then + exit 0 + fi + test "$attempt" = 3 || sleep 5 + done + exit 1 # Slash-command runs are maintainer-approved and need authenticated pulls # for images that are not covered by the prewarm cache. diff --git a/README.md b/README.md index ef1f0fe25..70d3ee66f 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,9 @@ hypeman logs --source vmm my-app # View Hypeman operational logs hypeman logs --source hypeman my-app + +# View software TPM logs for a TPM-backed QEMU guest +hypeman logs --source swtpm my-app ``` For all available commands, run `hypeman --help`. diff --git a/cmd/api/api/instances.go b/cmd/api/api/instances.go index adcaf05fa..c5ba06e42 100644 --- a/cmd/api/api/instances.go +++ b/cmd/api/api/instances.go @@ -884,6 +884,8 @@ func (s *ApiService) GetInstanceLogs(ctx context.Context, request oapi.GetInstan source = instances.LogSourceVMM case oapi.Hypeman: source = instances.LogSourceHypeman + case oapi.Swtpm: + source = instances.LogSourceSWTPM } } diff --git a/lib/hypervisor/README.md b/lib/hypervisor/README.md index 719df4f5f..cb1cf9a35 100644 --- a/lib/hypervisor/README.md +++ b/lib/hypervisor/README.md @@ -37,6 +37,8 @@ if hv.Capabilities().SupportsSnapshot { } ``` +Capabilities also describe boot requirements such as UEFI firmware and TPM support. Guest compatibility is checked against these properties instead of hard-coding a hypervisor type. Resource requirements and image policy remain guest-level validation concerns. + ## Platform Differences ### Linux (Cloud Hypervisor, QEMU) diff --git a/lib/hypervisor/cloudhypervisor/process.go b/lib/hypervisor/cloudhypervisor/process.go index 14b306be1..824b19160 100644 --- a/lib/hypervisor/cloudhypervisor/process.go +++ b/lib/hypervisor/cloudhypervisor/process.go @@ -72,7 +72,9 @@ func NewStarter() *Starter { // Verify Starter implements the interface var _ hypervisor.VMStarter = (*Starter)(nil) -func (s *Starter) ValidateConfig(hypervisor.VMConfig) error { return nil } +func (s *Starter) ValidateConfig(config hypervisor.VMConfig) error { + return hypervisor.ValidateDirectRawConfig("cloud-hypervisor", config) +} // SocketName returns the socket filename for Cloud Hypervisor. func (s *Starter) SocketName() string { @@ -108,6 +110,9 @@ func (s *Starter) ResolveVersion(p *paths.Paths, requested string) (string, erro // StartVM launches Cloud Hypervisor, configures the VM, and boots it. // Returns the process ID and a Hypervisor client for subsequent operations. func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) { + if err := s.ValidateConfig(config); err != nil { + return 0, nil, fmt.Errorf("validate cloud-hypervisor config: %w", err) + } log := logger.FromContext(ctx) // Validate version diff --git a/lib/hypervisor/config.go b/lib/hypervisor/config.go index 2562868da..07f392e8d 100644 --- a/lib/hypervisor/config.go +++ b/lib/hypervisor/config.go @@ -26,7 +26,12 @@ type VMConfig struct { // PCI device passthrough (GPU, etc.) PCIDevices []string - // Boot configuration + // Boot configuration. Empty BootMode preserves the existing direct-kernel + // behavior for Linux callers. + BootMode BootMode + Firmware *FirmwareConfig + TPM *TPMConfig + KernelPath string InitrdPath string KernelArgs string @@ -54,9 +59,54 @@ type CPUTopology struct { Packages int } +type BootMode string + +const ( + BootModeDirect BootMode = "direct" + BootModeUEFI BootMode = "uefi" +) + +// EffectiveBootMode preserves direct Linux kernel boot for existing callers. +func (c VMConfig) EffectiveBootMode() BootMode { + if c.BootMode == "" { + return BootModeDirect + } + return c.BootMode +} + +// FirmwareConfig describes UEFI firmware files. CodePath is immutable firmware; +// VarsPath is per-instance writable variable storage. +type FirmwareConfig struct { + CodePath string + VarsPath string + SecureBoot bool +} + +// TPMConfig describes a per-instance software TPM 2.0 endpoint. +type TPMConfig struct { + SocketPath string + StateDir string +} + +type DiskFormat string + +const ( + DiskFormatRaw DiskFormat = "raw" + DiskFormatQCOW2 DiskFormat = "qcow2" +) + +// EffectiveFormat preserves raw disks for existing callers. +func (d DiskConfig) EffectiveFormat() DiskFormat { + if d.Format == "" { + return DiskFormatRaw + } + return d.Format +} + // DiskConfig represents a disk attached to the VM type DiskConfig struct { Path string + Format DiskFormat Readonly bool IOBps int64 // Sustained I/O rate limit in bytes/sec (0 = unlimited) IOBurstBps int64 // Burst I/O rate in bytes/sec (0 = same as IOBps) diff --git a/lib/hypervisor/config_validation.go b/lib/hypervisor/config_validation.go new file mode 100644 index 000000000..4419e2a01 --- /dev/null +++ b/lib/hypervisor/config_validation.go @@ -0,0 +1,57 @@ +package hypervisor + +import "fmt" + +// ValidateBootConfig validates boot and disk fields shared by hypervisor backends. +func ValidateBootConfig(cfg VMConfig) error { + switch cfg.EffectiveBootMode() { + case BootModeDirect: + if cfg.Firmware != nil { + return fmt.Errorf("direct boot cannot specify firmware") + } + if cfg.TPM != nil { + return fmt.Errorf("direct boot cannot specify a TPM") + } + case BootModeUEFI: + if cfg.Firmware == nil { + return fmt.Errorf("UEFI boot requires firmware") + } + if cfg.Firmware.CodePath == "" || cfg.Firmware.VarsPath == "" { + return fmt.Errorf("UEFI boot requires firmware code and variable storage paths") + } + if cfg.KernelPath != "" || cfg.InitrdPath != "" || cfg.KernelArgs != "" { + return fmt.Errorf("UEFI boot cannot specify a direct kernel, initrd, or kernel arguments") + } + if cfg.TPM != nil && (cfg.TPM.SocketPath == "" || cfg.TPM.StateDir == "") { + return fmt.Errorf("TPM requires socket and state directory paths") + } + default: + return fmt.Errorf("unsupported boot mode %q", cfg.BootMode) + } + + for i, disk := range cfg.Disks { + switch disk.EffectiveFormat() { + case DiskFormatRaw, DiskFormatQCOW2: + default: + return fmt.Errorf("disk %d has unsupported format %q", i, disk.Format) + } + } + return nil +} + +// ValidateDirectRawConfig preserves the Linux-only contract of backends that +// do not implement firmware boot or qcow2 disks. +func ValidateDirectRawConfig(backend string, cfg VMConfig) error { + if err := ValidateBootConfig(cfg); err != nil { + return err + } + if cfg.EffectiveBootMode() != BootModeDirect { + return fmt.Errorf("%s does not support %s boot", backend, cfg.EffectiveBootMode()) + } + for i, disk := range cfg.Disks { + if disk.EffectiveFormat() != DiskFormatRaw { + return fmt.Errorf("%s does not support disk %d format %q", backend, i, disk.EffectiveFormat()) + } + } + return nil +} diff --git a/lib/hypervisor/config_validation_test.go b/lib/hypervisor/config_validation_test.go new file mode 100644 index 000000000..8bfa71916 --- /dev/null +++ b/lib/hypervisor/config_validation_test.go @@ -0,0 +1,51 @@ +package hypervisor + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateBootConfigPreservesDirectRawDefaults(t *testing.T) { + cfg := VMConfig{ + KernelPath: "/kernel", + Disks: []DiskConfig{{Path: "/rootfs"}}, + } + require.NoError(t, ValidateBootConfig(cfg)) + assert.Equal(t, BootModeDirect, cfg.EffectiveBootMode()) + assert.Equal(t, DiskFormatRaw, cfg.Disks[0].EffectiveFormat()) +} + +func TestValidateBootConfigUEFI(t *testing.T) { + valid := VMConfig{ + BootMode: BootModeUEFI, + Firmware: &FirmwareConfig{CodePath: "/ovmf/code", VarsPath: "/instance/vars"}, + TPM: &TPMConfig{SocketPath: "/instance/swtpm.sock", StateDir: "/instance/tpm"}, + Disks: []DiskConfig{{Path: "/instance/disk", Format: DiskFormatQCOW2}}, + } + require.NoError(t, ValidateBootConfig(valid)) + + tests := []struct { + name string + cfg VMConfig + }{ + {name: "missing firmware", cfg: VMConfig{BootMode: BootModeUEFI}}, + {name: "direct kernel", cfg: VMConfig{BootMode: BootModeUEFI, Firmware: valid.Firmware, KernelPath: "/kernel"}}, + {name: "incomplete TPM", cfg: VMConfig{BootMode: BootModeUEFI, Firmware: valid.Firmware, TPM: &TPMConfig{StateDir: "/state"}}}, + {name: "unknown disk", cfg: VMConfig{Disks: []DiskConfig{{Path: "/disk", Format: "vhdx"}}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Error(t, ValidateBootConfig(tt.cfg)) + }) + } +} + +func TestValidateDirectRawConfigRejectsFirmwareAndQCOW2(t *testing.T) { + uefi := VMConfig{BootMode: BootModeUEFI, Firmware: &FirmwareConfig{CodePath: "/code", VarsPath: "/vars"}} + assert.ErrorContains(t, ValidateDirectRawConfig("backend", uefi), "does not support uefi boot") + + qcow := VMConfig{Disks: []DiskConfig{{Path: "/disk", Format: DiskFormatQCOW2}}} + assert.ErrorContains(t, ValidateDirectRawConfig("backend", qcow), "does not support disk 0 format") +} diff --git a/lib/hypervisor/firecracker/process.go b/lib/hypervisor/firecracker/process.go index 01fd8ce2e..28c9268a6 100644 --- a/lib/hypervisor/firecracker/process.go +++ b/lib/hypervisor/firecracker/process.go @@ -58,7 +58,9 @@ func WithUFFDClient(client UFFDClient) StarterOption { var _ hypervisor.VMStarter = (*Starter)(nil) -func (s *Starter) ValidateConfig(hypervisor.VMConfig) error { return nil } +func (s *Starter) ValidateConfig(config hypervisor.VMConfig) error { + return hypervisor.ValidateDirectRawConfig("firecracker", config) +} func (s *Starter) SocketName() string { return "fc.sock" @@ -90,6 +92,9 @@ func (s *Starter) ResolveVersion(p *paths.Paths, requested string) (string, erro } func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) { + if err := s.ValidateConfig(config); err != nil { + return 0, nil, fmt.Errorf("validate firecracker config: %w", err) + } processCtx, processSpan := hypervisor.StartProcessSpan(ctx, hypervisor.TypeFirecracker) pid, err := s.startProcess(processCtx, p, version, socketPath) hypervisor.FinishTraceSpan(processSpan, err) diff --git a/lib/hypervisor/hypervisor.go b/lib/hypervisor/hypervisor.go index 947eade20..da117d8b0 100644 --- a/lib/hypervisor/hypervisor.go +++ b/lib/hypervisor/hypervisor.go @@ -378,6 +378,12 @@ type Capabilities struct { // SupportsVsock indicates if vsock communication is available SupportsVsock bool + // SupportsUEFIBoot indicates if firmware boot is available. + SupportsUEFIBoot bool + + // SupportsTPM indicates if a software TPM can be attached at boot. + SupportsTPM bool + // SupportsGPUPassthrough indicates if PCI device passthrough is available SupportsGPUPassthrough bool diff --git a/lib/hypervisor/qemu/README.md b/lib/hypervisor/qemu/README.md index 7229345e6..d79c81113 100644 --- a/lib/hypervisor/qemu/README.md +++ b/lib/hypervisor/qemu/README.md @@ -4,6 +4,14 @@ The `qemu` backend uses `q35` on amd64 and `virt` on arm64. These architecture-n QEMU ships many other machine models, including versioned compatibility aliases and hardware-emulation boards that do not fit Hypeman's guest contract. Hypeman intentionally does not mirror that open-ended list in its API. If another model eventually provides a useful, supportable capability profile, expose it as another hypervisor backend with explicit lifecycle and device guarantees rather than as an unchecked machine-type string. +## Firmware boot and TPM ownership + +The standard amd64 `qemu` profile supports UEFI firmware boot, Secure Boot variable storage, qcow2 disks, and software TPM 2.0 devices. These requirements are represented as runtime capabilities so callers can validate a guest's requirements without branching on the QEMU type name. Direct-kernel profiles reject firmware and TPM configuration. + +Firmware code is immutable host input. Each instance receives its own writable NVRAM file and TPM state directory, which must be preserved together with the guest disk. Hypeman starts `swtpm` first because QEMU connects to its control socket as a client; launching both processes concurrently would race that required ordering. Once the socket exists, QEMU starts immediately. + +QEMU and `swtpm` run in detached process groups and keep running if Hypeman restarts. QEMU retains its TPM control connection, and `swtpm --terminate` exits when that connection closes. Startup cleanup owns both processes as one cleanup stack so a partial boot cannot leave either process behind. TPM output is available through the instance logs API with `source=swtpm`. + ## `qemu-microvm` The `qemu-microvm` backend uses QEMU's Linux amd64-only `microvm` board. Upstream registers this board only in the x86 system emulator; `qemu-system-aarch64` does not provide an equivalent `microvm` machine. Hypeman uses direct kernel boot, `ttyS0` serial logs, and virtio-mmio transport for disks, networking, vsock, and the optional balloon. diff --git a/lib/hypervisor/qemu/config.go b/lib/hypervisor/qemu/config.go index 648763c22..b3959a29a 100644 --- a/lib/hypervisor/qemu/config.go +++ b/lib/hypervisor/qemu/config.go @@ -20,7 +20,11 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string { microvm := machine == MachineTypeMicroVM // Machine type with KVM acceleration (arch-specific when omitted). - args = append(args, "-machine", string(machine)+",accel=kvm") + machineArg := string(machine) + ",accel=kvm" + if cfg.Firmware != nil && cfg.Firmware.SecureBoot { + machineArg += ",smm=on" + } + args = append(args, "-machine", machineArg) if microvm { // Do not allow a host qemu.conf to add devices outside microvm's // documented eight virtio-mmio-device limit. @@ -51,6 +55,18 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string { args = append(args, "-device", strings.Join(balloonOpts, ",")) } + // Firmware boot. The code image is shared and immutable; variable storage is + // a per-instance writable copy. + if cfg.EffectiveBootMode() == hypervisor.BootModeUEFI { + args = append(args, + "-drive", fmt.Sprintf("if=pflash,format=raw,unit=0,file=%s,readonly=on", cfg.Firmware.CodePath), + "-drive", fmt.Sprintf("if=pflash,format=raw,unit=1,file=%s", cfg.Firmware.VarsPath), + ) + if cfg.Firmware.SecureBoot { + args = append(args, "-global", "driver=cfi.pflash01,property=secure,value=on") + } + } + // Kernel and initrd if cfg.KernelPath != "" { args = append(args, "-kernel", cfg.KernelPath) @@ -64,7 +80,7 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string { // Disk configuration for i, disk := range cfg.Disks { - driveOpts := fmt.Sprintf("file=%s,format=raw,if=none,id=drive%d", disk.Path, i) + driveOpts := fmt.Sprintf("file=%s,format=%s,if=none,id=drive%d", disk.Path, disk.EffectiveFormat(), i) if disk.Readonly { // Disable host-side file locking for shared readonly bases so multiple // VMs can boot concurrently from the same image without lock contention. @@ -80,6 +96,15 @@ func buildArgs(cfg hypervisor.VMConfig, machine MachineType) []string { args = append(args, "-device", fmt.Sprintf("%s,drive=drive%d", virtioDevice(microvm, "virtio-blk"), i)) } + // Software TPM 2.0. The swtpm process is started by Starter before QEMU. + if cfg.TPM != nil { + args = append(args, + "-chardev", fmt.Sprintf("socket,id=chrtpm,path=%s", cfg.TPM.SocketPath), + "-tpmdev", "emulator,id=tpm0,chardev=chrtpm", + "-device", "tpm-crb,tpmdev=tpm0", + ) + } + // Network configuration for i, net := range cfg.Networks { netdevOpts := fmt.Sprintf("tap,id=net%d,ifname=%s,script=no,downscript=no", i, net.TAPDevice) diff --git a/lib/hypervisor/qemu/config_test.go b/lib/hypervisor/qemu/config_test.go index c8d25584f..4d4ae0595 100644 --- a/lib/hypervisor/qemu/config_test.go +++ b/lib/hypervisor/qemu/config_test.go @@ -81,6 +81,35 @@ func TestBuildArgs_Disks(t *testing.T) { assert.Contains(t, args, "virtio-blk-pci,drive=drive1") } +func TestBuildArgs_UEFISecureBootTPMAndQCOW2(t *testing.T) { + cfg := hypervisor.VMConfig{ + VCPUs: 2, + MemoryBytes: 1024 * 1024 * 1024, + BootMode: hypervisor.BootModeUEFI, + Firmware: &hypervisor.FirmwareConfig{ + CodePath: "/firmware/OVMF_CODE.fd", + VarsPath: "/instance/OVMF_VARS.fd", + SecureBoot: true, + }, + TPM: &hypervisor.TPMConfig{ + SocketPath: "/instance/swtpm.sock", + StateDir: "/instance/tpm", + }, + Disks: []hypervisor.DiskConfig{{Path: "/instance/windows.qcow2", Format: hypervisor.DiskFormatQCOW2}}, + } + + args := buildArgs(cfg, MachineTypeQ35) + assert.Contains(t, args, "q35,accel=kvm,smm=on") + assert.Contains(t, args, "if=pflash,format=raw,unit=0,file=/firmware/OVMF_CODE.fd,readonly=on") + assert.Contains(t, args, "if=pflash,format=raw,unit=1,file=/instance/OVMF_VARS.fd") + assert.Contains(t, args, "driver=cfi.pflash01,property=secure,value=on") + assert.Contains(t, args, "file=/instance/windows.qcow2,format=qcow2,if=none,id=drive0") + assert.Contains(t, args, "socket,id=chrtpm,path=/instance/swtpm.sock") + assert.Contains(t, args, "emulator,id=tpm0,chardev=chrtpm") + assert.Contains(t, args, "tpm-crb,tpmdev=tpm0") + assert.NotContains(t, args, "-kernel") +} + func TestBuildArgs_Network(t *testing.T) { cfg := hypervisor.VMConfig{ VCPUs: 1, @@ -187,6 +216,25 @@ func TestBuildArgs_MicroVM(t *testing.T) { } } +func TestProfilesValidateFirmwareAndDiskFormats(t *testing.T) { + uefi := hypervisor.VMConfig{ + BootMode: hypervisor.BootModeUEFI, + Firmware: &hypervisor.FirmwareConfig{CodePath: "/code", VarsPath: "/vars"}, + Disks: []hypervisor.DiskConfig{{Path: "/disk", Format: hypervisor.DiskFormatQCOW2}}, + } + standard := StandardProfile{} + if standardMachineType() == MachineTypeQ35 { + assert.True(t, standard.capabilities().SupportsUEFIBoot) + assert.True(t, standard.capabilities().SupportsTPM) + assert.NoError(t, standard.validateConfig(uefi)) + } else { + assert.False(t, standard.capabilities().SupportsUEFIBoot) + assert.False(t, standard.capabilities().SupportsTPM) + assert.ErrorContains(t, standard.validateConfig(uefi), "does not support UEFI boot on this host") + } + assert.ErrorContains(t, MicroVMProfile{}.validateConfig(uefi), "does not support uefi boot") +} + func TestBuildArgs_GuestMemoryBalloon(t *testing.T) { cfg := hypervisor.VMConfig{ VCPUs: 1, diff --git a/lib/hypervisor/qemu/fork.go b/lib/hypervisor/qemu/fork.go index 1141b7f4d..4599c033e 100644 --- a/lib/hypervisor/qemu/fork.go +++ b/lib/hypervisor/qemu/fork.go @@ -89,6 +89,14 @@ func rewriteQEMUConfigPaths(cfg hypervisor.VMConfig, sourceDir, targetDir string cfg.VsockSocket = replace(cfg.VsockSocket) cfg.KernelPath = replace(cfg.KernelPath) cfg.InitrdPath = replace(cfg.InitrdPath) + if cfg.Firmware != nil { + cfg.Firmware.CodePath = replace(cfg.Firmware.CodePath) + cfg.Firmware.VarsPath = replace(cfg.Firmware.VarsPath) + } + if cfg.TPM != nil { + cfg.TPM.SocketPath = replace(cfg.TPM.SocketPath) + cfg.TPM.StateDir = replace(cfg.TPM.StateDir) + } return cfg } diff --git a/lib/hypervisor/qemu/fork_test.go b/lib/hypervisor/qemu/fork_test.go index 833b1fdc8..81f074e81 100644 --- a/lib/hypervisor/qemu/fork_test.go +++ b/lib/hypervisor/qemu/fork_test.go @@ -34,7 +34,16 @@ func TestPrepareFork_RewritesSnapshotConfig(t *testing.T) { VsockSocket: sourceDir + "/vsock/vsock.sock", KernelPath: sourceDir + "/kernel/vmlinuz", InitrdPath: sourceDir + "/kernel/initrd", - KernelArgs: "console=ttyS0 root=" + sourceDir + "/rootfs note=keep-" + sourceDir + "-as-substring", + Firmware: &hypervisor.FirmwareConfig{ + CodePath: sourceDir + "/OVMF_CODE.fd", + VarsPath: sourceDir + "/OVMF_VARS.fd", + SecureBoot: true, + }, + TPM: &hypervisor.TPMConfig{ + SocketPath: sourceDir + "/swtpm.sock", + StateDir: sourceDir + "/tpm", + }, + KernelArgs: "console=ttyS0 root=" + sourceDir + "/rootfs note=keep-" + sourceDir + "-as-substring", Disks: []hypervisor.DiskConfig{ {Path: sourceDir + "/overlay.raw"}, {Path: "/volumes/volume-data.raw"}, @@ -77,6 +86,12 @@ func TestPrepareFork_RewritesSnapshotConfig(t *testing.T) { assert.Equal(t, targetDir+"/logs/fork-app.log", updated.SerialLogPath) assert.Equal(t, targetDir+"/kernel/vmlinuz", updated.KernelPath) assert.Equal(t, targetDir+"/kernel/initrd", updated.InitrdPath) + require.NotNil(t, updated.Firmware) + assert.Equal(t, targetDir+"/OVMF_CODE.fd", updated.Firmware.CodePath) + assert.Equal(t, targetDir+"/OVMF_VARS.fd", updated.Firmware.VarsPath) + require.NotNil(t, updated.TPM) + assert.Equal(t, targetDir+"/swtpm.sock", updated.TPM.SocketPath) + assert.Equal(t, targetDir+"/tpm", updated.TPM.StateDir) assert.Equal(t, initial.KernelArgs, updated.KernelArgs) assert.Equal(t, targetDir+"/overlay.raw", updated.Disks[0].Path) assert.Equal(t, "/volumes/volume-data.raw", updated.Disks[1].Path, "non-instance paths should remain unchanged") diff --git a/lib/hypervisor/qemu/process.go b/lib/hypervisor/qemu/process.go index c02c5beef..c3f59e8e5 100644 --- a/lib/hypervisor/qemu/process.go +++ b/lib/hypervisor/qemu/process.go @@ -28,8 +28,9 @@ import ( // Timeout constants for QEMU operations const ( - // socketWaitTimeout is how long to wait for QMP socket to become available after process start - socketWaitTimeout = 10 * time.Second + // qemuSocketWaitTimeout bounds QMP startup on a heavily loaded host. Process + // exit is checked on every poll, so deterministic startup failures return early. + qemuSocketWaitTimeout = 30 * time.Second // migrationTimeout is how long to wait for migration to complete migrationTimeout = 30 * time.Second @@ -365,7 +366,7 @@ func (s *Starter) startQEMUProcess(ctx context.Context, p *paths.Paths, version // Wait for socket to be ready socketWaitStart := time.Now() - if err := waitForSocketOrExit(socketPath, socketWaitTimeout, proc); err != nil { + if err := waitForSocketOrExit(socketPath, qemuSocketWaitTimeout, proc); err != nil { processSpan.RecordError(err) processSpan.SetStatus(codes.Error, err.Error()) cu.Clean() @@ -425,6 +426,25 @@ func (s *Starter) validateSnapshotMachineType(stored MachineType) (MachineType, return expected, nil } +func (s *Starter) startConfiguredProcess(ctx context.Context, p *paths.Paths, version, socketPath string, config hypervisor.VMConfig, args []string) (int, *QEMU, *cleanup.Cleanup, error) { + cu := cleanup.Make(func() {}) + tpmProcess, err := startSWTPM(config.TPM, filepath.Dir(socketPath)) + if err != nil { + return 0, nil, nil, err + } + if tpmProcess != nil { + cu.Add(tpmProcess.cleanup) + } + + pid, hv, qemuCleanup, err := s.startQEMUProcess(ctx, p, version, socketPath, args) + if err != nil { + cu.Clean() + return 0, nil, nil, err + } + cu.Add(qemuCleanup.Clean) + return pid, hv, &cu, nil +} + // StartVM launches QEMU with the VM configuration and returns a Hypervisor client. // QEMU receives all configuration via command-line arguments at process start. func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) { @@ -474,7 +494,7 @@ func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, s // Build command arguments: QMP socket + VM configuration args := buildQMPArgs(socketPath) args = append(args, buildArgs(attempt, machineType)...) - pid, hv, cu, err = s.startQEMUProcess(ctx, p, version, socketPath, args) + pid, hv, cu, err = s.startConfiguredProcess(ctx, p, version, socketPath, attempt, args) if err == nil { booted = attempt started = true @@ -609,7 +629,7 @@ func (s *Starter) RestoreVM(ctx context.Context, p *paths.Paths, version string, incomingURI := "exec:cat < " + memoryFile args = append(args, "-incoming", incomingURI) - pid, hv, cu, err := s.startQEMUProcess(ctx, p, version, socketPath, args) + pid, hv, cu, err := s.startConfiguredProcess(ctx, p, version, socketPath, config, args) if err != nil { return 0, nil, err } diff --git a/lib/hypervisor/qemu/profile.go b/lib/hypervisor/qemu/profile.go index 2ac3a2cbf..07485b849 100644 --- a/lib/hypervisor/qemu/profile.go +++ b/lib/hypervisor/qemu/profile.go @@ -25,11 +25,17 @@ func (StandardProfile) machineType() (MachineType, error) { return standardMachineType(), nil } func (StandardProfile) capabilities() hypervisor.Capabilities { - return qemuCapabilities(true) + supportsFirmware := standardMachineType() == MachineTypeQ35 + return qemuCapabilities(true, supportsFirmware) } -func (StandardProfile) validateConfig(hypervisor.VMConfig) error { return nil } -func (StandardProfile) requiresStoredMachineType() bool { return false } -func (StandardProfile) requiresStoredVersion() bool { return false } +func (p StandardProfile) validateConfig(cfg hypervisor.VMConfig) error { + if err := hypervisor.ValidateBootConfig(cfg); err != nil { + return err + } + return validateProfileCapabilities(p.hypervisorType(), p.capabilities(), cfg) +} +func (StandardProfile) requiresStoredMachineType() bool { return false } +func (StandardProfile) requiresStoredVersion() bool { return false } // MicroVMProfile selects QEMU's minimal x86 microvm board and enforces its // virtio-mmio device contract. @@ -40,9 +46,12 @@ func (MicroVMProfile) machineType() (MachineType, error) { return microVMMachineType() } func (MicroVMProfile) capabilities() hypervisor.Capabilities { - return qemuCapabilities(false) + return qemuCapabilities(false, false) } func (MicroVMProfile) validateConfig(cfg hypervisor.VMConfig) error { + if err := hypervisor.ValidateDirectRawConfig("qemu-microvm", cfg); err != nil { + return err + } if cfg.HotplugBytes > 0 { return fmt.Errorf("microvm does not support hotplug memory") } @@ -65,7 +74,17 @@ func (MicroVMProfile) validateConfig(cfg hypervisor.VMConfig) error { func (MicroVMProfile) requiresStoredMachineType() bool { return true } func (MicroVMProfile) requiresStoredVersion() bool { return true } -func qemuCapabilities(supportsPCI bool) hypervisor.Capabilities { +func validateProfileCapabilities(name hypervisor.Type, caps hypervisor.Capabilities, cfg hypervisor.VMConfig) error { + if cfg.EffectiveBootMode() == hypervisor.BootModeUEFI && !caps.SupportsUEFIBoot { + return fmt.Errorf("%s does not support UEFI boot on this host", name) + } + if cfg.TPM != nil && !caps.SupportsTPM { + return fmt.Errorf("%s does not support TPM devices", name) + } + return nil +} + +func qemuCapabilities(supportsPCI, supportsFirmware bool) hypervisor.Capabilities { return hypervisor.Capabilities{ SupportsSnapshot: true, // PrepareFork rewrites the saved QEMU VM config for forks (fork.go); @@ -75,6 +94,8 @@ func qemuCapabilities(supportsPCI bool) hypervisor.Capabilities { SupportsBalloonControl: true, SupportsPause: true, SupportsVsock: true, + SupportsUEFIBoot: supportsFirmware, + SupportsTPM: supportsFirmware, SupportsGPUPassthrough: supportsPCI, SupportsDiskIOLimit: true, SupportsGracefulVMMShutdown: true, diff --git a/lib/hypervisor/qemu/swtpm.go b/lib/hypervisor/qemu/swtpm.go new file mode 100644 index 000000000..68c409539 --- /dev/null +++ b/lib/hypervisor/qemu/swtpm.go @@ -0,0 +1,89 @@ +package qemu + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "syscall" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" +) + +const swtpmSocketWaitTimeout = 30 * time.Second + +func startSWTPM(config *hypervisor.TPMConfig, instanceDir string) (*startedProcess, error) { + if config == nil { + return nil, nil + } + + binary, err := exec.LookPath("swtpm") + if err != nil { + return nil, fmt.Errorf("find swtpm: %w", err) + } + if err := os.MkdirAll(config.StateDir, 0700); err != nil { + return nil, fmt.Errorf("create swtpm state directory: %w", err) + } + if err := os.MkdirAll(filepath.Dir(config.SocketPath), 0755); err != nil { + return nil, fmt.Errorf("create swtpm socket directory: %w", err) + } + if err := os.Remove(config.SocketPath); err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("remove stale swtpm socket: %w", err) + } + + logsDir := filepath.Join(instanceDir, "logs") + if err := os.MkdirAll(logsDir, 0755); err != nil { + return nil, fmt.Errorf("create swtpm logs directory: %w", err) + } + logPath := filepath.Join(logsDir, "swtpm.log") + logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return nil, fmt.Errorf("create swtpm log: %w", err) + } + defer logFile.Close() + + cmd := exec.Command(binary, + "socket", + "--tpm2", + "--tpmstate", "dir="+config.StateDir, + "--ctrl", "type=unixio,path="+config.SocketPath, + "--terminate", + ) + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + proc, err := startManagedProcess(cmd, config.SocketPath) + if err != nil { + return nil, fmt.Errorf("start swtpm: %w", err) + } + if err := waitForSocketFileOrExit(config.SocketPath, swtpmSocketWaitTimeout, proc); err != nil { + proc.cleanup() + if logData, readErr := os.ReadFile(logPath); readErr == nil && len(logData) > 0 { + const maxLogBytes = 32 << 10 + if len(logData) > maxLogBytes { + logData = logData[len(logData)-maxLogBytes:] + } + return nil, fmt.Errorf("wait for swtpm: %w; swtpm.log: %s", err, logData) + } + return nil, fmt.Errorf("wait for swtpm: %w", err) + } + return proc, nil +} + +// A connect probe would make swtpm treat the probe as its control client; with +// --terminate, closing that probe would stop swtpm before QEMU connects. +func waitForSocketFileOrExit(socketPath string, timeout time.Duration, proc *startedProcess) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + if info, err := os.Stat(socketPath); err == nil && info.Mode()&os.ModeSocket != 0 { + return nil + } + if waitErr, exited := proc.checkExited(); exited { + return fmt.Errorf("swtpm exited early: %w", waitErr) + } + time.Sleep(socketPollInterval) + } + return fmt.Errorf("timeout waiting for socket") +} diff --git a/lib/hypervisor/qemu/windows_config_integration_linux_test.go b/lib/hypervisor/qemu/windows_config_integration_linux_test.go new file mode 100644 index 000000000..0556c3b03 --- /dev/null +++ b/lib/hypervisor/qemu/windows_config_integration_linux_test.go @@ -0,0 +1,126 @@ +//go:build linux && amd64 + +package qemu + +import ( + "context" + "io/fs" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/kernel/hypeman/lib/hypervisor" + "github.com/kernel/hypeman/lib/paths" + "github.com/stretchr/testify/require" +) + +func requireWindowsConfigDependency(t *testing.T, path, description string) string { + t.Helper() + if path != "" { + if _, err := os.Stat(path); err == nil { + return path + } + } + if os.Getenv("CI") == "true" { + t.Fatalf("required Windows config integration dependency is missing: %s (%s)", description, path) + } + t.Skipf("%s is unavailable", description) + return "" +} + +func TestWindowsConfigIntegration(t *testing.T) { + if os.Getenv("HYPEMAN_RUN_WINDOWS_CONFIG_INTEGRATION") != "1" { + t.Skip("run by the dedicated Windows config CI gate") + } + requireWindowsConfigDependency(t, "/dev/kvm", "KVM") + if _, err := exec.LookPath("qemu-system-x86_64"); err != nil { + requireWindowsConfigDependency(t, "", "qemu-system-x86_64") + } + if _, err := exec.LookPath("qemu-img"); err != nil { + requireWindowsConfigDependency(t, "", "qemu-img") + } + if _, err := exec.LookPath("swtpm"); err != nil { + requireWindowsConfigDependency(t, "", "swtpm") + } + + codePath := os.Getenv("HYPEMAN_WINDOWS_OVMF_CODE") + if codePath == "" { + codePath = "/usr/share/OVMF/OVMF_CODE_4M.secboot.fd" + } + varsTemplate := os.Getenv("HYPEMAN_WINDOWS_OVMF_VARS") + if varsTemplate == "" { + varsTemplate = "/usr/share/OVMF/OVMF_VARS_4M.ms.fd" + } + requireWindowsConfigDependency(t, codePath, "Secure Boot OVMF code") + requireWindowsConfigDependency(t, varsTemplate, "Microsoft-enrolled OVMF variables") + + dir, err := os.MkdirTemp("/tmp", "hypeman-win-config-") + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, os.RemoveAll(dir)) }) + + varsData, err := os.ReadFile(varsTemplate) + require.NoError(t, err) + varsPath := filepath.Join(dir, "OVMF_VARS.fd") + require.NoError(t, os.WriteFile(varsPath, varsData, 0600)) + + basePath := filepath.Join(dir, "base.raw") + base, err := os.Create(basePath) + require.NoError(t, err) + require.NoError(t, base.Truncate(64<<20)) + require.NoError(t, base.Close()) + + diskPath := filepath.Join(dir, "instance.qcow2") + output, err := exec.Command("qemu-img", "create", "-f", "qcow2", "-F", "raw", "-b", basePath, diskPath).CombinedOutput() + require.NoError(t, err, "qemu-img create: %s", output) + + socketPath := filepath.Join(dir, "qemu.sock") + tpmSocket := filepath.Join(dir, "swtpm.sock") + tpmState := filepath.Join(dir, "tpm") + config := hypervisor.VMConfig{ + VCPUs: 1, + MemoryBytes: 512 << 20, + BootMode: hypervisor.BootModeUEFI, + Firmware: &hypervisor.FirmwareConfig{ + CodePath: codePath, + VarsPath: varsPath, + SecureBoot: true, + }, + TPM: &hypervisor.TPMConfig{SocketPath: tpmSocket, StateDir: tpmState}, + Disks: []hypervisor.DiskConfig{{Path: diskPath, Format: hypervisor.DiskFormatQCOW2}}, + } + + starter := NewStarter() + boot := func() { + pid, vm, err := starter.StartVM(context.Background(), paths.New(dir), "", socketPath, config) + require.NoError(t, err) + require.Positive(t, pid) + info, err := vm.GetVMInfo(context.Background()) + require.NoError(t, err) + require.Equal(t, hypervisor.StateRunning, info.State) + require.NoError(t, vm.Shutdown(context.Background())) + require.Eventually(t, func() bool { + _, err := os.Stat(socketPath) + return os.IsNotExist(err) + }, 5*time.Second, 20*time.Millisecond) + } + + boot() + info, err := os.Stat(diskPath) + require.NoError(t, err) + require.Positive(t, info.Size(), "qcow2 overlay must contain metadata") + + var stateFiles int + require.NoError(t, filepath.WalkDir(tpmState, func(path string, entry fs.DirEntry, err error) error { + if err == nil && !entry.IsDir() { + stateFiles++ + } + return err + })) + require.Positive(t, stateFiles, "swtpm must persist TPM 2.0 state") + require.FileExists(t, varsPath) + + boot() + require.FileExists(t, varsPath) +} diff --git a/lib/hypervisor/vz/starter.go b/lib/hypervisor/vz/starter.go index d62026b53..ec5132fd4 100644 --- a/lib/hypervisor/vz/starter.go +++ b/lib/hypervisor/vz/starter.go @@ -105,7 +105,9 @@ func NewStarter() *Starter { var _ hypervisor.VMStarter = (*Starter)(nil) -func (s *Starter) ValidateConfig(hypervisor.VMConfig) error { return nil } +func (s *Starter) ValidateConfig(config hypervisor.VMConfig) error { + return hypervisor.ValidateDirectRawConfig("vz", config) +} func (s *Starter) SocketName() string { return "vz.sock" @@ -130,6 +132,9 @@ func (s *Starter) ResolveVersion(p *paths.Paths, requested string) (string, erro // StartVM spawns a vz-shim subprocess to host the VM. func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) { + if err := s.ValidateConfig(config); err != nil { + return 0, nil, fmt.Errorf("validate vz config: %w", err) + } shimConfig := buildShimConfigFromVMConfig(config, socketPath) return s.startShim(ctx, p, version, shimConfig, 30*time.Second) } diff --git a/lib/instances/logs.go b/lib/instances/logs.go index f10e8537d..54b280f81 100644 --- a/lib/instances/logs.go +++ b/lib/instances/logs.go @@ -24,6 +24,8 @@ const ( LogSourceVMM LogSource = "vmm" // LogSourceHypeman is the hypeman operations log LogSourceHypeman LogSource = "hypeman" + // LogSourceSWTPM is the software TPM log for TPM-backed QEMU guests. + LogSourceSWTPM LogSource = "swtpm" ) // ErrTailNotFound is returned when the tail command is not available @@ -73,6 +75,8 @@ func (m *manager) streamInstanceLogs(ctx context.Context, id string, tail int, f logPath = m.paths.InstanceVMMLog(id) case LogSourceHypeman: logPath = m.paths.InstanceHypemanLog(id) + case LogSourceSWTPM: + logPath = m.paths.InstanceSWTPMLog(id) default: // Default to app log for backwards compatibility logPath = m.paths.InstanceAppLog(id) diff --git a/lib/instances/manager.go b/lib/instances/manager.go index 5d595d823..0568ff4ba 100644 --- a/lib/instances/manager.go +++ b/lib/instances/manager.go @@ -781,7 +781,7 @@ func (m *manager) StreamInstanceLogs(ctx context.Context, id string, tail int, f return m.streamInstanceLogs(ctx, id, tail, follow, source) } -// RotateLogs rotates all instance logs (app, vmm, hypeman) that exceed maxBytes +// RotateLogs rotates all instance logs that exceed maxBytes func (m *manager) RotateLogs(ctx context.Context, maxBytes int64, maxFiles int) error { instances, err := m.listInstances(ctx) if err != nil { @@ -790,11 +790,11 @@ func (m *manager) RotateLogs(ctx context.Context, maxBytes int64, maxFiles int) var lastErr error for _, inst := range instances { - // Rotate all three log types logPaths := []string{ m.paths.InstanceAppLog(inst.Id), m.paths.InstanceVMMLog(inst.Id), m.paths.InstanceHypemanLog(inst.Id), + m.paths.InstanceSWTPMLog(inst.Id), } for _, logPath := range logPaths { if err := rotateLogIfNeeded(logPath, maxBytes, maxFiles); err != nil { diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 52ab3b2c0..938c49b4f 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -278,6 +278,7 @@ const ( const ( App GetInstanceLogsParamsSource = "app" Hypeman GetInstanceLogsParamsSource = "hypeman" + Swtpm GetInstanceLogsParamsSource = "swtpm" Vmm GetInstanceLogsParamsSource = "vmm" ) @@ -19143,56 +19144,56 @@ var swaggerSpec = []string{ "tKZlwhDXb5mml8frtVFfn53BR4j9h1VQL4+Jr4da3B/avlUF9LOzSKg25BcHU7hTKOOwo5eGWn2zmN+u", "g/orsanHIgT7J9i1a5DPyGUFAfCyBZ7K89uf5fyTCWP99ooCOBcjiVMdgTaZiHttMRY8CYP/7Y9GIaDr", "jkCEOIyPjEO4Npif5byoZlAjZZplXcnXDROoeJmmG2iY7FRQzbSJZW7+ok3MlIKPHXW3ETfZoZGrZEWv", - "LKE6DDt/sHeB/IKRRAgvHlwqy1R7/R4Tedo7/tX9a5mmvX7PjacCS34D4X4LoGOzwfWIF7szFdTGr2L5", - "TfAY68y+AsjYuDmcOt0ukb/AF/70nkVvs/uEZAjyQcOI+zmJoJXx1g0+QhbAkl8WepQzNTWktnZTjZ/l", - "wA4vzn25nS4hHBfu0wv/5Weg/W6L7PBjJn66dx7isT6CLzkTVq/NZiZVE3JoW+zHZ09IH25L1qbahUK+", - "0ubN7XydCNNK6usswn0QY/k1mhuZUsMjKP0TLaTUFbIv8IGxSJcz3xaUCcYN1DNdCP2lJdVLZwi+dIL8", - "sTNaEVp95PoYwucu8D78hX9UfvFDRS8vOH7fC98Ajw+1yRVnM5LRXDMrV+UpI9EqslwRaz0xGi1IRDOT", - "KwZl7BhJueBpnlaBn+2OLSmAVFzup5d9Ms0NSaiag16EDzGcXrFIpikTMQML2VgsGF1yq9QpklDDRLQa", - "aAblb5eMXEt1lUgag5LvonCwfJ5ilgIBRTtlhsbUUBA1Lu2Jn2AWz2VRERcVa8HelNQQj4XKxbcI6W+b", - "vfQDvSQMMKu5XhSVEyMaMxEFsZwvPm829uGtwRfMNCf6ieJybsVLP2WgTtXq6YfzecTwfGHByFK5bezC", - "5jcIvbpdiaynP3gy+vc80jhXP8dP5OIplnjTKf48fDsF0X02/p1P78CRisQ5dlc5lUDmf1avTMFQquFO", - "kFqJ23hb10xRIq5Y5hvxvL0//J+nt7CmfSacsN+q2LcVIyon/TmwXLeqt+K5n8iM6GxJFavYJ2TBPqbq", - "k4lPUlW43Jdi7nQMG49mwber3MkoCtqXFF/ZdpNtu5CD27Jtb5tdc6pXGDkXA4jSDHNwZ8ZtZdXOdPBv", - "mgvSmF2FZX5yFln6Du6MLZ4WjBBZY0ZXiaTxnyFMd4MHJ5JKIf4DIEp8SfijFathNUAfbHNllbO+z618", - "fXa228YllNnII5T5gjlEJSnGfpYGSv8/XzKleOxgOsnJ2RMXMMs1UbkYkucpN8RIcsVYVuaUAJDH0M7P", - "I2GsV1SvQV70e0wYtcokF2brKMpXP85g3t2qDvsd80kHaP3VId3ZIQ2W/S+PnQGXgawJnMBmzdRQs7XQ", - "JhczqVKUy+hU5rZ1y4PsMtn9xIKVM54wvdKGpRgXOMsTOG5QHMEVwHXf4S73ISrWnhxMWMuYSrnWXAo9", - "Fi5bI2PK9m0/t+1XQpyCDgFDC/56jkzy8wifs4PBiDFq2lYNMIugsGbvuLdHs2wvpoa2hGi54b3HkH6A", - "eDiiV+lUJjwiCRdXmuwk/ArVE7LUJLF/7G4MqJvAdx+6vO/tT5Zd6VMxk8HiaUizBTH/qfKqHFvzjskv", - "jq09Y9XD4vkPbHSYrW0vIKwYTQZQkNcj15Dc8IS/RVZnG+Ha8AiTfkrIgtdnJWrBWJwxo+w7FJLLkoRF", - "xhts9jIlo71xPhodRhkH+LNDBoMDhtf+OIUeT85fYSIoS6Va9cfC/gMafvn4HL27M+qsCZWBusrB5HTv", - "+ZYQ4wtYpn/jGD2c4EbsgOCGf3UJ3hwRpPUM6ZYjKrNNqpLM/vRBpE6C+2pX+DLtCgDJVMxmZ65oBEKx", - "XuQmltcibENYyiRP7T/wj9NtwF6GRovX8OpnI+3icLZ24yf4RRxKN6eYYXHHT+L0wAX7UmNW7cL5KYAQ", - "U4sGDN4Cj82fkbo/vPm+uo6fobvTragvnPrZnK27vvncGDzGRXU9vpRjjpTmZ2LkZuvTNeXt1qfvExld", - "aQeGUjUbWr0NAMbtjyUgtHMRgpgAuZnEgQgR9ibjCpDfGgZIxNzRhBLDVMoFTfZgztgIQFt7KxZdSg4p", - "0lHCIUmNx4BalAA63fWCCWJnA4Yq30DFo6tdaanqO1VnpJFkyiKZMg/3vRtS3f5GuflBqjp29+fCF19W", - "1t/Ox07VznMLXHl7j+8FX35G30CodJw7h7If0c4zWf6IpqA+gb0Z9w5Hetzrk3HvIB337A6cUDChUkPu", - "kZSL3DA9JE/QvgVJsPdHRLNIilh71HFvwTsc6baUWCTLlvzK+/DdXYo9jqpgKV+4TkLswb5H7PeQtEN2", - "qgfOncm4D4cuJjI3aO5358q9FTMD5pHdO/fVVs7IV92+Cyf/mzu+NR4Fu2zZZWXrkbNnuV6wdpPbz1jJ", - "JzdTQLP21TX1gvxdTnWfCHaN1nClzXCN79mvz7GDu0Dat13dBGXfzf0rxH4HiP1yrcJwiRhgaa9kTx2I", - "mcjeICIs9WnvjoZAkwDsBhnRhDw/OR2LyLIiBPdTLJXAnRwgON7Cj/92QZ6evOiTJ1DpkfyYT3eH5LlI", - "Vr7eNvpoxgIlMWReERVkilTL4tD1jGMH6vmYweK2g09UOhlPRsCz4vfKB4n3ewtGY5BI/uj9LLGzAEbw", - "i5/tAbL7574str23UfjovWBGrQaPZ4ap9WbPXJ6UKFAr3CXtQeCc4IbQk7ZD7bHPyj5RNkBwisODXgCr", - "4t3Xqgcfv0Lo3XjJME4E681Nc8D6FJBkQOPVlxXLpBekYI4hFli9rou6AW1Zwo6XbVQwoMu2yO/PyOS+", - "kXfVkOD/XU8XzPSLdTRltX2yRFzUG9nq6fXJwQsEJHaOqohmNOJm1Sc0Sdwd5W6CIiJlUIi/U8XoVSyv", - "xXAsXhSVTlxCLzk5f9X3jloSc32FLThf7JA8XzKl82kxOAIHDb3GsOYsHgsjSUSTKE+suMFmMxZBLi4U", - "MNEtvtxiKL2PeHbKToLVVipR7fkXV+QtTBOweyVZNCluD7d6T7EooTxth/92ghoEHEKowdQ2KgXhYpa4", - "kKpISa2Ja2rAEj7n08QFCOkheblgRNOUjUWWUCGYIrnGqHg79EGmmNY5JnjbBgAmFymqT0pov0xJ40IT", - "EimVxmgCS+Gvz4g2LNtAZi+w5TOY80eSbbFx19MnMlI3xtBuCnGvELshSCm44JaO8sQHMN5pKDoO6FNL", - "iV/KwX+p+HzOlD0VFJkshuPhsfbLiYe+lrHcWvDxonirW8HHotVKVmIlY28jNNukRLuOezeL+gt0fsVb", - "0fvco5tlEf9kP+rYdz1bNTwI9+g9Z/lnqaN/UUkS7GrAKin8SzMnVUZeO6q1RNvtsFqdM2s/ZqZrZ/ys", - "Twab9SWjZdFa+mybwvv5EcLoblEe7rok2pdNWzW0q5pu2pLyvx3P/rOgwI8DZP+JUU5uAWT/WeXdA9L4", - "p8M/CR7UT5VHX/M9+2qzf3os+o+VPo+A9ADH1pY+j1zPBa9uVJReu3e6qUmuxT+TBO/iHW8gv/tl/6r1", - "d1AZKou1zQVtCZ6lmVn5gDbnqyyDzjR/y4YtjuAibvXjuYJvEdL54cjD02lrQOefszj8J4kZdcX7uCan", - "TwJV178wjMHqmatdLHv21hlQFS34krUb3esn2C1Rptggkxk4V2JcMLce/i4zVA3nb4lr3mGuun9B9UcA", - "y2cxiblikUlWWInTcgTs4xtNlLSaADyXatUeJYJH5Acl08duNlvuQ3emnDGsjDNMV4OYGjpYem6zwYT2", - "HtGdPp7SMjzCBXn2Pdlhb4zCGhNkZjUfwmfFkmK5fQ00uVsd8P6oxbLJ37LJfNpllBuqhTx31VhIlGsj", - "U7/3p0/IDlQfmzNh98KK+jOQZDMllzxmcW2MvaVMcFX3Wxb0pnZXK1QUpeO8coGD+yQyTJcLaf6WZ3W2", - "UITETLmgMLitdTnqZwqT+G1/lAsfgOP2yI/i6xXmNL8dr+xYSoRKmG4RjZQI8bz79Zr7kq+5ajKUv9Nq", - "t50Pz9lsvO6WH9UxbeljFH4ocufu1mz9+vNJ6eH6i8zmcabzZaGQtpnNPy8SHN3d/XDX5vLXX3AK6DPm", - "le+KqRwasC2GCOZniOmO2ZIlMkuhIjm82+v3cpX0jnsLY7LjvT2I/V5IbY6PHj047L377d3/HwAA//9w", - "eSdugOoBAA==", + "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", + "AwAA//+bMKlSiOoBAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/paths/paths.go b/lib/paths/paths.go index add23ff52..7e20cba38 100644 --- a/lib/paths/paths.go +++ b/lib/paths/paths.go @@ -241,6 +241,11 @@ func (p *Paths) InstanceHypemanLog(id string) string { return filepath.Join(p.InstanceLogs(id), "hypeman.log") } +// InstanceSWTPMLog returns the path to the instance software TPM log. +func (p *Paths) InstanceSWTPMLog(id string) string { + return filepath.Join(p.InstanceLogs(id), "swtpm.log") +} + // InstanceSnapshots returns the path to instance snapshots directory. func (p *Paths) InstanceSnapshots(id string) string { return filepath.Join(p.InstanceDir(id), "snapshots") diff --git a/openapi.yaml b/openapi.yaml index b117d2035..cd92d81bf 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3570,7 +3570,7 @@ paths: required: false schema: type: string - enum: [app, vmm, hypeman] + enum: [app, vmm, hypeman, swtpm] default: app description: | Log source to stream: