Skip to content
This repository was archived by the owner on Jan 2, 2026. It is now read-only.

Commit e4bbc14

Browse files
alexisbouchezclaude
andcommitted
fix: patch snapshot config paths and optimize copy with hard links
- Add patchSnapshotConfig() to update disk and tap device paths in config.json before restore (fixes "No such file or directory" errors when restoring) - Change copyDir to copyDirWithLinks to use hard links for large snapshot files (memory-ranges is 512MB), falling back to copy if cross-device - config.json is always copied (not linked) since we need to modify it This enables snapshot restore to work across different instances by updating the hardcoded device paths in the CloudHypervisor snapshot. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c38ed96 commit e4bbc14

File tree

1 file changed

+83
-7
lines changed

1 file changed

+83
-7
lines changed

runtime/drivers/vm/vm.go

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package vm
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"log/slog"
78
"os"
@@ -103,10 +104,15 @@ func (vm *vm) StartFromSnapshot(ctx context.Context, globalSnapshotPath, jailSna
103104

104105
// Copy snapshot from global storage into the jail
105106
jailHostPath := getInstanceDir(vm.id) + jailSnapshotPath
106-
if err := copyDir(globalSnapshotPath, jailHostPath); err != nil {
107+
if err := copyDirWithLinks(globalSnapshotPath, jailHostPath); err != nil {
107108
return fmt.Errorf("failed to copy snapshot to jail: %w", err)
108109
}
109110

111+
// Patch config.json with current device paths (rootfs, tap device)
112+
if err := vm.patchSnapshotConfig(jailHostPath); err != nil {
113+
return fmt.Errorf("failed to patch snapshot config: %w", err)
114+
}
115+
110116
// Chown to ravel-jailer user so CloudHypervisor can read
111117
jailerUid, jailerGid, err := setupRavelJailerUser()
112118
if err != nil {
@@ -116,7 +122,7 @@ func (vm *vm) StartFromSnapshot(ctx context.Context, globalSnapshotPath, jailSna
116122
return fmt.Errorf("failed to chown snapshot directory: %w", err)
117123
}
118124

119-
slog.Debug("snapshot copied to jail", "from", globalSnapshotPath, "to", jailHostPath)
125+
slog.Debug("snapshot copied and patched", "from", globalSnapshotPath, "to", jailHostPath)
120126

121127
err = vm.cmd.Start()
122128
if err != nil {
@@ -166,8 +172,66 @@ func (vm *vm) StartFromSnapshot(ctx context.Context, globalSnapshotPath, jailSna
166172
return nil
167173
}
168174

169-
// copyDir copies a directory from src to dst
170-
func copyDir(src, dst string) error {
175+
// patchSnapshotConfig updates the snapshot config.json with current device paths
176+
func (vm *vm) patchSnapshotConfig(snapshotPath string) error {
177+
configPath := snapshotPath + "/config.json"
178+
179+
// Read the config
180+
data, err := os.ReadFile(configPath)
181+
if err != nil {
182+
return fmt.Errorf("failed to read config.json: %w", err)
183+
}
184+
185+
// Parse as generic JSON to preserve structure
186+
var config map[string]interface{}
187+
if err := json.Unmarshal(data, &config); err != nil {
188+
return fmt.Errorf("failed to parse config.json: %w", err)
189+
}
190+
191+
// Update disk path - get the current rootfs from vmConfig
192+
if disks, ok := config["disks"].([]interface{}); ok && len(disks) > 0 {
193+
if disk, ok := disks[0].(map[string]interface{}); ok {
194+
// Get the new rootfs path from vmConfig
195+
if vm.vmConfig.Disks != nil && len(*vm.vmConfig.Disks) > 0 {
196+
newRootfs := (*vm.vmConfig.Disks)[0].Path
197+
oldRootfs := disk["path"]
198+
disk["path"] = newRootfs
199+
slog.Debug("patched rootfs path", "old", oldRootfs, "new", newRootfs)
200+
}
201+
}
202+
}
203+
204+
// Update tap device name
205+
if nets, ok := config["net"].([]interface{}); ok && len(nets) > 0 {
206+
if net, ok := nets[0].(map[string]interface{}); ok {
207+
// Get the new tap device from vmConfig
208+
if vm.vmConfig.Net != nil && len(*vm.vmConfig.Net) > 0 {
209+
newTap := (*vm.vmConfig.Net)[0].Tap
210+
if newTap != nil {
211+
oldTap := net["tap"]
212+
net["tap"] = *newTap
213+
slog.Debug("patched tap device", "old", oldTap, "new", *newTap)
214+
}
215+
}
216+
}
217+
}
218+
219+
// Write back the modified config
220+
newData, err := json.MarshalIndent(config, "", " ")
221+
if err != nil {
222+
return fmt.Errorf("failed to marshal config.json: %w", err)
223+
}
224+
225+
if err := os.WriteFile(configPath, newData, 0600); err != nil {
226+
return fmt.Errorf("failed to write config.json: %w", err)
227+
}
228+
229+
return nil
230+
}
231+
232+
// copyDirWithLinks copies a directory from src to dst, using hard links for large files
233+
// config.json is always copied (not linked) since we need to modify it
234+
func copyDirWithLinks(src, dst string) error {
171235
if err := os.MkdirAll(dst, 0755); err != nil {
172236
return err
173237
}
@@ -182,12 +246,24 @@ func copyDir(src, dst string) error {
182246
dstPath := dst + "/" + entry.Name()
183247

184248
if entry.IsDir() {
185-
if err := copyDir(srcPath, dstPath); err != nil {
249+
if err := copyDirWithLinks(srcPath, dstPath); err != nil {
186250
return err
187251
}
188252
} else {
189-
if err := copyFile(srcPath, dstPath); err != nil {
190-
return err
253+
// Always copy config.json since we need to modify it
254+
if entry.Name() == "config.json" {
255+
if err := copyFile(srcPath, dstPath); err != nil {
256+
return err
257+
}
258+
continue
259+
}
260+
261+
// Try hard link first (instant, no copy), fall back to copy
262+
if err := os.Link(srcPath, dstPath); err != nil {
263+
// Hard link failed (maybe cross-device), fall back to copy
264+
if err := copyFile(srcPath, dstPath); err != nil {
265+
return err
266+
}
191267
}
192268
}
193269
}

0 commit comments

Comments
 (0)