diff --git a/cloud-hypervisor/tests/integration.rs b/cloud-hypervisor/tests/integration.rs index 6bcbecddb..6e4f8c1fe 100644 --- a/cloud-hypervisor/tests/integration.rs +++ b/cloud-hypervisor/tests/integration.rs @@ -10814,16 +10814,22 @@ mod common_sequential { #[test] #[cfg(not(feature = "mshv"))] fn test_snapshot_restore_hotplug_virtiomem() { - _test_snapshot_restore(true); + _test_snapshot_restore(true, false); } #[test] #[cfg(not(feature = "mshv"))] // See issue #7437 fn test_snapshot_restore_basic() { - _test_snapshot_restore(false); + _test_snapshot_restore(false, false); } - fn _test_snapshot_restore(use_hotplug: bool) { + #[test] + #[cfg(not(feature = "mshv"))] + fn test_snapshot_restore_with_resume() { + _test_snapshot_restore(false, true); + } + + fn _test_snapshot_restore(use_hotplug: bool, use_resume_option: bool) { let disk_config = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); let guest = Guest::new(Box::new(disk_config)); let kernel_path = direct_kernel_boot_path(); @@ -10975,7 +10981,7 @@ mod common_sequential { ]) .args([ "--restore", - format!("source_url=file://{snapshot_dir}").as_str(), + format!("source_url=file://{snapshot_dir},resume={use_resume_option}").as_str(), ]) .capture_output() .spawn() @@ -11005,28 +11011,12 @@ mod common_sequential { &expected_events, &event_path_restored )); - let latest_events = [&MetaEvent { - event: "restored".to_string(), - device_id: None, - }]; - assert!(check_latest_events_exact( - &latest_events, - &event_path_restored - )); - - // Remove the snapshot dir - let _ = remove_dir_all(snapshot_dir.as_str()); - - let r = std::panic::catch_unwind(|| { - // Resume the VM - assert!(remote_command(&api_socket_restored, "resume", None)); - // There is no way that we can ensure the 'write()' to the - // event file is completed when the 'resume' request is - // returned successfully, because the 'write()' was done - // asynchronously from a different thread of Cloud - // Hypervisor (e.g. the event-monitor thread). - thread::sleep(std::time::Duration::new(1, 0)); + if use_resume_option { let latest_events = [ + &MetaEvent { + event: "restored".to_string(), + device_id: None, + }, &MetaEvent { event: "resuming".to_string(), device_id: None, @@ -11040,6 +11030,49 @@ mod common_sequential { &latest_events, &event_path_restored )); + } else { + let latest_events = [&MetaEvent { + event: "restored".to_string(), + device_id: None, + }]; + assert!(check_latest_events_exact( + &latest_events, + &event_path_restored + )); + } + + // Remove the snapshot dir + let _ = remove_dir_all(snapshot_dir.as_str()); + + let r = std::panic::catch_unwind(|| { + if use_resume_option { + // VM was automatically resumed via restore option, just wait for events + thread::sleep(std::time::Duration::new(1, 0)); + } else { + // Resume the VM manually + assert!(remote_command(&api_socket_restored, "resume", None)); + // There is no way that we can ensure the 'write()' to the + // event file is completed when the 'resume' request is + // returned successfully, because the 'write()' was done + // asynchronously from a different thread of Cloud + // Hypervisor (e.g. the event-monitor thread). + thread::sleep(std::time::Duration::new(1, 0)); + + let latest_events = [ + &MetaEvent { + event: "resuming".to_string(), + device_id: None, + }, + &MetaEvent { + event: "resumed".to_string(), + device_id: None, + }, + ]; + assert!(check_latest_events_exact( + &latest_events, + &event_path_restored + )); + } // Perform same checks to validate VM has been properly restored assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); diff --git a/docs/snapshot_restore.md b/docs/snapshot_restore.md index 2cf8eda5a..567f77a9a 100644 --- a/docs/snapshot_restore.md +++ b/docs/snapshot_restore.md @@ -90,6 +90,15 @@ start using it. ./ch-remote --api-socket=/tmp/cloud-hypervisor.sock resume ``` +Alternatively, the `resume` option can be used to automatically resume the VM +after restore completes: + +```bash +./cloud-hypervisor \ + --api-socket /tmp/cloud-hypervisor.sock \ + --restore source_url=file:///home/foo/snapshot,resume=true +``` + At this point, the VM is fully restored and is identical to the VM which was snapshot earlier. diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 8b284660a..93ec6c891 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -2603,17 +2603,20 @@ pub struct RestoreConfig { pub memory_restore_mode: MemoryRestoreMode, #[serde(default)] pub net_fds: Option>, + #[serde(default)] + pub resume: bool, } impl RestoreConfig { pub const SYNTAX: &'static str = "Restore from a VM snapshot. \ \nRestore parameters \"source_url=,prefault=on|off,memory_restore_mode=copy|ondemand,\ - net_fds=\" \ + net_fds=,resume=true|false\" \ \n`source_url` should be a valid URL (e.g file:///foo/bar or tcp://192.168.1.10/foo) \ \n`prefault` controls eager prefaulting for the copy-based restore path (disabled by default) \ \n`memory_restore_mode=copy` preserves the existing eager read-copy restore behavior, while `memory_restore_mode=ondemand` enables lazy demand paging and fails restore if userfaultfd support is unavailable \ \n`net_fds` is a list of net ids with new file descriptors. \ - Only net devices backed by FDs directly are needed as input."; + Only net devices backed by FDs directly are needed as input.\ + \n `resume` controls whether the VM will be directly resumed after restore "; pub fn parse(restore: &str) -> Result { let mut parser = OptionParser::new(); @@ -2621,7 +2624,8 @@ impl RestoreConfig { .add("source_url") .add("prefault") .add("memory_restore_mode") - .add("net_fds"); + .add("net_fds") + .add("resume"); parser.parse(restore).map_err(Error::ParseRestore)?; let source_url = parser @@ -2649,12 +2653,18 @@ impl RestoreConfig { }) .collect() }); + let resume = parser + .convert::("resume") + .map_err(Error::ParseRestore)? + .unwrap_or(Toggle(false)) + .0; Ok(RestoreConfig { source_url, prefault, memory_restore_mode, net_fds, + resume, }) } @@ -4546,6 +4556,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, net_fds: None, + resume: false, } ); assert_eq!( @@ -4568,6 +4579,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" fds: Some(vec![5, 6, 7, 8]), } ]), + resume: false, } ); assert_eq!( @@ -4577,6 +4589,17 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" prefault: false, memory_restore_mode: MemoryRestoreMode::OnDemand, net_fds: None, + resume: false, + } + ); + assert_eq!( + RestoreConfig::parse("source_url=/path/to/snapshot,resume=on")?, + RestoreConfig { + source_url: PathBuf::from("/path/to/snapshot"), + prefault: false, + memory_restore_mode: MemoryRestoreMode::Copy, + net_fds: None, + resume: true, } ); // Parsing should fail as source_url is a required field @@ -4678,6 +4701,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" fds: Some(vec![7, 8]), }, ]), + resume: false, }; valid_config.validate(&snapshot_vm_config).unwrap(); @@ -4742,6 +4766,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" prefault: false, memory_restore_mode: MemoryRestoreMode::Copy, net_fds: None, + resume: false, }; snapshot_vm_config.net = Some(vec![NetConfig { id: Some("net2".to_owned()), @@ -4755,6 +4780,7 @@ id=\"{id}\",pci_segment={pci_segment},queue_sizes={queue_sizes}" prefault: true, memory_restore_mode: MemoryRestoreMode::OnDemand, net_fds: None, + resume: false, }; assert_eq!( invalid_restore_mode.validate(&snapshot_vm_config), diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 0b82be49e..cb72d69d9 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -1849,16 +1849,22 @@ impl RequestHandler for Vmm { restore_cfg.prefault, restore_cfg.memory_restore_mode, ) - .map_err(|vm_restore_err| { - error!("VM Restore failed: {vm_restore_err:?}"); - - // Cleanup the VM being created while vm restore + .and_then(|()| { + if restore_cfg.resume { + self.vm_resume() + } else { + Ok(()) + } + }) + .map_err(|e| { + error!("VM Restore failed: {e:?}"); if let Err(e) = self.vm_delete() { return e; } + e + })?; - vm_restore_err - }) + Ok(()) } #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))]