Skip to content

Commit fa369c1

Browse files
committed
Handle qemu pool backend switches and version policy
1 parent 8cc10e5 commit fa369c1

5 files changed

Lines changed: 58 additions & 25 deletions

File tree

lib/hypervisor/qemu/pool.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package qemu
22

33
import (
4-
"fmt"
54
"sync"
65

76
"github.com/kernel/hypeman/lib/hypervisor"
@@ -28,11 +27,11 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
2827
// Try read lock first for existing connection
2928
clientPool.RLock()
3029
if client, ok := clientPool.clients[socketPath]; ok {
31-
clientPool.RUnlock()
32-
if client.hypervisorType != hypervisorType {
33-
return nil, poolTypeMismatchError(socketPath, client.hypervisorType, hypervisorType)
30+
if client.hypervisorType == hypervisorType {
31+
clientPool.RUnlock()
32+
return client, nil
3433
}
35-
return client, nil
34+
// Backend identity changed for this socket path. Recreate under write lock.
3635
}
3736
clientPool.RUnlock()
3837

@@ -42,10 +41,11 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
4241

4342
// Double-check after acquiring write lock
4443
if client, ok := clientPool.clients[socketPath]; ok {
45-
if client.hypervisorType != hypervisorType {
46-
return nil, poolTypeMismatchError(socketPath, client.hypervisorType, hypervisorType)
44+
if client.hypervisorType == hypervisorType {
45+
return client, nil
4746
}
48-
return client, nil
47+
// Stale pooled backend type for this socket path. Drop and reconnect.
48+
removeLocked(socketPath)
4949
}
5050

5151
// Create new client
@@ -58,20 +58,22 @@ func GetOrCreateForType(socketPath string, hypervisorType hypervisor.Type) (*QEM
5858
return client, nil
5959
}
6060

61-
func poolTypeMismatchError(socketPath string, cached, requested hypervisor.Type) error {
62-
return fmt.Errorf("QEMU client for %s is pooled as hypervisor %s, not %s", socketPath, cached, requested)
63-
}
64-
6561
// Remove closes and removes a client from the pool.
6662
// Called automatically on errors to allow fresh reconnection.
6763
// Close is done asynchronously to avoid blocking if the connection is in a bad state.
6864
func Remove(socketPath string) {
6965
clientPool.Lock()
7066
defer clientPool.Unlock()
67+
removeLocked(socketPath)
68+
}
7169

70+
// removeLocked removes an entry while clientPool lock is held.
71+
func removeLocked(socketPath string) {
7272
if client, ok := clientPool.clients[socketPath]; ok {
7373
delete(clientPool.clients, socketPath)
7474
// Close asynchronously to avoid blocking on stuck connections
75-
go client.client.Close()
75+
if client.client != nil {
76+
go client.client.Close()
77+
}
7678
}
7779
}

lib/hypervisor/qemu/pool_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/require"
88
)
99

10-
func TestGetOrCreateForTypeRejectsCachedBackendMismatch(t *testing.T) {
10+
func TestGetOrCreateForTypeEvictsCachedBackendMismatch(t *testing.T) {
1111
socketPath := t.TempDir() + "/qemu.sock"
1212
clientPool.Lock()
1313
clientPool.clients[socketPath] = &QEMU{socketPath: socketPath, hypervisorType: hypervisor.TypeQEMU}
@@ -19,5 +19,9 @@ func TestGetOrCreateForTypeRejectsCachedBackendMismatch(t *testing.T) {
1919
})
2020

2121
_, err := GetOrCreateForType(socketPath, hypervisor.TypeQEMUMicroVM)
22-
require.ErrorContains(t, err, "pooled as hypervisor qemu, not qemu-microvm")
22+
require.Error(t, err)
23+
clientPool.RLock()
24+
_, stillPooled := clientPool.clients[socketPath]
25+
clientPool.RUnlock()
26+
require.False(t, stillPooled, "stale cached backend must be evicted after mismatch")
2327
}

lib/instances/create.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -748,13 +748,13 @@ func (m *manager) startAndBootVM(
748748
return fmt.Errorf("get vm starter: %w", err)
749749
}
750750

751-
// qemu-microvm snapshots are tied to the binary that boots the VM. Refresh
752-
// metadata on every cold start so host upgrades do not leave the instance's
753-
// reported version pinned to its original creation time.
754-
if stored.HypervisorType == hypervisor.TypeQEMUMicroVM {
751+
// Some hypervisors tie snapshot compatibility to the exact runtime binary.
752+
// Refresh metadata on cold start so host upgrades do not leave the stored
753+
// version pinned to original creation time.
754+
if refreshHypervisorVersionOnColdStart(stored.HypervisorType) {
755755
detectedVersion, err := starter.GetVersion(m.paths)
756756
if err != nil {
757-
return fmt.Errorf("get QEMU version for qemu-microvm start: %w", err)
757+
return fmt.Errorf("get hypervisor version for %s start: %w", stored.HypervisorType, err)
758758
}
759759
stored.HypervisorVersion = detectedVersion
760760
}

lib/instances/hypervisor_version.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,46 @@ import (
88
"github.com/kernel/hypeman/lib/logger"
99
)
1010

11+
type hypervisorVersionPolicy struct {
12+
enforceExactInstalledVersion bool
13+
refreshOnColdStart bool
14+
}
15+
16+
var hypervisorVersionPoliciesByType = map[hypervisor.Type]hypervisorVersionPolicy{
17+
hypervisor.TypeQEMUMicroVM: {
18+
enforceExactInstalledVersion: true,
19+
refreshOnColdStart: true,
20+
},
21+
}
22+
23+
func resolveHypervisorVersionPolicy(hvType hypervisor.Type) hypervisorVersionPolicy {
24+
if policy, ok := hypervisorVersionPoliciesByType[hvType]; ok {
25+
return policy
26+
}
27+
return hypervisorVersionPolicy{}
28+
}
29+
30+
func enforceExactInstalledHypervisorVersion(hvType hypervisor.Type) bool {
31+
return resolveHypervisorVersionPolicy(hvType).enforceExactInstalledVersion
32+
}
33+
34+
func refreshHypervisorVersionOnColdStart(hvType hypervisor.Type) bool {
35+
return resolveHypervisorVersionPolicy(hvType).refreshOnColdStart
36+
}
37+
1138
func (m *manager) resolveCreateHypervisorVersion(
1239
ctx context.Context,
1340
starter hypervisor.VMStarter,
1441
hvType hypervisor.Type,
1542
requested string,
1643
) (string, error) {
17-
if hvType == hypervisor.TypeQEMUMicroVM {
44+
if enforceExactInstalledHypervisorVersion(hvType) {
1845
detected, err := starter.GetVersion(m.paths)
1946
if err != nil {
20-
return "", fmt.Errorf("get QEMU version for qemu-microvm: %w", err)
47+
return "", fmt.Errorf("get installed hypervisor version for %s: %w", hvType, err)
2148
}
2249
if requested != "" && requested != detected {
23-
return "", fmt.Errorf("%w: requested qemu-microvm version %q does not match installed QEMU %q", ErrInvalidRequest, requested, detected)
50+
return "", fmt.Errorf("%w: requested %s hypervisor version %q does not match installed version %q", ErrInvalidRequest, hvType, requested, detected)
2451
}
2552
return detected, nil
2653
}

lib/instances/snapshot.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,8 +532,8 @@ func (m *manager) prepareSnapshotTarget(ctx context.Context, source StoredMetada
532532
if target != source.HypervisorType {
533533
version, err = starter.GetVersion(m.paths)
534534
if err != nil {
535-
if target == hypervisor.TypeQEMUMicroVM {
536-
return nil, "", fmt.Errorf("get QEMU version for qemu-microvm snapshot target: %w", err)
535+
if enforceExactInstalledHypervisorVersion(target) {
536+
return nil, "", fmt.Errorf("get installed hypervisor version for %s snapshot target: %w", target, err)
537537
}
538538
logger.FromContext(ctx).WarnContext(ctx, "failed to get hypervisor version", "hypervisor", target, "error", err)
539539
version = "unknown"

0 commit comments

Comments
 (0)