• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 use std::env;
6 use std::io::BufRead;
7 use std::io::Write;
8 use std::path::PathBuf;
9 use std::process::Command;
10 use std::sync::Once;
11 use std::time::Duration;
12 
13 use anyhow::anyhow;
14 use anyhow::bail;
15 use anyhow::Context;
16 use anyhow::Result;
17 use base::syslog;
18 use log::Level;
19 use prebuilts::download_file;
20 
21 use crate::sys::SerialArgs;
22 use crate::sys::TestVmSys;
23 use crate::utils::run_with_timeout;
24 
25 const PREBUILT_URL: &str = "https://storage.googleapis.com/crosvm/integration_tests";
26 
27 #[cfg(target_arch = "x86_64")]
28 const ARCH: &str = "x86_64";
29 #[cfg(target_arch = "arm")]
30 const ARCH: &str = "arm";
31 #[cfg(target_arch = "aarch64")]
32 const ARCH: &str = "aarch64";
33 
34 /// Timeout when waiting for pipes that are expected to be ready.
35 const COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(5);
36 
37 /// Timeout for the VM to boot and the delegate to report that it's ready.
38 const BOOT_TIMEOUT: Duration = Duration::from_secs(30);
39 
40 /// Default timeout when waiting for guest commands to execute
41 const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
42 
prebuilt_version() -> &'static str43 fn prebuilt_version() -> &'static str {
44     include_str!("../../guest_under_test/PREBUILT_VERSION").trim()
45 }
46 
kernel_prebuilt_url() -> String47 fn kernel_prebuilt_url() -> String {
48     format!(
49         "{}/guest-bzimage-{}-{}",
50         PREBUILT_URL,
51         ARCH,
52         prebuilt_version()
53     )
54 }
55 
rootfs_prebuilt_url() -> String56 fn rootfs_prebuilt_url() -> String {
57     format!(
58         "{}/guest-rootfs-{}-{}",
59         PREBUILT_URL,
60         ARCH,
61         prebuilt_version()
62     )
63 }
64 
65 /// The kernel bzImage is stored next to the test executable, unless overridden by
66 /// CROSVM_CARGO_TEST_KERNEL_BINARY
kernel_path() -> PathBuf67 pub(super) fn kernel_path() -> PathBuf {
68     match env::var("CROSVM_CARGO_TEST_KERNEL_BINARY") {
69         Ok(value) => PathBuf::from(value),
70         Err(_) => env::current_exe()
71             .unwrap()
72             .parent()
73             .unwrap()
74             .join(format!("bzImage-{}", prebuilt_version())),
75     }
76 }
77 
78 /// The rootfs image is stored next to the test executable, unless overridden by
79 /// CROSVM_CARGO_TEST_ROOTFS_IMAGE
rootfs_path() -> PathBuf80 pub(super) fn rootfs_path() -> PathBuf {
81     match env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE") {
82         Ok(value) => PathBuf::from(value),
83         Err(_) => env::current_exe()
84             .unwrap()
85             .parent()
86             .unwrap()
87             .join(format!("rootfs-{}", prebuilt_version())),
88     }
89 }
90 
91 /// Represents a command running in the guest. See `TestVm::exec_in_guest_async()`
92 #[must_use]
93 pub struct GuestProcess {
94     command: String,
95     timeout: Duration,
96 }
97 
98 impl GuestProcess {
with_timeout(self, duration: Duration) -> Self99     pub fn with_timeout(self, duration: Duration) -> Self {
100         Self {
101             timeout: duration,
102             ..self
103         }
104     }
105 
106     /// Waits for the process to finish execution and return the produced stdout.
107     /// Will fail on a non-zero exit code.
wait(self, vm: &mut TestVm) -> Result<String>108     pub fn wait(self, vm: &mut TestVm) -> Result<String> {
109         let command = self.command.clone();
110         let (exit_code, output) = self.wait_unchecked(vm)?;
111         if exit_code != 0 {
112             bail!(
113                 "Command `{}` terminated with exit code {}",
114                 command,
115                 exit_code
116             );
117         }
118         Ok(output)
119     }
120 
121     /// Same as `wait` but will return a tuple of (exit code, output) instead of failing
122     /// on a non-zero exit code.
wait_unchecked(self, vm: &mut TestVm) -> Result<(i32, String)>123     pub fn wait_unchecked(self, vm: &mut TestVm) -> Result<(i32, String)> {
124         // First read echo of command
125         let echo = vm
126             .read_line_from_guest(COMMUNICATION_TIMEOUT)
127             .with_context(|| {
128                 format!(
129                     "Command `{}`: Failed to read echo from guest pipe",
130                     self.command
131                 )
132             })?;
133         assert_eq!(echo.trim(), self.command.trim());
134 
135         // Then read stdout and exit code
136         let mut output = Vec::<String>::new();
137         let exit_code = loop {
138             let line = vm.read_line_from_guest(self.timeout).with_context(|| {
139                 format!(
140                     "Command `{}`: Failed to read response from guest",
141                     self.command
142                 )
143             })?;
144             let trimmed = line.trim();
145             if trimmed.starts_with(TestVm::EXIT_CODE_LINE) {
146                 let exit_code_str = &trimmed[(TestVm::EXIT_CODE_LINE.len() + 1)..];
147                 break exit_code_str.parse::<i32>().unwrap();
148             }
149             output.push(trimmed.to_owned());
150         };
151 
152         // Finally get the VM in a ready state again.
153         vm.wait_for_guest(COMMUNICATION_TIMEOUT)
154             .with_context(|| format!("Command `{}`: Failed to wait for guest", self.command))?;
155 
156         Ok((exit_code, output.join("\n")))
157     }
158 }
159 
160 /// Configuration to start `TestVm`.
161 pub struct Config {
162     /// Extra arguments for the `run` subcommand.
163     pub(super) extra_args: Vec<String>,
164 
165     /// Use `O_DIRECT` for the rootfs.
166     pub(super) o_direct: bool,
167 
168     /// Log level of `TestVm`
169     pub(super) log_level: Level,
170 
171     /// File to save crosvm log to
172     pub(super) log_file: Option<String>,
173 
174     /// Wrapper command line for executing `TestVM`
175     pub(super) wrapper_cmd: Option<String>,
176 }
177 
178 impl Default for Config {
default() -> Self179     fn default() -> Self {
180         Self {
181             log_level: Level::Info,
182             extra_args: Default::default(),
183             o_direct: Default::default(),
184             log_file: None,
185             wrapper_cmd: None,
186         }
187     }
188 }
189 
190 impl Config {
191     /// Creates a new `run` command with `extra_args`.
new() -> Self192     pub fn new() -> Self {
193         Default::default()
194     }
195 
196     /// Uses extra arguments for `crosvm run`.
197     #[allow(dead_code)]
extra_args(mut self, args: Vec<String>) -> Self198     pub fn extra_args(mut self, args: Vec<String>) -> Self {
199         let mut args = args;
200         self.extra_args.append(&mut args);
201         self
202     }
203 
204     /// Uses `O_DIRECT` for the rootfs.
o_direct(mut self) -> Self205     pub fn o_direct(mut self) -> Self {
206         self.o_direct = true;
207         self
208     }
209 
210     /// Uses `disable-sandbox` argument for `crosvm run`.
disable_sandbox(mut self) -> Self211     pub fn disable_sandbox(mut self) -> Self {
212         self.extra_args.push("--disable-sandbox".to_string());
213         self
214     }
215 
from_env() -> Self216     pub fn from_env() -> Self {
217         let mut cfg: Config = Default::default();
218         env::var("CROSVM_CARGO_TEST_E2E_WRAPPER_CMD").map_or((), |x| cfg.wrapper_cmd = Some(x));
219         env::var("CROSVM_CARGO_TEST_LOG_FILE").map_or((), |x| cfg.log_file = Some(x));
220         env::var("CROSVM_CARGO_TEST_LOG_LEVEL_DEBUG").map_or((), |_| cfg.log_level = Level::Debug);
221         cfg
222     }
223 }
224 
225 static PREP_ONCE: Once = Once::new();
226 
227 /// Test fixture to spin up a VM running a guest that can be communicated with.
228 ///
229 /// After creation, commands can be sent via exec_in_guest. The VM is stopped
230 /// when this instance is dropped.
231 pub struct TestVm {
232     // Platform-dependent bits
233     sys: TestVmSys,
234     // The guest is ready to receive a command.
235     ready: bool,
236 }
237 
238 impl TestVm {
239     /// Line sent by the delegate binary when the guest is ready.
240     const READY_LINE: &'static str = "\x05READY";
241     /// Line sent by the delegate binary to terminate the stdout and send the exit code.
242     const EXIT_CODE_LINE: &'static str = "\x05EXIT_CODE";
243 
244     /// Downloads prebuilts if needed.
initialize_once()245     fn initialize_once() {
246         if let Err(e) = syslog::init() {
247             panic!("failed to initiailize syslog: {}", e);
248         }
249 
250         // It's possible the prebuilts downloaded by crosvm-9999.ebuild differ
251         // from the version that crosvm was compiled for.
252         if let Ok(value) = env::var("CROSVM_CARGO_TEST_PREBUILT_VERSION") {
253             if value != prebuilt_version() {
254                 panic!(
255                     "Environment provided prebuilts are version {}, but crosvm was compiled \
256                     for prebuilt version {}. Did you update PREBUILT_VERSION everywhere?",
257                     value,
258                     prebuilt_version()
259                 );
260             }
261         }
262 
263         let kernel_path = kernel_path();
264         if env::var("CROSVM_CARGO_TEST_KERNEL_BINARY").is_err() {
265             if !kernel_path.exists() {
266                 download_file(&kernel_prebuilt_url(), &kernel_path).unwrap();
267             }
268         }
269         assert!(kernel_path.exists(), "{:?} does not exist", kernel_path);
270 
271         let rootfs_path = rootfs_path();
272         if env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE").is_err() {
273             if !rootfs_path.exists() {
274                 download_file(&rootfs_prebuilt_url(), &rootfs_path).unwrap();
275             }
276         }
277         assert!(rootfs_path.exists(), "{:?} does not exist", rootfs_path);
278 
279         TestVmSys::check_rootfs_file(&rootfs_path);
280     }
281 
282     /// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
283     /// files if necessary.
284     ///
285     /// This generic method takes a `FnOnce` argument which is in charge of completing the `Command`
286     /// with all the relevant options needed to boot the VM.
new_generic<F>(f: F, cfg: Config) -> Result<TestVm> where F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,287     pub fn new_generic<F>(f: F, cfg: Config) -> Result<TestVm>
288     where
289         F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
290     {
291         PREP_ONCE.call_once(TestVm::initialize_once);
292         let mut vm = TestVm {
293             sys: TestVmSys::new_generic(f, cfg).with_context(|| "Could not start crosvm")?,
294             ready: false,
295         };
296         vm.wait_for_guest(BOOT_TIMEOUT)
297             .with_context(|| "Guest did not become ready after boot")?;
298         Ok(vm)
299     }
300 
new(cfg: Config) -> Result<TestVm>301     pub fn new(cfg: Config) -> Result<TestVm> {
302         TestVm::new_generic(TestVmSys::append_config_args, cfg)
303     }
304 
305     /// Instanciate a new crosvm instance using a configuration file. The first call will trigger
306     /// the download of prebuilt files if necessary.
new_with_config_file(cfg: Config) -> Result<TestVm>307     pub fn new_with_config_file(cfg: Config) -> Result<TestVm> {
308         TestVm::new_generic(TestVmSys::append_config_file_arg, cfg)
309     }
310 
311     /// Executes the provided command in the guest.
312     /// Returns the stdout that was produced by the command, or a GuestProcessError::ExitCode if
313     /// the program did not exit with 0.
exec_in_guest(&mut self, command: &str) -> Result<String>314     pub fn exec_in_guest(&mut self, command: &str) -> Result<String> {
315         self.exec_in_guest_async(command)?.wait(self)
316     }
317 
318     /// Same as `exec_in_guest` but will return a tuple of (exit code, output) instead of failing
319     /// on a non-zero exit code.
exec_in_guest_unchecked(&mut self, command: &str) -> Result<(i32, String)>320     pub fn exec_in_guest_unchecked(&mut self, command: &str) -> Result<(i32, String)> {
321         self.exec_in_guest_async(command)?.wait_unchecked(self)
322     }
323 
324     /// Executes the provided command in the guest asynchronously.
325     /// The command will be run in the guest, but output will not be read until GuestProcess::wait
326     /// is called.
exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess>327     pub fn exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess> {
328         assert!(self.ready);
329         self.ready = false;
330 
331         // Send command and read echo from the pipe
332         self.write_line_to_guest(command, COMMUNICATION_TIMEOUT)
333             .with_context(|| format!("Command `{}`: Failed to write to guest pipe", command))?;
334 
335         Ok(GuestProcess {
336             command: command.to_owned(),
337             timeout: DEFAULT_COMMAND_TIMEOUT,
338         })
339     }
340 
341     // Waits for the guest to be ready to receive commands
wait_for_guest(&mut self, timeout: Duration) -> Result<()>342     fn wait_for_guest(&mut self, timeout: Duration) -> Result<()> {
343         assert!(!self.ready);
344         let line = self.read_line_from_guest(timeout)?;
345         if line.trim() == TestVm::READY_LINE {
346             self.ready = true;
347             Ok(())
348         } else {
349             Err(anyhow!(
350                 "Recevied unexpected data from delegate: {:?}",
351                 line.trim()
352             ))
353         }
354     }
355 
356     /// Reads one line via the `from_guest` pipe from the guest delegate.
read_line_from_guest(&mut self, timeout: Duration) -> Result<String>357     fn read_line_from_guest(&mut self, timeout: Duration) -> Result<String> {
358         let reader = self.sys.from_guest_reader.clone();
359         run_with_timeout(
360             move || {
361                 let mut data = String::new();
362                 reader.lock().unwrap().read_line(&mut data)?;
363                 println!("<- {:?}", data);
364                 Ok(data)
365             },
366             timeout,
367         )?
368     }
369 
370     /// Send one line via the `to_guest` pipe to the guest delegate.
write_line_to_guest(&mut self, data: &str, timeout: Duration) -> Result<()>371     fn write_line_to_guest(&mut self, data: &str, timeout: Duration) -> Result<()> {
372         let writer = self.sys.to_guest.clone();
373         let data = data.to_owned();
374         run_with_timeout(
375             move || -> Result<()> {
376                 println!("-> {:?}", data);
377                 writeln!(writer.lock().unwrap(), "{}", data)?;
378                 Ok(())
379             },
380             timeout,
381         )?
382     }
383 
stop(&mut self) -> Result<()>384     pub fn stop(&mut self) -> Result<()> {
385         self.sys.crosvm_command("stop", vec![])
386     }
387 
suspend(&mut self) -> Result<()>388     pub fn suspend(&mut self) -> Result<()> {
389         self.sys.crosvm_command("suspend", vec![])
390     }
391 
resume(&mut self) -> Result<()>392     pub fn resume(&mut self) -> Result<()> {
393         self.sys.crosvm_command("resume", vec![])
394     }
395 
disk(&mut self, args: Vec<String>) -> Result<()>396     pub fn disk(&mut self, args: Vec<String>) -> Result<()> {
397         self.sys.crosvm_command("disk", args)
398     }
399 
snapshot(&mut self, filename: &std::path::Path) -> Result<()>400     pub fn snapshot(&mut self, filename: &std::path::Path) -> Result<()> {
401         self.sys.crosvm_command(
402             "snapshot",
403             vec!["take".to_string(), String::from(filename.to_str().unwrap())],
404         )
405     }
406 
407     // No argument is passed in restore as we will always restore snapshot.bkp for testing.
restore(&mut self, filename: &std::path::Path) -> Result<()>408     pub fn restore(&mut self, filename: &std::path::Path) -> Result<()> {
409         self.sys.crosvm_command(
410             "snapshot",
411             vec![
412                 "restore".to_string(),
413                 String::from(filename.to_str().unwrap()),
414             ],
415         )
416     }
417 }
418 
419 impl Drop for TestVm {
drop(&mut self)420     fn drop(&mut self) {
421         self.stop().unwrap();
422         let status = self.sys.process.take().unwrap().wait().unwrap();
423         if !status.success() {
424             panic!("VM exited illegally: {}", status);
425         }
426     }
427 }
428