Skip to content

Commit de7e8f8

Browse files
committed
Make vGPU retention records fully delete-only
A fork or snapshot of a failed-create retention stub could never boot: the stub has no boot configuration, and clearing the delete-only marker on the child would only produce a startable-but-broken record that recreates a vGPU from GPUProfile with incomplete metadata. Reject fork and snapshot of retention stubs with the same invalid_state guidance as start, so delete (which retries the release) is the only action on them.
1 parent a81105d commit de7e8f8

4 files changed

Lines changed: 63 additions & 0 deletions

File tree

lib/instances/fork.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,12 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin
219219
default:
220220
return nil, false, fmt.Errorf("%w: cannot fork from state %s (must be Stopped or Standby)", ErrInvalidState, source.State)
221221
}
222+
if stored.GPURetainedForCleanup {
223+
// A delete-only retention stub from a failed create has no boot
224+
// configuration, so a fork of it could never boot. Delete the stub to
225+
// release its retained vGPU assignment.
226+
return nil, false, fmt.Errorf("%w: instance retains a vGPU assignment from a failed create and has no boot configuration; delete it to release the assignment", ErrInvalidState)
227+
}
222228

223229
if !supportValidated {
224230
if err := m.validateForkSupport(ctx, stored.HypervisorType); err != nil {

lib/instances/fork_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ func TestForkInstanceClearsVGPUAssignment(t *testing.T) {
6363
assert.Equal(t, "/sys/bus/pci/devices/0000:82:00.4", source.GPUDevicePath)
6464
}
6565

66+
func TestForkInstanceRejectsVGPURetentionRecord(t *testing.T) {
67+
manager, _ := setupTestManager(t)
68+
ctx := context.Background()
69+
hvType := hypervisor.Type("fork-vgpu-retention-test")
70+
hypervisor.RegisterCapabilities(hvType, hypervisor.Capabilities{SupportsConcurrentForkPrepare: true})
71+
manager.vmStarters[hvType] = concurrentForkPrepareTestStarter{}
72+
73+
sourceID := "fork-vgpu-retention-source"
74+
createStoppedSnapshotSourceFixture(t, manager, sourceID, sourceID, hvType)
75+
76+
meta, err := manager.loadMetadata(sourceID)
77+
require.NoError(t, err)
78+
meta.GPUProfile = "NVIDIA L40S-2Q"
79+
meta.GPUFramework = devices.VGPUFramework("future-framework")
80+
meta.GPUDevicePath = "/sys/bus/pci/devices/0000:82:00.4"
81+
meta.GPURetainedForCleanup = true
82+
require.NoError(t, manager.saveMetadata(meta))
83+
84+
// The delete-only retention stub has no boot configuration, so a fork of
85+
// it could never boot; only delete may act on it.
86+
_, err = manager.ForkInstance(ctx, sourceID, ForkInstanceRequest{Name: "fork-vgpu-retention-copy"})
87+
require.ErrorIs(t, err, ErrInvalidState)
88+
require.ErrorContains(t, err, "delete it to release the assignment")
89+
}
90+
6691
func TestForkInstance_VZStoppedSourceSupported(t *testing.T) {
6792
t.Parallel()
6893
manager, _ := setupTestManager(t)

lib/instances/snapshot.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps
6666
inst := m.toInstance(ctx, meta)
6767
stored := &meta.StoredMetadata
6868

69+
if stored.GPURetainedForCleanup {
70+
// A delete-only retention stub from a failed create has no boot
71+
// configuration, so a snapshot of it could never be restored or
72+
// forked into a bootable instance. Delete the stub to release its
73+
// retained vGPU assignment.
74+
return nil, fmt.Errorf("%w: instance retains a vGPU assignment from a failed create and has no boot configuration; delete it to release the assignment", ErrInvalidState)
75+
}
6976
if err := validateForkVolumeSafety(stored.Volumes); err != nil {
7077
return nil, fmt.Errorf("%w: snapshot requires readonly volume attachments: %v", ErrNotSupported, err)
7178
}

lib/instances/snapshot_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ func TestForkSnapshotClearsVGPUAssignment(t *testing.T) {
5252
assert.Equal(t, "/sys/bus/pci/devices/0000:82:00.4", source.GPUDevicePath)
5353
}
5454

55+
func TestCreateSnapshotRejectsVGPURetentionRecord(t *testing.T) {
56+
mgr, _ := setupTestManager(t)
57+
ctx := context.Background()
58+
59+
sourceID := "snapshot-vgpu-retention"
60+
createStoppedSnapshotSourceFixture(t, mgr, sourceID, sourceID, mgr.defaultHypervisor)
61+
62+
meta, err := mgr.loadMetadata(sourceID)
63+
require.NoError(t, err)
64+
meta.GPUProfile = "NVIDIA L40S-2Q"
65+
meta.GPUFramework = devices.VGPUFramework("future-framework")
66+
meta.GPUDevicePath = "/sys/bus/pci/devices/0000:82:00.4"
67+
meta.GPURetainedForCleanup = true
68+
require.NoError(t, mgr.saveMetadata(meta))
69+
70+
// The delete-only retention stub has no boot configuration, so a snapshot
71+
// of it could never be restored or forked into a bootable instance.
72+
_, err = mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{
73+
Kind: SnapshotKindStopped,
74+
Name: "snapshot-vgpu-retention",
75+
})
76+
require.ErrorIs(t, err, ErrInvalidState)
77+
require.ErrorContains(t, err, "delete it to release the assignment")
78+
}
79+
5580
func TestRestoreSnapshotDoesNotResurrectStaleVGPUAssignment(t *testing.T) {
5681
mgr, _ := setupTestManager(t)
5782
ctx := context.Background()

0 commit comments

Comments
 (0)