From 068b5ecb6318ae7ec56a1025cff174b953708609 Mon Sep 17 00:00:00 2001 From: Rob Bradford Date: Tue, 17 Mar 2026 09:30:01 -0700 Subject: [PATCH] vmm: Add support for resuming automatically on restore Add an option that can be used when restoring to resume the VM. This is particularly useful when restoring the VM via the direct VMM command line, when you might not want/have an API socket configured. Signed-off-by: Rob Bradford --- cloud-hypervisor/tests/integration.rs | 83 +++++++++++++++++++-------- docs/snapshot_restore.md | 9 +++ vmm/src/config.rs | 32 ++++++++++- vmm/src/lib.rs | 18 ++++-- 4 files changed, 108 insertions(+), 34 deletions(-) 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"))]