diff --git a/cmd/api/api/snapshots.go b/cmd/api/api/snapshots.go index 620d9574..98f1b335 100644 --- a/cmd/api/api/snapshots.go +++ b/cmd/api/api/snapshots.go @@ -161,6 +161,8 @@ func (s *ApiService) DeleteSnapshot(ctx context.Context, request oapi.DeleteSnap switch { case errors.Is(err, instances.ErrSnapshotNotFound): return oapi.DeleteSnapshot404JSONResponse{Code: "not_found", Message: "snapshot not found"}, nil + case errors.Is(err, instances.ErrInvalidState): + return oapi.DeleteSnapshot409JSONResponse{Code: "conflict", Message: err.Error()}, nil default: log.ErrorContext(ctx, "failed to delete snapshot", "error", err) return oapi.DeleteSnapshot500JSONResponse{Code: "internal_error", Message: "failed to delete snapshot"}, nil @@ -334,6 +336,7 @@ func snapshotToOAPI(snapshot instances.Snapshot) oapi.Snapshot { SourceHypervisor: sourceHypervisor, CreatedAt: snapshot.CreatedAt, SizeBytes: snapshot.SizeBytes, + RefCount: snapshot.RefCount, Name: lo.ToPtr(snapshot.Name), } if snapshot.CompressionState != "" { diff --git a/lib/forkvm/copy.go b/lib/forkvm/copy.go index fda4a48f..78cd245a 100644 --- a/lib/forkvm/copy.go +++ b/lib/forkvm/copy.go @@ -33,12 +33,23 @@ type copyState struct { reflinkDead bool } +// CopyOptions tunes CopyGuestDirectory behavior. +type CopyOptions struct { + // SkipRelPaths lists exact relative paths under srcDir to skip. Paths use + // forward slashes regardless of platform. + SkipRelPaths []string +} + // CopyGuestDirectory recursively copies a guest directory to a new destination. // Regular files are cloned via reflink (FICLONE) when the underlying filesystem // supports it; otherwise we fall back to a sparse extent copy // (SEEK_DATA/SEEK_HOLE). Runtime sockets and logs are skipped because they are // host-runtime artifacts. func CopyGuestDirectory(srcDir, dstDir string) error { + return CopyGuestDirectoryWithOptions(srcDir, dstDir, CopyOptions{}) +} + +func CopyGuestDirectoryWithOptions(srcDir, dstDir string, opts CopyOptions) error { srcInfo, err := os.Stat(srcDir) if err != nil { return fmt.Errorf("stat source directory: %w", err) @@ -56,6 +67,11 @@ func CopyGuestDirectory(srcDir, dstDir string) error { state.reflinkDead = true } + skipSet := make(map[string]struct{}, len(opts.SkipRelPaths)) + for _, relPath := range opts.SkipRelPaths { + skipSet[filepath.ToSlash(relPath)] = struct{}{} + } + return filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr @@ -68,6 +84,12 @@ func CopyGuestDirectory(srcDir, dstDir string) error { if relPath == "." { return nil } + if _, skip := skipSet[filepath.ToSlash(relPath)]; skip { + if d.IsDir() { + return filepath.SkipDir + } + return nil + } if d.IsDir() && shouldSkipDirectory(relPath) { return filepath.SkipDir } diff --git a/lib/forkvm/copy_test.go b/lib/forkvm/copy_test.go index c71f6c4e..bc2e476b 100644 --- a/lib/forkvm/copy_test.go +++ b/lib/forkvm/copy_test.go @@ -57,3 +57,19 @@ func TestCopyGuestDirectory_DoesNotSkipTmpSuffixedDirectories(t *testing.T) { assert.DirExists(t, filepath.Join(dst, "snapshots", "snapshot-latest", "memory-ranges.lz4.tmp")) assert.FileExists(t, filepath.Join(dst, "snapshots", "snapshot-latest", "memory-ranges.lz4.tmp", "nested.txt")) } + +func TestCopyGuestDirectoryWithOptions_SkipsRelativePaths(t *testing.T) { + src := filepath.Join(t.TempDir(), "src") + dst := filepath.Join(t.TempDir(), "dst") + + require.NoError(t, os.MkdirAll(filepath.Join(src, "snapshots", "snapshot-latest"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(src, "overlay.raw"), []byte("overlay"), 0644)) + require.NoError(t, os.WriteFile(filepath.Join(src, "snapshots", "snapshot-latest", "memory"), []byte("memory"), 0644)) + + require.NoError(t, CopyGuestDirectoryWithOptions(src, dst, CopyOptions{ + SkipRelPaths: []string{"snapshots/snapshot-latest/memory"}, + })) + + assert.FileExists(t, filepath.Join(dst, "overlay.raw")) + assert.NoFileExists(t, filepath.Join(dst, "snapshots", "snapshot-latest", "memory")) +} diff --git a/lib/instances/fork.go b/lib/instances/fork.go index c29d7189..19faf56e 100644 --- a/lib/instances/fork.go +++ b/lib/instances/fork.go @@ -283,6 +283,7 @@ func (m *manager) forkInstanceFromStoppedOrStandby(ctx context.Context, id strin forkMeta.ExitCode = nil forkMeta.ExitMessage = "" forkMeta.RestartStatus = restartpolicy.Status{} + forkMeta.ForkOfSnapshot = "" // Forks are new instances; phase accounting must not inherit the source's // cumulative durations. The first transition into the fork's runtime // phase (Standby for snapshot forks, Stopped for stopped forks) will be diff --git a/lib/instances/snapshot.go b/lib/instances/snapshot.go index 05d8084e..94762a83 100644 --- a/lib/instances/snapshot.go +++ b/lib/instances/snapshot.go @@ -2,9 +2,11 @@ package instances import ( "context" + "encoding/json" "errors" "fmt" "os" + "path/filepath" "time" "github.com/kernel/hypeman/lib/forkvm" @@ -30,6 +32,13 @@ func (m *manager) listSnapshots(ctx context.Context, filter *ListSnapshotsFilter if err != nil { return nil, fmt.Errorf("list snapshots: %w", err) } + counts, err := m.snapshotForkCounts() + if err != nil { + return nil, err + } + for i := range snapshots { + snapshots[i].RefCount = counts[snapshots[i].Id] + } return snapshots, nil } @@ -42,6 +51,11 @@ func (m *manager) getSnapshot(ctx context.Context, snapshotID string) (*Snapshot } return nil, err } + refCount, err := m.countSnapshotForks(snapshotID) + if err != nil { + return nil, err + } + snapshot.RefCount = refCount return snapshot, nil } @@ -140,6 +154,8 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps return nil, copyErr } + snapshotMeta := cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata) + snapshotMeta.ForkOfSnapshot = "" rec := &snapshotRecord{ Snapshot: Snapshot{ Id: snapshotID, @@ -151,7 +167,7 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps SourceHypervisor: stored.HypervisorType, CreatedAt: time.Now(), }, - StoredMetadata: cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata), + StoredMetadata: snapshotMeta, } sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) if err != nil { @@ -192,6 +208,8 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps if err := m.copySnapshotPayload(id, snapshotGuestDir); err != nil { return nil, err } + snapshotMeta := cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata) + snapshotMeta.ForkOfSnapshot = "" rec := &snapshotRecord{ Snapshot: Snapshot{ Id: snapshotID, @@ -203,7 +221,7 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps SourceHypervisor: stored.HypervisorType, CreatedAt: time.Now(), }, - StoredMetadata: cloneStoredMetadataWithoutPendingStandbyCompression(meta.StoredMetadata), + StoredMetadata: snapshotMeta, } sizeBytes, err := snapshotstore.DirectoryFileSize(snapshotGuestDir) if err != nil { @@ -224,6 +242,20 @@ func (m *manager) createSnapshot(ctx context.Context, id string, req CreateSnaps } func (m *manager) deleteSnapshot(ctx context.Context, snapshotID string) error { + if _, err := m.snapshotStore().Get(snapshotID); err != nil { + if errors.Is(err, snapshotstore.ErrNotFound) { + return ErrSnapshotNotFound + } + return err + } + forks, err := m.countSnapshotForks(snapshotID) + if err != nil { + return err + } + if forks > 0 { + return fmt.Errorf("%w: cannot delete snapshot %s with %d fork(s)", ErrInvalidState, snapshotID, forks) + } + target, err := m.cancelAndWaitCompressionJob(ctx, m.snapshotJobKeyForSnapshot(snapshotID)) if err != nil { return fmt.Errorf("wait for snapshot compression to stop: %w", err) @@ -303,6 +335,7 @@ func (m *manager) restoreSnapshot(ctx context.Context, id string, snapshotID str restored.ExitCode = nil restored.ExitMessage = "" restored.HypervisorType = targetHypervisor + restored.ForkOfSnapshot = "" starter, err := m.getVMStarter(targetHypervisor) if err != nil { @@ -416,12 +449,23 @@ func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkS return nil, fmt.Errorf("prepare snapshot memory for fork: %w", err) } - if err := forkvm.CopyGuestDirectory(m.paths.SnapshotGuestDir(snapshotID), dstDir); err != nil { + srcDir := m.paths.SnapshotGuestDir(snapshotID) + copyOpts := forkvm.CopyOptions{} + srcMemPath, srcMemRel, hasSharedMem := snapshotMemHardlinkSource(srcDir) + if hasSharedMem { + copyOpts.SkipRelPaths = []string{srcMemRel} + } + if err := forkvm.CopyGuestDirectoryWithOptions(srcDir, dstDir, copyOpts); err != nil { if errors.Is(err, forkvm.ErrSparseCopyUnsupported) { return nil, fmt.Errorf("fork from snapshot requires sparse-capable filesystem (SEEK_DATA/SEEK_HOLE unsupported): %w", err) } return nil, fmt.Errorf("clone snapshot payload: %w", err) } + if hasSharedMem { + if err := installForkSnapshotMemHardlink(srcMemPath, dstDir, srcMemRel); err != nil { + return nil, fmt.Errorf("hardlink snapshot mem-file into fork: %w", err) + } + } starter, err := m.getVMStarter(targetHypervisor) if err != nil { @@ -450,6 +494,7 @@ func (m *manager) forkSnapshot(ctx context.Context, snapshotID string, req ForkS forkMeta.ExitCode = nil forkMeta.ExitMessage = "" forkMeta.RestartStatus = restartpolicy.Status{} + forkMeta.ForkOfSnapshot = snapshotID if rec.Snapshot.Kind == SnapshotKindStandby { forkMeta.VsockCID = rec.StoredMetadata.VsockCID } else { @@ -644,3 +689,58 @@ func (m *manager) listSnapshotRecords() ([]snapshotRecord, error) { } return records, nil } + +func (m *manager) countSnapshotForks(snapshotID string) (int, error) { + counts, err := m.snapshotForkCounts() + if err != nil { + return 0, err + } + return counts[snapshotID], nil +} + +func (m *manager) snapshotForkCounts() (map[string]int, error) { + metaFiles, err := m.listMetadataFiles() + if err != nil { + return nil, err + } + + counts := make(map[string]int) + for _, metaPath := range metaFiles { + content, err := os.ReadFile(metaPath) + if err != nil { + return nil, fmt.Errorf("read instance metadata %s: %w", metaPath, err) + } + var meta metadata + if err := json.Unmarshal(content, &meta); err != nil { + return nil, fmt.Errorf("unmarshal instance metadata %s: %w", metaPath, err) + } + if meta.ForkOfSnapshot != "" { + counts[meta.ForkOfSnapshot]++ + } + } + return counts, nil +} + +func snapshotMemHardlinkSource(srcDir string) (absPath, relSlash string, ok bool) { + abs, found := findRawSnapshotMemoryFile(srcDir) + if !found { + return "", "", false + } + rel, err := filepath.Rel(srcDir, abs) + if err != nil { + return "", "", false + } + return abs, filepath.ToSlash(rel), true +} + +func installForkSnapshotMemHardlink(srcMemPath, dstDir, relSlash string) error { + dstMem := filepath.Join(dstDir, filepath.FromSlash(relSlash)) + if err := os.MkdirAll(filepath.Dir(dstMem), 0755); err != nil { + return fmt.Errorf("ensure fork mem-file parent dir: %w", err) + } + _ = os.Remove(dstMem) + if err := os.Link(srcMemPath, dstMem); err != nil { + return fmt.Errorf("link snapshot mem-file: %w", err) + } + return nil +} diff --git a/lib/instances/snapshot_schedule.go b/lib/instances/snapshot_schedule.go index 541f6a6d..cf579f44 100644 --- a/lib/instances/snapshot_schedule.go +++ b/lib/instances/snapshot_schedule.go @@ -327,6 +327,9 @@ func (m *manager) cleanupScheduledSnapshots(ctx context.Context, instanceID stri if errors.Is(err, ErrSnapshotNotFound) { continue } + if errors.Is(err, ErrInvalidState) { + continue + } errs = append(errs, fmt.Errorf("delete snapshot %s: %w", snapshotID, err)) } } diff --git a/lib/instances/snapshot_test.go b/lib/instances/snapshot_test.go index cc634f55..eeb0e26c 100644 --- a/lib/instances/snapshot_test.go +++ b/lib/instances/snapshot_test.go @@ -43,6 +43,7 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { got, err := mgr.GetSnapshot(ctx, snap.Id) require.NoError(t, err) require.Equal(t, snap.Id, got.Id) + require.Equal(t, 0, got.RefCount) forked, err := mgr.ForkSnapshot(ctx, snap.Id, ForkSnapshotRequest{ Name: "snapshot-stopped-fork", @@ -52,8 +53,25 @@ func TestStoppedSnapshotLifecycleAndForkAfterSourceDeletion(t *testing.T) { require.NoError(t, err) require.Equal(t, StateStopped, forked.State) require.Equal(t, hvType, forked.HypervisorType) - t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), forked.Id) }) + forkMeta, err := mgr.loadMetadata(forked.Id) + require.NoError(t, err) + require.Equal(t, snap.Id, forkMeta.ForkOfSnapshot) + + got, err = mgr.GetSnapshot(ctx, snap.Id) + require.NoError(t, err) + require.Equal(t, 1, got.RefCount) + + snapshots, err := mgr.ListSnapshots(ctx, &ListSnapshotsFilter{SourceInstanceID: &sourceID}) + require.NoError(t, err) + require.Len(t, snapshots, 1) + require.Equal(t, 1, snapshots[0].RefCount) + + err = mgr.DeleteSnapshot(ctx, snap.Id) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidState) + + require.NoError(t, mgr.DeleteInstance(ctx, forked.Id)) require.NoError(t, mgr.DeleteSnapshot(ctx, snap.Id)) _, err = mgr.GetSnapshot(ctx, snap.Id) require.Error(t, err) @@ -280,6 +298,79 @@ func TestForkSnapshotFromCompressedSourceCopiesRawMemory(t *testing.T) { assert.False(t, ok, "forked snapshot payload should not retain compressed memory artifacts from the source snapshot") } +func TestDeleteSnapshotFailsWhenForkMetadataUnreadable(t *testing.T) { + t.Parallel() + + mgr, _ := setupTestManager(t) + ctx := context.Background() + + hvType := mgr.defaultHypervisor + sourceID := "snapshot-delete-refcount-src" + createStoppedSnapshotSourceFixture(t, mgr, sourceID, "snapshot-delete-refcount-src", hvType) + + snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStopped, + Name: "delete-refcount", + }) + require.NoError(t, err) + + badID := "snapshot-delete-bad-metadata" + require.NoError(t, mgr.ensureDirectories(badID)) + require.NoError(t, os.WriteFile(mgr.paths.InstanceMetadata(badID), []byte("{"), 0644)) + + err = mgr.DeleteSnapshot(ctx, snap.Id) + require.Error(t, err) + assert.Contains(t, err.Error(), "unmarshal instance metadata") +} + +func TestForkSnapshotHardlinksRawMemoryFile(t *testing.T) { + t.Parallel() + + mgr, _ := setupTestManager(t) + ctx := context.Background() + + hvType := mgr.defaultHypervisor + sourceID := "snapshot-fork-hardlink-src" + createStandbySnapshotSourceFixture(t, mgr, sourceID, "snapshot-fork-hardlink-src", hvType) + + snap, err := mgr.CreateSnapshot(ctx, sourceID, CreateSnapshotRequest{ + Kind: SnapshotKindStandby, + Name: "standby-for-fork-hardlink", + }) + require.NoError(t, err) + + memContents := []byte("guest memory bytes for hardlink test") + snapshotDir := mgr.paths.SnapshotGuestDir(snap.Id) + snapshotConfigPath := filepath.Join(snapshotDir, "snapshots", "snapshot-latest", "config.json") + require.NoError(t, os.MkdirAll(filepath.Dir(snapshotConfigPath), 0755)) + require.NoError(t, os.WriteFile(snapshotConfigPath, []byte(`{}`), 0644)) + snapshotMem := filepath.Join(snapshotDir, "snapshots", "snapshot-latest", "memory") + require.NoError(t, os.WriteFile(snapshotMem, memContents, 0644)) + + srcInfo, err := os.Stat(snapshotMem) + require.NoError(t, err) + + forked, err := mgr.ForkSnapshot(ctx, snap.Id, ForkSnapshotRequest{ + Name: "snapshot-fork-hardlink", + TargetState: StateStandby, + }) + require.NoError(t, err) + t.Cleanup(func() { _ = mgr.DeleteInstance(context.Background(), forked.Id) }) + + forkMem := filepath.Join(mgr.paths.InstanceSnapshotLatest(forked.Id), "memory") + forkInfo, err := os.Stat(forkMem) + require.NoError(t, err) + assert.True(t, os.SameFile(srcInfo, forkInfo), "fork mem-file should share an inode with the snapshot mem-file") + + got, err := os.ReadFile(forkMem) + require.NoError(t, err) + assert.Equal(t, memContents, got) + + err = mgr.DeleteSnapshot(ctx, snap.Id) + require.Error(t, err) + assert.ErrorIs(t, err, ErrInvalidState) +} + func createStoppedSnapshotSourceFixture(t *testing.T, mgr *manager, id, name string, hvType hypervisor.Type) { t.Helper() require.NoError(t, mgr.ensureDirectories(id)) diff --git a/lib/instances/types.go b/lib/instances/types.go index 8c031fc9..ff465ee8 100644 --- a/lib/instances/types.go +++ b/lib/instances/types.go @@ -147,6 +147,9 @@ type StoredMetadata struct { // Persisted so server restarts can recover delayed or interrupted jobs. PendingStandbyCompression *PendingStandbyCompression + // Snapshot this instance was forked from, if any. + ForkOfSnapshot string + // Automatic standby policy driven by host-observed inbound TCP activity. AutoStandby *autostandby.Policy diff --git a/lib/oapi/oapi.go b/lib/oapi/oapi.go index 1c053f62..cd25b8b6 100644 --- a/lib/oapi/oapi.go +++ b/lib/oapi/oapi.go @@ -1424,6 +1424,9 @@ type Snapshot struct { // Name Optional human-readable snapshot name (unique per source instance) Name *string `json:"name"` + // RefCount Number of instances directly forked from this snapshot + RefCount int `json:"ref_count"` + // SizeBytes Total payload size in bytes SizeBytes int64 `json:"size_bytes"` @@ -6503,6 +6506,7 @@ type DeleteSnapshotResponse struct { Body []byte HTTPResponse *http.Response JSON404 *Error + JSON409 *Error JSON500 *Error } @@ -9227,6 +9231,13 @@ func ParseDeleteSnapshotResponse(rsp *http.Response) (*DeleteSnapshotResponse, e } response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -13895,6 +13906,15 @@ func (response DeleteSnapshot404JSONResponse) VisitDeleteSnapshotResponse(w http return json.NewEncoder(w).Encode(response) } +type DeleteSnapshot409JSONResponse Error + +func (response DeleteSnapshot409JSONResponse) VisitDeleteSnapshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type DeleteSnapshot500JSONResponse Error func (response DeleteSnapshot500JSONResponse) VisitDeleteSnapshotResponse(w http.ResponseWriter) error { @@ -16044,122 +16064,123 @@ var swaggerSpec = []string{ "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/lib/snapshot/types.go b/lib/snapshot/types.go index 59848aa1..c36dab6a 100644 --- a/lib/snapshot/types.go +++ b/lib/snapshot/types.go @@ -33,6 +33,7 @@ type Snapshot struct { Compression *SnapshotCompressionConfig `json:"compression,omitempty"` CompressedSizeBytes *int64 `json:"compressed_size_bytes,omitempty"` UncompressedSizeBytes *int64 `json:"uncompressed_size_bytes,omitempty"` + RefCount int `json:"-"` } // SnapshotCompressionAlgorithm defines supported compression algorithms. diff --git a/openapi.yaml b/openapi.yaml index c6bce872..beec5fd8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -711,7 +711,7 @@ components: Snapshot: type: object - required: [id, kind, source_instance_id, source_instance_name, source_hypervisor, created_at, size_bytes] + required: [id, kind, source_instance_id, source_instance_name, source_hypervisor, created_at, size_bytes, ref_count] properties: id: type: string @@ -749,6 +749,10 @@ components: format: int64 description: Total payload size in bytes example: 104857600 + ref_count: + type: integer + description: Number of instances directly forked from this snapshot + example: 3 compression_state: type: string enum: [none, compressing, compressed, error] @@ -2938,6 +2942,12 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + 409: + description: Snapshot cannot be deleted while it is referenced + content: + application/json: + schema: + $ref: "#/components/schemas/Error" 500: description: Internal server error content: