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.extra_args(vec![
66 "--pmem".to_string(),
67 format!("{},ro=true", pmem_file.path().display().to_string()),
68 ]);
69 // TODO: Remove once USB has snapshot/restore support.
70 config = config.extra_args(vec!["--no-usb".to_string()]);
71 if disabled_sandbox {
72 config = config.disable_sandbox();
73 }
74 config
75 };
76
77 let mut vm = TestVm::new(new_config()).unwrap();
78
79 // Verify RAM is saved and restored by interacting with a filesystem pinned in RAM (i.e. tmpfs
80 // with swap disabled).
81 vm.exec_in_guest("swapoff -a").unwrap();
82 vm.exec_in_guest("mount -t tmpfs none /tmp").unwrap();
83
84 vm.exec_in_guest("echo foo > /tmp/foo").unwrap();
85 assert_eq!(
86 "foo",
87 vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
88 );
89
90 vm.suspend_full().unwrap();
91 // Take snapshot of original VM state
92 println!("snapshotting VM - clean state");
93 let dir = tempdir().unwrap();
94 let snap1_path = dir.path().join("snapshot.bkp");
95 vm.snapshot(&snap1_path).unwrap();
96 vm.resume_full().unwrap();
97
98 vm.exec_in_guest("echo bar > /tmp/foo").unwrap();
99 assert_eq!(
100 "bar",
101 vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
102 );
103
104 vm.suspend_full().unwrap();
105 let snap2_path = dir.path().join("snapshot2.bkp");
106
107 // Write command to VM
108 // This command will get queued and not run while the VM is suspended. The command is saved in
109 // the serial device. After the snapshot is taken, the VM is resumed. At that point, the
110 // command runs and is validated.
111 let echo_cmd = vm.exec_in_guest_async("echo 42").unwrap();
112 // Take snapshot of modified VM
113 println!("snapshotting VM - mod state");
114 vm.snapshot(&snap2_path).unwrap();
115
116 vm.resume_full().unwrap();
117 assert_eq!("42", echo_cmd.wait_ok(&mut vm).unwrap().stdout.trim());
118
119 println!("restoring VM - to clean state");
120
121 // shut down VM
122 drop(vm);
123 // Start up VM with cold restore.
124 let mut vm = TestVm::new_restore_suspended(new_config().extra_args(vec![
125 "--restore".to_string(),
126 snap1_path.to_str().unwrap().to_string(),
127 "--suspended".to_string(),
128 ]))
129 .unwrap();
130
131 // snapshot VM after restore
132 println!("snapshotting VM - clean state restored");
133 let snap3_path = dir.path().join("snapshot3.bkp");
134 vm.snapshot(&snap3_path).unwrap();
135 vm.resume_full().unwrap();
136
137 assert_eq!(
138 "foo",
139 vm.exec_in_guest("cat /tmp/foo").unwrap().stdout.trim()
140 );
141
142 let (equal, output) = compare_snapshots(&snap1_path, &snap2_path);
143 assert!(
144 !equal,
145 "1st and 2nd snapshot are unexpectedly equal:\n{output}"
146 );
147
148 let (equal, output) = compare_snapshots(&snap1_path, &snap3_path);
149 assert!(equal, "1st and 3rd snapshot are not equal:\n{output}");
150
151 Ok(())
152 }
153
154 #[test]
snapshot_vhost_user_root()155 fn snapshot_vhost_user_root() {
156 call_test_with_sudo("snapshot_vhost_user")
157 }
158
159 // This test will fail/hang if ran by its self.
160 #[ignore = "Only to be called by snapshot_vhost_user_root"]
161 #[test]
snapshot_vhost_user()162 fn snapshot_vhost_user() {
163 fn spin_up_vhost_user_devices() -> (
164 VhostUserBackend,
165 VhostUserBackend,
166 NamedTempFile,
167 NamedTempFile,
168 ) {
169 let block_socket = NamedTempFile::new().unwrap();
170 let disk = prepare_disk_img();
171
172 // Spin up block vhost user process
173 let block_vu_config =
174 create_vu_block_config(CmdType::Device, block_socket.path(), disk.path());
175 let block_vu_device = VhostUserBackend::new(block_vu_config).unwrap();
176
177 // Spin up net vhost user process.
178 // Queue handlers don't get activated currently.
179 let net_socket = NamedTempFile::new().unwrap();
180 let net_config = create_net_config(net_socket.path());
181 let net_vu_device = VhostUserBackend::new(net_config).unwrap();
182
183 (block_vu_device, net_vu_device, block_socket, net_socket)
184 }
185
186 let dir = tempdir().unwrap();
187 let snap_path = dir.path().join("snapshot.bkp");
188
189 {
190 let (_block_vu_device, _net_vu_device, block_socket, net_socket) =
191 spin_up_vhost_user_devices();
192
193 let mut config = Config::new();
194 config = config.with_vhost_user("block", block_socket.path());
195 config = config.with_vhost_user("net", net_socket.path());
196 config = config.extra_args(vec!["--no-usb".to_string()]);
197 let mut vm = TestVm::new(config).unwrap();
198
199 // suspend VM
200 vm.suspend_full().unwrap();
201 vm.snapshot(&snap_path).unwrap();
202 drop(vm);
203 }
204
205 let (_block_vu_device, _net_vu_device, block_socket, net_socket) = spin_up_vhost_user_devices();
206
207 let mut config = Config::new();
208 // Start up VM with cold restore.
209 config = config.with_vhost_user("block", block_socket.path());
210 config = config.with_vhost_user("net", net_socket.path());
211 config = config.extra_args(vec![
212 "--restore".to_string(),
213 snap_path.to_str().unwrap().to_string(),
214 "--no-usb".to_string(),
215 ]);
216 let _vm = TestVm::new_restore(config).unwrap();
217 }
218
create_net_config(socket: &Path) -> VuConfig219 fn create_net_config(socket: &Path) -> VuConfig {
220 let socket_path = socket.to_str().unwrap();
221 println!("socket={socket_path}");
222 VuConfig::new(CmdType::Device, "net").extra_args(vec![
223 "net".to_string(),
224 "--device".to_string(),
225 format!(
226 "{},{},{},{}",
227 socket_path, "192.168.10.1", "255.255.255.0", "12:34:56:78:9a:bc"
228 ),
229 ])
230 }
231