1use std::env;
6use std::io::Write;
7use std::path::Path;
8use std::path::PathBuf;
9use std::process::Command;
10use std::sync::Once;
11use std::time::Duration;
12
13use anyhow::anyhow;
14use anyhow::bail;
15use anyhow::Context;
16use anyhow::Result;
17use base::syslog;
18use base::test_utils::check_can_sudo;
19use crc32fast::hash;
20use delegate::wire_format::DelegateMessage;
21use delegate::wire_format::ExitStatus;
22use delegate::wire_format::GuestToHostMessage;
23use delegate::wire_format::HostToGuestMessage;
24use delegate::wire_format::ProgramExit;
25use log::info;
26use log::Level;
27use prebuilts::download_file;
28use readclock::ClockValues;
29use url::Url;
30
31use crate::sys::SerialArgs;
32use crate::sys::TestVmSys;
33use crate::utils::run_with_timeout;
34
35const PREBUILT_URL: &str = "https://storage.googleapis.com/crosvm/integration_tests";
36
37#[cfg(target_arch = "x86_64")]
38const ARCH: &str = "x86_64";
39#[cfg(target_arch = "aarch64")]
40const ARCH: &str = "aarch64";
41#[cfg(target_arch = "riscv64")]
42const ARCH: &str = "riscv64";
43
44const COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(5);
46
47const BOOT_TIMEOUT: Duration = Duration::from_secs(60);
49
50const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
52
53fn prebuilt_version() -> &'static str {
54 include_str!("../../guest_under_test/PREBUILT_VERSION").trim()
55}
56
57fn kernel_prebuilt_url_string() -> Url {
58 Url::parse(&format!(
59 "{}/guest-bzimage-{}-{}",
60 PREBUILT_URL,
61 ARCH,
62 prebuilt_version()
63 ))
64 .unwrap()
65}
66
67fn rootfs_prebuilt_url_string() -> Url {
68 Url::parse(&format!(
69 "{}/guest-rootfs-{}-{}",
70 PREBUILT_URL,
71 ARCH,
72 prebuilt_version()
73 ))
74 .unwrap()
75}
76
77pub(super) fn local_path_from_url(url: &Url) -> PathBuf {
78 if url.scheme() == "file" {
79 return url.to_file_path().unwrap();
80 }
81 if url.scheme() != "http" && url.scheme() != "https" {
82 panic!("Only file, http, https URLs are supported for artifacts")
83 }
84 env::current_exe().unwrap().parent().unwrap().join(format!(
85 "e2e_prebuilt-{:x}-{:x}",
86 hash(url.as_str().as_bytes()),
87 hash(url.path().as_bytes())
88 ))
89}
90
91#[must_use]
93pub struct GuestProcess {
94 command: String,
95 timeout: Duration,
96}
97
98impl GuestProcess {
99 pub fn with_timeout(self, duration: Duration) -> Self {
100 Self {
101 timeout: duration,
102 ..self
103 }
104 }
105
106 pub fn wait_ok(self, vm: &mut TestVm) -> Result<ProgramExit> {
109 let command = self.command.clone();
110 let result = self.wait_result(vm)?;
111
112 match &result.exit_status {
113 ExitStatus::Code(0) => Ok(result),
114 ExitStatus::Code(code) => {
115 bail!("Command `{}` terminated with exit code {}", command, code)
116 }
117 ExitStatus::Signal(code) => bail!("Command `{}` stopped with signal {}", command, code),
118 ExitStatus::None => bail!("Command `{}` stopped for unknown reason", command),
119 }
120 }
121
122 pub fn wait_result(self, vm: &mut TestVm) -> Result<ProgramExit> {
125 let message = vm.read_message_from_guest(self.timeout).with_context(|| {
126 format!(
127 "Command `{}`: Failed to read response from guest",
128 self.command
129 )
130 })?;
131 match message {
133 GuestToHostMessage::ProgramExit(exit_info) => Ok(exit_info),
134 _ => bail!("Receive other message when anticipating ProgramExit"),
135 }
136 }
137}
138
139pub struct Config {
141 pub(super) extra_args: Vec<String>,
143
144 pub(super) o_direct: bool,
146
147 pub(super) log_level: Level,
149
150 pub(super) log_file: Option<String>,
152
153 pub(super) wrapper_cmd: Option<String>,
155
156 pub(super) kernel_url: Url,
158
159 pub(super) initrd_url: Option<Url>,
161
162 pub(super) rootfs_url: Option<Url>,
164
165 pub(super) rootfs_rw: bool,
167
168 pub(super) rootfs_compressed: bool,
170
171 pub(super) console_hardware: String,
173}
174
175impl Default for Config {
176 fn default() -> Self {
177 Self {
178 log_level: Level::Info,
179 extra_args: Default::default(),
180 o_direct: Default::default(),
181 log_file: None,
182 wrapper_cmd: None,
183 kernel_url: kernel_prebuilt_url_string(),
184 initrd_url: None,
185 rootfs_url: Some(rootfs_prebuilt_url_string()),
186 rootfs_rw: false,
187 rootfs_compressed: false,
188 console_hardware: "virtio-console".to_owned(),
189 }
190 }
191}
192
193impl Config {
194 pub fn new() -> Self {
196 Self::from_env()
197 }
198
199 pub fn extra_args(mut self, args: Vec<String>) -> Self {
201 let mut args = args;
202 self.extra_args.append(&mut args);
203 self
204 }
205
206 pub fn o_direct(mut self) -> Self {
208 self.o_direct = true;
209 self
210 }
211
212 pub fn disable_sandbox(mut self) -> Self {
214 self.extra_args.push("--disable-sandbox".to_string());
215 self
216 }
217
218 pub fn from_env() -> Self {
219 let mut cfg: Config = Default::default();
220 if let Ok(wrapper_cmd) = env::var("CROSVM_CARGO_TEST_E2E_WRAPPER_CMD") {
221 cfg.wrapper_cmd = Some(wrapper_cmd);
222 }
223 if let Ok(log_file) = env::var("CROSVM_CARGO_TEST_LOG_FILE") {
224 cfg.log_file = Some(log_file);
225 }
226 if env::var("CROSVM_CARGO_TEST_LOG_LEVEL_DEBUG").is_ok() {
227 cfg.log_level = Level::Debug;
228 }
229 if let Ok(kernel_url) = env::var("CROSVM_CARGO_TEST_KERNEL_IMAGE") {
230 info!("Using overrided kernel from env CROSVM_CARGO_TEST_KERNEL_IMAGE={kernel_url}");
231 cfg.kernel_url = Url::from_file_path(kernel_url).unwrap();
232 }
233 if let Ok(initrd_url) = env::var("CROSVM_CARGO_TEST_INITRD_IMAGE") {
234 info!("Using overrided kernel from env CROSVM_CARGO_TEST_INITRD_IMAGE={initrd_url}");
235 cfg.initrd_url = Some(Url::from_file_path(initrd_url).unwrap());
236 }
237 if let Ok(rootfs_url) = env::var("CROSVM_CARGO_TEST_ROOTFS_IMAGE") {
238 info!("Using overrided kernel from env CROSVM_CARGO_TEST_ROOTFS_IMAGE={rootfs_url}");
239 cfg.rootfs_url = Some(Url::from_file_path(rootfs_url).unwrap());
240 }
241 cfg
242 }
243
244 pub fn with_kernel(mut self, url: &str) -> Self {
245 self.kernel_url = Url::parse(url).unwrap();
246 self
247 }
248
249 pub fn with_initrd(mut self, url: &str) -> Self {
250 self.initrd_url = Some(Url::parse(url).unwrap());
251 self
252 }
253
254 pub fn with_rootfs(mut self, url: &str) -> Self {
255 self.rootfs_url = Some(Url::parse(url).unwrap());
256 self
257 }
258
259 pub fn rootfs_is_rw(mut self) -> Self {
260 self.rootfs_rw = true;
261 self
262 }
263
264 pub fn rootfs_is_compressed(mut self) -> Self {
265 self.rootfs_compressed = true;
266 self
267 }
268
269 pub fn with_stdout_hardware(mut self, hw_type: &str) -> Self {
270 self.console_hardware = hw_type.into();
271 self
272 }
273
274 pub fn with_vhost_user(mut self, device_type: &str, socket_path: &Path) -> Self {
275 self.extra_args.push("--vhost-user".to_string());
276 self.extra_args.push(format!(
277 "{},socket={}",
278 device_type,
279 socket_path.to_str().unwrap()
280 ));
281 self
282 }
283}
284
285static PREP_ONCE: Once = Once::new();
286
287pub struct TestVm {
292 sys: TestVmSys,
294 ready: bool,
296 sudo: bool,
298}
299
300impl TestVm {
301 fn initialize_once() {
303 if let Err(e) = syslog::init() {
304 panic!("failed to initiailize syslog: {e}");
305 }
306
307 info!("Prebuilt version to be used: {}", prebuilt_version());
310 if let Ok(value) = env::var("CROSVM_CARGO_TEST_PREBUILT_VERSION") {
311 if value != prebuilt_version() {
312 panic!(
313 "Environment provided prebuilts are version {}, but crosvm was compiled \
314 for prebuilt version {}. Did you update PREBUILT_VERSION everywhere?",
315 value,
316 prebuilt_version()
317 );
318 }
319 }
320 }
321
322 fn initiailize_artifacts(cfg: &Config) {
323 let kernel_path = local_path_from_url(&cfg.kernel_url);
324 if !kernel_path.exists() && cfg.kernel_url.scheme() != "file" {
325 download_file(cfg.kernel_url.as_str(), &kernel_path).unwrap();
326 }
327 assert!(kernel_path.exists(), "{kernel_path:?} does not exist");
328
329 if let Some(initrd_url) = &cfg.initrd_url {
330 let initrd_path = local_path_from_url(initrd_url);
331 if !initrd_path.exists() && initrd_url.scheme() != "file" {
332 download_file(initrd_url.as_str(), &initrd_path).unwrap();
333 }
334 assert!(initrd_path.exists(), "{initrd_path:?} does not exist");
335 }
336
337 if let Some(rootfs_url) = &cfg.rootfs_url {
338 let rootfs_download_path = local_path_from_url(rootfs_url);
339 if !rootfs_download_path.exists() && rootfs_url.scheme() != "file" {
340 download_file(rootfs_url.as_str(), &rootfs_download_path).unwrap();
341 }
342 assert!(
343 rootfs_download_path.exists(),
344 "{rootfs_download_path:?} does not exist"
345 );
346
347 if cfg.rootfs_compressed {
348 let rootfs_raw_path = rootfs_download_path.with_extension("raw");
349 Command::new("zstd")
350 .arg("-d")
351 .arg(&rootfs_download_path)
352 .arg("-o")
353 .arg(&rootfs_raw_path)
354 .arg("-f")
355 .output()
356 .expect("Failed to decompress rootfs");
357 TestVmSys::check_rootfs_file(&rootfs_raw_path);
358 } else {
359 TestVmSys::check_rootfs_file(&rootfs_download_path);
360 }
361 }
362 }
363
364 pub fn new_generic<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm>
370 where
371 F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
372 {
373 PREP_ONCE.call_once(TestVm::initialize_once);
374
375 TestVm::initiailize_artifacts(&cfg);
376
377 let mut vm = TestVm {
378 sys: TestVmSys::new_generic(f, cfg, sudo).with_context(|| "Could not start crosvm")?,
379 ready: false,
380 sudo,
381 };
382 vm.wait_for_guest_ready(BOOT_TIMEOUT)
383 .with_context(|| "Guest did not become ready after boot")?;
384 Ok(vm)
385 }
386
387 pub fn new_generic_restore<F>(f: F, cfg: Config, sudo: bool) -> Result<TestVm>
388 where
389 F: FnOnce(&mut Command, &SerialArgs, &Config) -> Result<()>,
390 {
391 PREP_ONCE.call_once(TestVm::initialize_once);
392 let mut vm = TestVm {
393 sys: TestVmSys::new_generic(f, cfg, sudo).with_context(|| "Could not start crosvm")?,
394 ready: false,
395 sudo,
396 };
397 vm.ready = true;
398 Ok(vm)
400 }
401
402 pub fn new(cfg: Config) -> Result<TestVm> {
403 TestVm::new_generic(TestVmSys::append_config_args, cfg, false)
404 }
405
406 pub fn new_restore(cfg: Config) -> Result<TestVm> {
408 let mut vm = TestVm::new_generic_restore(TestVmSys::append_config_args, cfg, false)?;
409 let vm = run_with_timeout(
414 move || {
415 vm.resume_full().expect("failed to resume after VM restore");
416 vm
417 },
418 Duration::from_secs(60),
419 )
420 .expect("VM restore timeout");
421
422 Ok(vm)
423 }
424
425 pub fn new_restore_suspended(cfg: Config) -> Result<TestVm> {
427 TestVm::new_generic_restore(TestVmSys::append_config_args, cfg, false)
428 }
429
430 pub fn new_sudo(cfg: Config) -> Result<TestVm> {
431 check_can_sudo();
432
433 TestVm::new_generic(TestVmSys::append_config_args, cfg, true)
434 }
435
436 pub fn exec_in_guest(&mut self, command: &str) -> Result<ProgramExit> {
439 self.exec_in_guest_async(command)?.wait_ok(self)
440 }
441
442 pub fn exec_in_guest_unchecked(&mut self, command: &str) -> Result<ProgramExit> {
445 self.exec_in_guest_async(command)?.wait_result(self)
446 }
447
448 pub fn exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess> {
452 assert!(self.ready);
453 self.ready = false;
454
455 self.write_message_to_guest(
457 &HostToGuestMessage::RunCommand {
458 command: command.to_owned(),
459 },
460 COMMUNICATION_TIMEOUT,
461 )
462 .with_context(|| format!("Command `{command}`: Failed to write to guest pipe"))?;
463
464 Ok(GuestProcess {
465 command: command.to_owned(),
466 timeout: DEFAULT_COMMAND_TIMEOUT,
467 })
468 }
469
470 fn wait_for_guest_ready(&mut self, timeout: Duration) -> Result<()> {
472 assert!(!self.ready);
473 let message: GuestToHostMessage = self.read_message_from_guest(timeout)?;
474 match message {
475 GuestToHostMessage::Ready => {
476 self.ready = true;
477 Ok(())
478 }
479 _ => Err(anyhow!("Recevied unexpected data from delegate")),
480 }
481 }
482
483 fn read_message_from_guest(&mut self, timeout: Duration) -> Result<GuestToHostMessage> {
485 let reader = self.sys.from_guest_reader.clone();
486
487 let result = run_with_timeout(
488 move || loop {
489 let message = { reader.lock().unwrap().next() };
490
491 if let Some(message_result) = message {
492 if let Ok(msg) = message_result {
493 match msg {
494 DelegateMessage::GuestToHost(guest_to_host) => {
495 return Ok(guest_to_host);
496 }
497 DelegateMessage::HostToGuest(_) => {
499 continue;
500 }
501 }
502 } else {
503 bail!(format!(
504 "Failed to receive message from guest: {:?}",
505 message_result.unwrap_err()
506 ))
507 };
508 };
509 },
510 timeout,
511 );
512 match result {
513 Ok(x) => {
514 self.ready = true;
515 x
516 }
517 Err(x) => Err(x),
518 }
519 }
520
521 fn write_message_to_guest(
523 &mut self,
524 data: &HostToGuestMessage,
525 timeout: Duration,
526 ) -> Result<()> {
527 let writer = self.sys.to_guest.clone();
528 let data_str = serde_json::to_string_pretty(&DelegateMessage::HostToGuest(data.clone()))?;
529 run_with_timeout(
530 move || -> Result<()> {
531 println!("-> {}", &data_str);
532 {
533 writeln!(writer.lock().unwrap(), "{}", &data_str)?;
534 }
535 Ok(())
536 },
537 timeout,
538 )?
539 }
540
541 pub fn hotplug_tap(&mut self, tap_name: &str) -> Result<()> {
543 self.sys
544 .crosvm_command(
545 "virtio-net",
546 vec!["add".to_owned(), tap_name.to_owned()],
547 self.sudo,
548 )
549 .map(|_| ())
550 }
551
552 pub fn remove_pci_device(&mut self, bus_num: u8) -> Result<()> {
554 self.sys
555 .crosvm_command(
556 "virtio-net",
557 vec!["remove".to_owned(), bus_num.to_string()],
558 self.sudo,
559 )
560 .map(|_| ())
561 }
562
563 pub fn stop(&mut self) -> Result<()> {
564 self.sys
565 .crosvm_command("stop", vec![], self.sudo)
566 .map(|_| ())
567 }
568
569 pub fn suspend(&mut self) -> Result<()> {
570 self.sys
571 .crosvm_command("suspend", vec![], self.sudo)
572 .map(|_| ())
573 }
574
575 pub fn suspend_full(&mut self) -> Result<()> {
576 self.sys
577 .crosvm_command("suspend", vec!["--full".to_string()], self.sudo)
578 .map(|_| ())
579 }
580
581 pub fn resume(&mut self) -> Result<()> {
582 self.sys
583 .crosvm_command("resume", vec![], self.sudo)
584 .map(|_| ())
585 }
586
587 pub fn resume_full(&mut self) -> Result<()> {
588 self.sys
589 .crosvm_command("resume", vec!["--full".to_string()], self.sudo)
590 .map(|_| ())
591 }
592
593 pub fn disk(&mut self, args: Vec<String>) -> Result<()> {
594 self.sys.crosvm_command("disk", args, self.sudo).map(|_| ())
595 }
596
597 pub fn snapshot(&mut self, filename: &std::path::Path) -> Result<()> {
598 self.sys
599 .crosvm_command(
600 "snapshot",
601 vec!["take".to_string(), String::from(filename.to_str().unwrap())],
602 self.sudo,
603 )
604 .map(|_| ())
605 }
606
607 pub fn restore(&mut self, filename: &std::path::Path) -> Result<()> {
609 self.sys
610 .crosvm_command(
611 "snapshot",
612 vec![
613 "restore".to_string(),
614 String::from(filename.to_str().unwrap()),
615 ],
616 self.sudo,
617 )
618 .map(|_| ())
619 }
620
621 pub fn swap_command(&mut self, command: &str) -> Result<Vec<u8>> {
622 self.sys
623 .crosvm_command("swap", vec![command.to_string()], self.sudo)
624 }
625
626 pub fn guest_clock_values(&mut self) -> Result<ClockValues> {
627 let output = self
628 .exec_in_guest("readclock")
629 .context("Failed to execute readclock binary")?;
630 serde_json::from_str(&output.stdout).context("Failed to parse result")
631 }
632}
633
634impl Drop for TestVm {
635 fn drop(&mut self) {
636 self.stop().unwrap();
637 let status = self.sys.process.take().unwrap().wait().unwrap();
638 if !status.success() {
639 panic!("VM exited illegally: {status}");
640 }
641 }
642}