xref: /aosp_15_r20/external/crosvm/e2e_tests/tests/suspend_resume.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #![cfg(any(target_os = "android", target_os = "linux"))]
6 
7 use std::io::Write;
8 use std::path::Path;
9 
10 use base::test_utils::call_test_with_sudo;
11 use fixture::utils::create_vu_block_config;
12 use fixture::utils::prepare_disk_img;
13 use fixture::vhost_user::CmdType;
14 use fixture::vhost_user::Config as VuConfig;
15 use fixture::vhost_user::VhostUserBackend;
16 use fixture::vm::Config;
17 use fixture::vm::TestVm;
18 use tempfile::tempdir;
19 use tempfile::NamedTempFile;
20 
21 // Tests for suspend/resume.
22 //
23 // System-wide suspend/resume, snapshot/restore.
24 // Tests below check for snapshot/restore functionality, and suspend/resume.
25 
compare_snapshots(a: &Path, b: &Path) -> (bool, String)26 fn compare_snapshots(a: &Path, b: &Path) -> (bool, String) {
27     let result = std::process::Command::new("diff")
28         .arg("-qr")
29         // vcpu and irqchip have timestamps that differ even if a freshly restored VM is
30         // snapshotted before it starts running again.
31         .arg("--exclude")
32         .arg("vcpu*")
33         .arg("--exclude")
34         .arg("irqchip")
35         // KVM's pvclock seems to advance some even if the vCPUs haven't started yet.
36         .arg("--exclude")
37         .arg("pvclock")
38         .arg(a)
39         .arg(b)
40         .output()
41         .unwrap();
42     (
43         result.status.success(),
44         String::from_utf8(result.stdout).unwrap(),
45     )
46 }
47 
48 #[test]
suspend_snapshot_restore_resume() -> anyhow::Result<()>49 fn suspend_snapshot_restore_resume() -> anyhow::Result<()> {
50     suspend_resume_system(false)
51 }
52 
53 #[test]
suspend_snapshot_restore_resume_disable_sandbox() -> anyhow::Result<()>54 fn suspend_snapshot_restore_resume_disable_sandbox() -> anyhow::Result<()> {
55     suspend_resume_system(true)
56 }
57 
suspend_resume_system(disabled_sandbox: bool) -> anyhow::Result<()>58 fn suspend_resume_system(disabled_sandbox: bool) -> anyhow::Result<()> {
59     let mut pmem_file = NamedTempFile::new().unwrap();
60     pmem_file.write_all(&[0; 4096]).unwrap();
61     pmem_file.as_file_mut().sync_all().unwrap();
62 
63     let new_config = || {
64         let mut config = Config::new();
65         config = config.with_stdout_hardware("legacy-virtio-console");
66         config = config.extra_args(vec![
67             "--pmem".to_string(),
68             format!("{},ro=true", pmem_file.path().display().to_string()),
69         ]);
70         // TODO: Remove once USB has snapshot/restore support.
71         config = config.extra_args(vec!["--no-usb".to_string()]);
72         if disabled_sandbox {
73             config = config.disable_sandbox();
74         }
75         config
76     };
77 
78     let mut vm = TestVm::new(new_config()).unwrap();
79 
80     // Verify RAM is saved and restored by interacting with a filesystem pinned in RAM (i.e. tmpfs
81     // with swap disabled).
82     vm.exec_in_guest("swapoff -a").unwrap();
83     vm.exec_in_guest("mount -t tmpfs none /tmp").unwrap();
84 
85     vm.exec_in_guest("echo foo > /tmp/foo").unwrap();
86     assert_eq!(
87         "foo",
88         vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
89     );
90 
91     vm.suspend_full().unwrap();
92     // Take snapshot of original VM state
93     println!("snapshotting VM - clean state");
94     let dir = tempdir().unwrap();
95     let snap1_path = dir.path().join("snapshot.bkp");
96     vm.snapshot(&snap1_path).unwrap();
97     vm.resume_full().unwrap();
98 
99     vm.exec_in_guest("echo bar > /tmp/foo").unwrap();
100     assert_eq!(
101         "bar",
102         vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
103     );
104 
105     vm.suspend_full().unwrap();
106     let snap2_path = dir.path().join("snapshot2.bkp");
107 
108     // Write command to VM
109     // This command will get queued and not run while the VM is suspended. The command is saved in
110     // the serial device. After the snapshot is taken, the VM is resumed. At that point, the
111     // command runs and is validated.
112     let echo_cmd = vm.exec_in_guest_async("echo 42").unwrap();
113     // Take snapshot of modified VM
114     println!("snapshotting VM - mod state");
115     vm.snapshot(&snap2_path).unwrap();
116 
117     vm.resume_full().unwrap();
118     assert_eq!("42", echo_cmd.wait_ok(&mut vm).unwrap().stdout.trim());
119 
120     println!("restoring VM - to clean state");
121 
122     // shut down VM
123     drop(vm);
124     // Start up VM with cold restore.
125     let mut vm = TestVm::new_restore_suspended(new_config().extra_args(vec![
126         "--restore".to_string(),
127         snap1_path.to_str().unwrap().to_string(),
128         "--suspended".to_string(),
129     ]))
130     .unwrap();
131 
132     // snapshot VM after restore
133     println!("snapshotting VM - clean state restored");
134     let snap3_path = dir.path().join("snapshot3.bkp");
135     vm.snapshot(&snap3_path).unwrap();
136     vm.resume_full().unwrap();
137 
138     assert_eq!(
139         "foo",
140         vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
141     );
142 
143     let (equal, output) = compare_snapshots(&snap1_path, &snap2_path);
144     assert!(
145         !equal,
146         "1st and 2nd snapshot are unexpectedly equal:\n{output}"
147     );
148 
149     let (equal, output) = compare_snapshots(&snap1_path, &snap3_path);
150     assert!(equal, "1st and 3rd snapshot are not equal:\n{output}");
151 
152     Ok(())
153 }
154 
155 #[test]
snapshot_vhost_user_root()156 fn snapshot_vhost_user_root() {
157     call_test_with_sudo("snapshot_vhost_user")
158 }
159 
160 // This test will fail/hang if ran by its self.
161 #[ignore = "Only to be called by snapshot_vhost_user_root"]
162 #[test]
snapshot_vhost_user()163 fn snapshot_vhost_user() {
164     fn spin_up_vhost_user_devices() -> (
165         VhostUserBackend,
166         VhostUserBackend,
167         NamedTempFile,
168         NamedTempFile,
169     ) {
170         let block_socket = NamedTempFile::new().unwrap();
171         let disk = prepare_disk_img();
172 
173         // Spin up block vhost user process
174         let block_vu_config =
175             create_vu_block_config(CmdType::Device, block_socket.path(), disk.path());
176         let block_vu_device = VhostUserBackend::new(block_vu_config).unwrap();
177 
178         // Spin up net vhost user process.
179         // Queue handlers don't get activated currently.
180         let net_socket = NamedTempFile::new().unwrap();
181         let net_config = create_net_config(net_socket.path());
182         let net_vu_device = VhostUserBackend::new(net_config).unwrap();
183 
184         (block_vu_device, net_vu_device, block_socket, net_socket)
185     }
186 
187     let dir = tempdir().unwrap();
188     let snap_path = dir.path().join("snapshot.bkp");
189 
190     {
191         let (_block_vu_device, _net_vu_device, block_socket, net_socket) =
192             spin_up_vhost_user_devices();
193 
194         let mut config = Config::new();
195         config = config.with_stdout_hardware("legacy-virtio-console");
196         config = config.with_vhost_user("block", block_socket.path());
197         config = config.with_vhost_user("net", net_socket.path());
198         config = config.extra_args(vec!["--no-usb".to_string()]);
199         let mut vm = TestVm::new(config).unwrap();
200 
201         // suspend VM
202         vm.suspend_full().unwrap();
203         vm.snapshot(&snap_path).unwrap();
204         drop(vm);
205     }
206 
207     let (_block_vu_device, _net_vu_device, block_socket, net_socket) = spin_up_vhost_user_devices();
208 
209     let mut config = Config::new();
210     // Start up VM with cold restore.
211     config = config.with_stdout_hardware("legacy-virtio-console");
212     config = config.with_vhost_user("block", block_socket.path());
213     config = config.with_vhost_user("net", net_socket.path());
214     config = config.extra_args(vec![
215         "--restore".to_string(),
216         snap_path.to_str().unwrap().to_string(),
217         "--no-usb".to_string(),
218     ]);
219     let _vm = TestVm::new_restore(config).unwrap();
220 }
221 
create_net_config(socket: &Path) -> VuConfig222 fn create_net_config(socket: &Path) -> VuConfig {
223     let socket_path = socket.to_str().unwrap();
224     println!("socket={socket_path}");
225     VuConfig::new(CmdType::Device, "net").extra_args(vec![
226         "net".to_string(),
227         "--device".to_string(),
228         format!(
229             "{},{},{},{}",
230             socket_path, "192.168.10.1", "255.255.255.0", "12:34:56:78:9a:bc"
231         ),
232     ])
233 }
234