fixture/
vm.rs

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
5use 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
44/// Timeout when waiting for pipes that are expected to be ready.
45const COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(5);
46
47/// Timeout for the VM to boot and the delegate to report that it's ready.
48const BOOT_TIMEOUT: Duration = Duration::from_secs(60);
49
50/// Default timeout when waiting for guest commands to execute
51const 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/// Represents a command running in the guest. See `TestVm::exec_in_guest_async()`
92#[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    /// Waits for the process to finish execution and return ExitStatus.
107    /// Will fail on a non-zero exit code.
108    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    /// Same as `wait_ok` but will return a ExitStatus instead of failing on a non-zero exit code,
123    /// will only fail when cannot receive output from guest.
124    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        // VM is ready when receiving any message (as for current protocol)
132        match message {
133            GuestToHostMessage::ProgramExit(exit_info) => Ok(exit_info),
134            _ => bail!("Receive other message when anticipating ProgramExit"),
135        }
136    }
137}
138
139/// Configuration to start `TestVm`.
140pub struct Config {
141    /// Extra arguments for the `run` subcommand.
142    pub(super) extra_args: Vec<String>,
143
144    /// Use `O_DIRECT` for the rootfs.
145    pub(super) o_direct: bool,
146
147    /// Log level of `TestVm`
148    pub(super) log_level: Level,
149
150    /// File to save crosvm log to
151    pub(super) log_file: Option<String>,
152
153    /// Wrapper command line for executing `TestVM`
154    pub(super) wrapper_cmd: Option<String>,
155
156    /// Url to kernel image
157    pub(super) kernel_url: Url,
158
159    /// Url to initrd image
160    pub(super) initrd_url: Option<Url>,
161
162    /// Url to rootfs image
163    pub(super) rootfs_url: Option<Url>,
164
165    /// If rootfs image is writable
166    pub(super) rootfs_rw: bool,
167
168    /// If rootfs image is zstd compressed
169    pub(super) rootfs_compressed: bool,
170
171    /// Console hardware type
172    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    /// Creates a new `run` command with `extra_args`.
195    pub fn new() -> Self {
196        Self::from_env()
197    }
198
199    /// Uses extra arguments for `crosvm run`.
200    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    /// Uses `O_DIRECT` for the rootfs.
207    pub fn o_direct(mut self) -> Self {
208        self.o_direct = true;
209        self
210    }
211
212    /// Uses `disable-sandbox` argument for `crosvm run`.
213    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
287/// Test fixture to spin up a VM running a guest that can be communicated with.
288///
289/// After creation, commands can be sent via exec_in_guest. The VM is stopped
290/// when this instance is dropped.
291pub struct TestVm {
292    // Platform-dependent bits
293    sys: TestVmSys,
294    // The guest is ready to receive a command.
295    ready: bool,
296    // True if commands should be ran with `sudo`.
297    sudo: bool,
298}
299
300impl TestVm {
301    /// Downloads prebuilts if needed.
302    fn initialize_once() {
303        if let Err(e) = syslog::init() {
304            panic!("failed to initiailize syslog: {e}");
305        }
306
307        // It's possible the prebuilts downloaded by crosvm-9999.ebuild differ
308        // from the version that crosvm was compiled for.
309        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    /// Instanciate a new crosvm instance. The first call will trigger the download of prebuilt
365    /// files if necessary.
366    ///
367    /// This generic method takes a `FnOnce` argument which is in charge of completing the `Command`
368    /// with all the relevant options needed to boot the VM.
369    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        // TODO(b/280607404): A cold restored VM cannot respond to cmds from `exec_in_guest_async`.
399        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    /// Create `TestVm` from a snapshot, using `--restore` but NOT `--suspended`.
407    pub fn new_restore(cfg: Config) -> Result<TestVm> {
408        let mut vm = TestVm::new_generic_restore(TestVmSys::append_config_args, cfg, false)?;
409        // Send a resume request to wait for the restore to finish.
410        // We don't want to return from this function until the restore is complete, otherwise it
411        // will be difficult to differentiate between a slow restore and a slow response from the
412        // guest.
413        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    /// Create `TestVm` from a snapshot, using `--restore` AND `--suspended`.
426    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    /// Executes the provided command in the guest.
437    /// Returns command output as Ok(ProgramExit), or an Error if the program did not exit with 0.
438    pub fn exec_in_guest(&mut self, command: &str) -> Result<ProgramExit> {
439        self.exec_in_guest_async(command)?.wait_ok(self)
440    }
441
442    /// Same as `exec_in_guest` but will return Ok(ProgramExit) instead of failing on a
443    /// non-zero exit code.
444    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    /// Executes the provided command in the guest asynchronously.
449    /// The command will be run in the guest, but output will not be read until
450    /// GuestProcess::wait_ok() or GuestProcess::wait_result() is called.
451    pub fn exec_in_guest_async(&mut self, command: &str) -> Result<GuestProcess> {
452        assert!(self.ready);
453        self.ready = false;
454
455        // Send command to guest
456        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    // Waits for the guest to be ready to receive commands
471    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    /// Reads one line via the `from_guest` pipe from the guest delegate.
484    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                            // Guest will send an echo of the message sent from host, ignore it
498                            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    /// Send one line via the `to_guest` pipe to the guest delegate.
522    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    /// Hotplug a tap device.
542    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    /// Remove hotplugged device on bus.
553    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    // No argument is passed in restore as we will always restore snapshot.bkp for testing.
608    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}