Skip to content

Commit e531ec4

Browse files
committed
Add version checking to hypervisor interface
1 parent 661159a commit e531ec4

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed

lib/hypervisor/cloudhypervisor/process.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ func (s *Starter) GetBinaryPath(p *paths.Paths, version string) (string, error)
4040
return vmm.GetBinaryPath(p, chVersion)
4141
}
4242

43+
// GetVersion returns the latest supported Cloud Hypervisor version.
44+
// Cloud Hypervisor binaries are embedded, so we return the latest known version.
45+
func (s *Starter) GetVersion(p *paths.Paths) (string, error) {
46+
return string(vmm.V49_0), nil
47+
}
48+
4349
// StartVM launches Cloud Hypervisor, configures the VM, and boots it.
4450
// Returns the process ID and a Hypervisor client for subsequent operations.
4551
func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) {

lib/hypervisor/hypervisor.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ type VMStarter interface {
5353
// GetBinaryPath returns the path to the hypervisor binary, extracting if needed.
5454
GetBinaryPath(p *paths.Paths, version string) (string, error)
5555

56+
// GetVersion returns the version of the hypervisor binary.
57+
// For embedded binaries (Cloud Hypervisor), returns the latest supported version.
58+
// For system binaries (QEMU), queries the installed binary for its version.
59+
GetVersion(p *paths.Paths) (string, error)
60+
5661
// StartVM launches the hypervisor process and boots the VM.
5762
// Returns the process ID and a Hypervisor client for subsequent operations.
5863
StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config VMConfig) (pid int, hv Hypervisor, err error)

lib/hypervisor/qemu/process.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11+
"regexp"
1112
"runtime"
1213
"syscall"
1314
"time"
@@ -63,6 +64,30 @@ func (s *Starter) GetBinaryPath(p *paths.Paths, version string) (string, error)
6364
return "", fmt.Errorf("%s not found; install with: %s", binaryName, qemuInstallHint())
6465
}
6566

67+
// GetVersion returns the version of the installed QEMU binary.
68+
// Parses the output of "qemu-system-* --version" to extract the version string.
69+
func (s *Starter) GetVersion(p *paths.Paths) (string, error) {
70+
binaryPath, err := s.GetBinaryPath(p, "")
71+
if err != nil {
72+
return "", err
73+
}
74+
75+
cmd := exec.Command(binaryPath, "--version")
76+
output, err := cmd.Output()
77+
if err != nil {
78+
return "", fmt.Errorf("get qemu version: %w", err)
79+
}
80+
81+
// Parse "QEMU emulator version 8.2.0 (Debian ...)" -> "8.2.0"
82+
re := regexp.MustCompile(`version (\d+\.\d+(?:\.\d+)?)`)
83+
matches := re.FindStringSubmatch(string(output))
84+
if len(matches) >= 2 {
85+
return matches[1], nil
86+
}
87+
88+
return "", fmt.Errorf("could not parse QEMU version from: %s", string(output))
89+
}
90+
6691
// StartVM launches QEMU with the VM configuration and returns a Hypervisor client.
6792
// QEMU receives all configuration via command-line arguments at process start.
6893
func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, socketPath string, config hypervisor.VMConfig) (int, hypervisor.Hypervisor, error) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package qemu
2+
3+
import (
4+
"os/exec"
5+
"regexp"
6+
"testing"
7+
8+
"github.com/onkernel/hypeman/lib/paths"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// TestGetVersion_Integration is an integration test that verifies GetVersion
14+
// works correctly with the actual QEMU binary installed on the system.
15+
func TestGetVersion_Integration(t *testing.T) {
16+
// Skip if QEMU is not installed
17+
binaryName, err := qemuBinaryName()
18+
if err != nil {
19+
t.Skipf("Skipping test: %v", err)
20+
}
21+
22+
_, err = exec.LookPath(binaryName)
23+
if err != nil {
24+
t.Skipf("Skipping test: QEMU binary %s not found in PATH", binaryName)
25+
}
26+
27+
// Create starter and get version
28+
starter := NewStarter()
29+
tmpDir := t.TempDir()
30+
p := paths.New(tmpDir)
31+
32+
version, err := starter.GetVersion(p)
33+
require.NoError(t, err, "GetVersion should not return an error")
34+
35+
// Verify version is not empty
36+
assert.NotEmpty(t, version, "Version should not be empty")
37+
38+
// Verify version matches expected format (e.g., "8.2.0", "9.0", "7.2.1")
39+
versionPattern := regexp.MustCompile(`^\d+\.\d+(\.\d+)?$`)
40+
assert.Regexp(t, versionPattern, version, "Version should match pattern X.Y or X.Y.Z")
41+
42+
t.Logf("Detected QEMU version: %s", version)
43+
}
44+
45+
// TestGetVersion_ParsesVersionCorrectly tests the version parsing logic
46+
// with various version string formats.
47+
func TestGetVersion_ParsesVersionCorrectly(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
output string
51+
expected string
52+
wantErr bool
53+
}{
54+
{
55+
name: "debian format",
56+
output: "QEMU emulator version 8.2.0 (Debian 1:8.2.0+dfsg-1)",
57+
expected: "8.2.0",
58+
},
59+
{
60+
name: "simple format",
61+
output: "QEMU emulator version 9.0.0",
62+
expected: "9.0.0",
63+
},
64+
{
65+
name: "two part version",
66+
output: "QEMU emulator version 9.0",
67+
expected: "9.0",
68+
},
69+
{
70+
name: "with git info",
71+
output: "QEMU emulator version 7.2.1 (qemu-7.2.1-1.fc38)",
72+
expected: "7.2.1",
73+
},
74+
{
75+
name: "invalid format",
76+
output: "Some random output",
77+
wantErr: true,
78+
},
79+
{
80+
name: "empty output",
81+
output: "",
82+
wantErr: true,
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
// Use the same regex as in GetVersion
89+
re := regexp.MustCompile(`version (\d+\.\d+(?:\.\d+)?)`)
90+
matches := re.FindStringSubmatch(tt.output)
91+
92+
if tt.wantErr {
93+
assert.Less(t, len(matches), 2, "Should not match for invalid input")
94+
} else {
95+
require.GreaterOrEqual(t, len(matches), 2, "Should find version match")
96+
assert.Equal(t, tt.expected, matches[1], "Parsed version should match expected")
97+
}
98+
})
99+
}
100+
}

lib/instances/create.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
"github.com/onkernel/hypeman/lib/logger"
1616
"github.com/onkernel/hypeman/lib/network"
1717
"github.com/onkernel/hypeman/lib/system"
18-
"github.com/onkernel/hypeman/lib/vmm"
1918
"github.com/onkernel/hypeman/lib/volumes"
2019
"go.opentelemetry.io/otel/attribute"
2120
"go.opentelemetry.io/otel/trace"
@@ -225,6 +224,13 @@ func (m *manager) createInstance(
225224
return nil, fmt.Errorf("get vm starter for %s: %w", hvType, err)
226225
}
227226

227+
// Get hypervisor version
228+
hvVersion, err := starter.GetVersion(m.paths)
229+
if err != nil {
230+
log.WarnContext(ctx, "failed to get hypervisor version", "hypervisor", hvType, "error", err)
231+
hvVersion = "unknown"
232+
}
233+
228234
// 10. Validate, resolve, and auto-bind devices (GPU passthrough)
229235
// Track devices we've marked as attached for cleanup on error.
230236
// The cleanup closure captures this slice by reference, so it will see
@@ -295,7 +301,7 @@ func (m *manager) createInstance(
295301
StoppedAt: nil,
296302
KernelVersion: string(kernelVer),
297303
HypervisorType: hvType,
298-
HypervisorVersion: string(vmm.V49_0), // Use latest
304+
HypervisorVersion: hvVersion,
299305
SocketPath: m.paths.InstanceSocket(id, starter.SocketName()),
300306
DataDir: m.paths.InstanceDir(id),
301307
VsockCID: vsockCID,

0 commit comments

Comments
 (0)