@@ -3,6 +3,7 @@ package qemu
33
44import (
55 "context"
6+ "encoding/json"
67 "fmt"
78 "net"
89 "os"
@@ -168,15 +169,145 @@ func (s *Starter) StartVM(ctx context.Context, p *paths.Paths, version string, s
168169 return 0 , nil , fmt .Errorf ("create client: %w" , err )
169170 }
170171
172+ // Save config for potential restore later
173+ // QEMU migration files only contain memory state, not device config
174+ if err := saveVMConfig (instanceDir , config ); err != nil {
175+ // Non-fatal - restore just won't work
176+ // Log would be nice but we don't have logger here
177+ }
178+
171179 // Success - release cleanup to prevent killing the process
172180 cu .Release ()
173181 return pid , hv , nil
174182}
175183
176184// RestoreVM starts QEMU and restores VM state from a snapshot.
177- // Not yet implemented for QEMU .
185+ // The VM is in paused state after restore; caller should call Resume() to continue execution .
178186func (s * Starter ) RestoreVM (ctx context.Context , p * paths.Paths , version string , socketPath string , snapshotPath string ) (int , hypervisor.Hypervisor , error ) {
179- return 0 , nil , fmt .Errorf ("restore not supported by QEMU implementation" )
187+ // Get binary path
188+ binaryPath , err := s .GetBinaryPath (p , version )
189+ if err != nil {
190+ return 0 , nil , fmt .Errorf ("get binary: %w" , err )
191+ }
192+
193+ // Check if socket is already in use
194+ if isSocketInUse (socketPath ) {
195+ return 0 , nil , fmt .Errorf ("socket already in use, QEMU may be running at %s" , socketPath )
196+ }
197+
198+ // Remove stale socket if exists
199+ os .Remove (socketPath )
200+
201+ // Load saved VM config from snapshot directory
202+ // QEMU requires exact same command-line args as when snapshot was taken
203+ config , err := loadVMConfig (snapshotPath )
204+ if err != nil {
205+ return 0 , nil , fmt .Errorf ("load vm config from snapshot: %w" , err )
206+ }
207+
208+ instanceDir := filepath .Dir (socketPath )
209+
210+ // Build command arguments: QMP socket + VM configuration + incoming migration
211+ args := []string {
212+ "-chardev" , fmt .Sprintf ("socket,id=qmp,path=%s,server=on,wait=off" , socketPath ),
213+ "-mon" , "chardev=qmp,mode=control" ,
214+ }
215+ // Append VM configuration as command-line arguments
216+ args = append (args , BuildArgs (config )... )
217+
218+ // Add incoming migration flag to restore from snapshot
219+ // The snapshot file is named "memory" in the snapshot directory
220+ incomingURI := "file://" + filepath .Join (snapshotPath , "memory" )
221+ args = append (args , "-incoming" , incomingURI )
222+
223+ // Create command
224+ cmd := exec .Command (binaryPath , args ... )
225+
226+ // Daemonize: detach from parent process group
227+ cmd .SysProcAttr = & syscall.SysProcAttr {
228+ Setpgid : true ,
229+ }
230+
231+ // Redirect stdout/stderr to VMM log file
232+ logsDir := filepath .Join (instanceDir , "logs" )
233+ if err := os .MkdirAll (logsDir , 0755 ); err != nil {
234+ return 0 , nil , fmt .Errorf ("create logs directory: %w" , err )
235+ }
236+
237+ vmmLogFile , err := os .OpenFile (
238+ filepath .Join (logsDir , "vmm.log" ),
239+ os .O_CREATE | os .O_WRONLY | os .O_APPEND ,
240+ 0644 ,
241+ )
242+ if err != nil {
243+ return 0 , nil , fmt .Errorf ("create vmm log: %w" , err )
244+ }
245+ defer vmmLogFile .Close ()
246+
247+ cmd .Stdout = vmmLogFile
248+ cmd .Stderr = vmmLogFile
249+
250+ if err := cmd .Start (); err != nil {
251+ return 0 , nil , fmt .Errorf ("start qemu: %w" , err )
252+ }
253+
254+ pid := cmd .Process .Pid
255+
256+ // Setup cleanup to kill the process if subsequent steps fail
257+ cu := cleanup .Make (func () {
258+ syscall .Kill (pid , syscall .SIGKILL )
259+ })
260+ defer cu .Clean ()
261+
262+ // Wait for socket to be ready
263+ if err := waitForSocket (socketPath , 10 * time .Second ); err != nil {
264+ vmmLogPath := filepath .Join (logsDir , "vmm.log" )
265+ if logData , readErr := os .ReadFile (vmmLogPath ); readErr == nil && len (logData ) > 0 {
266+ return 0 , nil , fmt .Errorf ("%w; vmm.log: %s" , err , string (logData ))
267+ }
268+ return 0 , nil , err
269+ }
270+
271+ // Create QMP client
272+ hv , err := New (socketPath )
273+ if err != nil {
274+ return 0 , nil , fmt .Errorf ("create client: %w" , err )
275+ }
276+
277+ // Success - release cleanup to prevent killing the process
278+ cu .Release ()
279+ return pid , hv , nil
280+ }
281+
282+ // vmConfigFile is the name of the file where VM config is saved for restore.
283+ const vmConfigFile = "qemu-config.json"
284+
285+ // saveVMConfig saves the VM configuration to a file in the instance directory.
286+ // This is needed for QEMU restore since migration files only contain memory state.
287+ func saveVMConfig (instanceDir string , config hypervisor.VMConfig ) error {
288+ configPath := filepath .Join (instanceDir , vmConfigFile )
289+ data , err := json .MarshalIndent (config , "" , " " )
290+ if err != nil {
291+ return fmt .Errorf ("marshal config: %w" , err )
292+ }
293+ if err := os .WriteFile (configPath , data , 0644 ); err != nil {
294+ return fmt .Errorf ("write config: %w" , err )
295+ }
296+ return nil
297+ }
298+
299+ // loadVMConfig loads the VM configuration from the instance directory.
300+ func loadVMConfig (instanceDir string ) (hypervisor.VMConfig , error ) {
301+ configPath := filepath .Join (instanceDir , vmConfigFile )
302+ data , err := os .ReadFile (configPath )
303+ if err != nil {
304+ return hypervisor.VMConfig {}, fmt .Errorf ("read config: %w" , err )
305+ }
306+ var config hypervisor.VMConfig
307+ if err := json .Unmarshal (data , & config ); err != nil {
308+ return hypervisor.VMConfig {}, fmt .Errorf ("unmarshal config: %w" , err )
309+ }
310+ return config , nil
180311}
181312
182313// qemuBinaryName returns the QEMU binary name for the host architecture.
0 commit comments