@@ -65,36 +65,34 @@ func (fc *Firecracker) cloneSetup(ctx context.Context, vmID string, vmCfg *types
6565func (fc * Firecracker ) cloneAfterExtract (ctx context.Context , vmID string , vmCfg * types.VMConfig , networkConfigs []* types.NetworkConfig , runDir , logDir string , now time.Time ) (* types.VM , error ) {
6666 logger := log .WithFunc ("firecracker.Clone" )
6767
68- // Rebuild storage/boot configs from the snapshot's cow.raw and existing record context.
69- // FC stores StorageConfigs on the VMRecord (not in a config.json like CH).
70- // We need to find the COW file in the snapshot and update paths.
68+ // Read snapshot metadata (cocoon.json) to reconstruct storage/boot config.
69+ // This makes the clone self-contained — no dependency on live VM records.
70+ meta , err := loadSnapshotMeta (runDir )
71+ if err != nil {
72+ return nil , fmt .Errorf ("load snapshot metadata: %w" , err )
73+ }
74+
7175 cowPath := fc .conf .COWRawPath (vmID )
7276 snapshotCOW := filepath .Join (runDir , cowFileName )
73-
74- // Move the extracted COW to its canonical location.
75- if err := os .Rename (snapshotCOW , cowPath ); err != nil {
76- return nil , fmt .Errorf ("move COW to canonical path: %w" , err )
77+ if renameErr := os .Rename (snapshotCOW , cowPath ); renameErr != nil {
78+ return nil , fmt .Errorf ("move COW to canonical path: %w" , renameErr )
7779 }
7880
79- // Rebuild storage configs: read-only layers from snapshot config (via blob IDs),
80- // plus the new COW disk.
81- storageConfigs , bootCfg , blobIDs , err := fc .rebuildFromSnapshot (ctx , vmID , vmCfg , cowPath )
82- if err != nil {
83- return nil , fmt .Errorf ("rebuild from snapshot: %w" , err )
84- }
81+ // Rebuild storage configs: reuse layer paths from metadata, update COW path.
82+ storageConfigs := rebuildCloneStorage (meta , cowPath )
83+ bootCfg := meta .BootConfig
84+ blobIDs := hypervisor .ExtractBlobIDs (storageConfigs , bootCfg )
8585
8686 if verifyErr := verifyBaseFiles (storageConfigs , bootCfg ); verifyErr != nil {
8787 return nil , fmt .Errorf ("verify base files: %w" , verifyErr )
8888 }
8989
90- // Expand COW if vmCfg requests larger storage.
9190 if vmCfg .Storage > 0 {
9291 if expandErr := expandRawImage (cowPath , vmCfg .Storage ); expandErr != nil {
9392 return nil , fmt .Errorf ("resize COW: %w" , expandErr )
9493 }
9594 }
9695
97- // Update bootCfg.Cmdline for the new clone (new VM name, IP, DNS).
9896 if bootCfg != nil {
9997 dns , dnsErr := fc .conf .DNSServers ()
10098 if dnsErr != nil {
@@ -103,9 +101,13 @@ func (fc *Firecracker) cloneAfterExtract(ctx context.Context, vmID string, vmCfg
103101 bootCfg .Cmdline = buildCmdline (storageConfigs , networkConfigs , vmCfg .Name , dns )
104102 }
105103
106- // Launch FC process, load snapshot, configure drives, resume.
107- sockPath := hypervisor .SocketPath (runDir )
104+ // FC snapshot/load requires drives at the same paths as the source.
105+ // Read-only layers are shared blobs (same path). COW changed path.
106+ // Create a temp symlink from the source COW path -> clone COW so load succeeds.
107+ symlinks := createDriveSymlinks (meta .StorageConfigs , storageConfigs )
108+ defer cleanupSymlinks (symlinks )
108109
110+ sockPath := hypervisor .SocketPath (runDir )
109111 withNetwork := len (networkConfigs ) > 0
110112 pid , err := fc .launchProcess (ctx , & hypervisor.VMRecord {
111113 VM : types.VM {NetworkConfigs : networkConfigs },
@@ -121,16 +123,10 @@ func (fc *Firecracker) cloneAfterExtract(ctx context.Context, vmID string, vmCfg
121123 return nil , err
122124 }
123125
124- // Finalize record -> Running.
125126 info := types.VM {
126- ID : vmID ,
127- State : types .VMStateRunning ,
128- Config : * vmCfg ,
129- StorageConfigs : storageConfigs ,
130- NetworkConfigs : networkConfigs ,
131- CreatedAt : now ,
132- UpdatedAt : now ,
133- StartedAt : & now ,
127+ ID : vmID , State : types .VMStateRunning ,
128+ Config : * vmCfg , StorageConfigs : storageConfigs , NetworkConfigs : networkConfigs ,
129+ CreatedAt : now , UpdatedAt : now , StartedAt : & now ,
134130 }
135131 if err := fc .DB .Update (ctx , func (idx * hypervisor.VMIndex ) error {
136132 r := idx .VMs [vmID ]
@@ -169,13 +165,9 @@ func (fc *Firecracker) restoreAndResumeClone(
169165 return fmt .Errorf ("snapshot/load: %w" , err )
170166 }
171167
172- // Re-configure drives after snapshot load.
173- // FC snapshot/load does NOT preserve drive config; drives must be re-attached.
174168 if err = fc .reconfigureDrives (ctx , hc , storageConfigs ); err != nil {
175169 return fmt .Errorf ("reconfigure drives: %w" , err )
176170 }
177-
178- // Re-configure network interfaces for the clone (new TAP devices, new MACs).
179171 if err = fc .reconfigureNetworks (ctx , hc , networkConfigs ); err != nil {
180172 return fmt .Errorf ("reconfigure networks: %w" , err )
181173 }
@@ -186,56 +178,50 @@ func (fc *Firecracker) restoreAndResumeClone(
186178 return nil
187179}
188180
189- // rebuildFromSnapshot reconstructs StorageConfigs, BootConfig, and blob IDs
190- // from the VM's image (looked up via vmCfg.Image) plus the new COW path.
191- // FC only supports OCI (direct boot), so we always have a kernel+initrd+layers.
192- func (fc * Firecracker ) rebuildFromSnapshot (ctx context.Context , _ string , vmCfg * types.VMConfig , cowPath string ) ([]* types.StorageConfig , * types.BootConfig , map [string ]struct {}, error ) {
193- // Look up the original VM that was snapshotted to find its storage layout.
194- // For clone, the snapshot already carried the COW; we need the read-only layers
195- // which are shared blobs on disk (referenced by the image).
196- // The caller (cmd layer) already resolved the image and passed storageConfigs
197- // via snapshotConfig.ImageBlobIDs. We reconstruct from the index.
198-
199- // Search for any existing VM with the same image to get layer paths.
200- // This is a fallback; the primary path is through the image resolver at the cmd layer.
201- var storageConfigs []* types.StorageConfig
202- var bootCfg * types.BootConfig
203-
204- if err := fc .DB .With (ctx , func (idx * hypervisor.VMIndex ) error {
205- for _ , rec := range idx .VMs {
206- if rec == nil || rec .Config .Image != vmCfg .Image {
207- continue
208- }
209- // Found a VM with the same image; reuse its read-only layers and boot config.
210- for _ , sc := range rec .StorageConfigs {
211- if sc .RO {
212- storageConfigs = append (storageConfigs , & types.StorageConfig {
213- Path : sc .Path ,
214- RO : true ,
215- Serial : sc .Serial ,
216- })
181+ // rebuildCloneStorage creates new StorageConfigs from snapshot metadata,
182+ // keeping read-only layer paths unchanged and updating the COW path.
183+ func rebuildCloneStorage (meta * snapshotMeta , cowPath string ) []* types.StorageConfig {
184+ var configs []* types.StorageConfig
185+ for _ , sc := range meta .StorageConfigs {
186+ if sc .RO {
187+ configs = append (configs , & types.StorageConfig {Path : sc .Path , RO : true , Serial : sc .Serial })
188+ }
189+ }
190+ configs = append (configs , & types.StorageConfig {Path : cowPath , RO : false , Serial : CowSerial })
191+ return configs
192+ }
193+
194+ // createDriveSymlinks creates temporary symlinks from source drive paths to
195+ // clone drive paths so FC snapshot/load can find drives at their original locations.
196+ // Only creates symlinks for paths that actually changed (i.e., COW disk).
197+ func createDriveSymlinks (srcConfigs , dstConfigs []* types.StorageConfig ) []string {
198+ var symlinks []string
199+ dstPaths := make (map [string ]string ) // srcPath → dstPath
200+ for i , src := range srcConfigs {
201+ if i < len (dstConfigs ) && src .Path != dstConfigs [i ].Path {
202+ dstPaths [src .Path ] = dstConfigs [i ].Path
203+ }
204+ }
205+ for srcPath , dstPath := range dstPaths {
206+ // Only create if source path doesn't already exist (avoid overwriting real files).
207+ if _ , err := os .Stat (srcPath ); err != nil {
208+ // Ensure parent directory exists for the symlink.
209+ if mkErr := os .MkdirAll (filepath .Dir (srcPath ), 0o700 ); mkErr == nil {
210+ if linkErr := os .Symlink (dstPath , srcPath ); linkErr == nil {
211+ symlinks = append (symlinks , srcPath )
217212 }
218213 }
219- if rec .BootConfig != nil {
220- b := * rec .BootConfig
221- bootCfg = & b
222- }
223- return nil
224214 }
225- return fmt .Errorf ("no VM with image %q found for layer reference" , vmCfg .Image )
226- }); err != nil {
227- return nil , nil , nil , err
228215 }
216+ return symlinks
217+ }
229218
230- // Append the new COW disk.
231- storageConfigs = append (storageConfigs , & types.StorageConfig {
232- Path : cowPath ,
233- RO : false ,
234- Serial : CowSerial ,
235- })
236-
237- blobIDs := hypervisor .ExtractBlobIDs (storageConfigs , bootCfg )
238- return storageConfigs , bootCfg , blobIDs , nil
219+ func cleanupSymlinks (paths []string ) {
220+ for _ , p := range paths {
221+ _ = os .Remove (p )
222+ // Clean up parent dir if it was created for the symlink (best-effort).
223+ _ = os .Remove (filepath .Dir (p ))
224+ }
239225}
240226
241227// reconfigureDrives re-attaches drives after FC snapshot/load.
0 commit comments