Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
google
GitHub Repository: google/crosvm
Path: blob/main/e2e_tests/fixture/src/vm.rs
5394 views
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::Write;
7
use std::path::Path;
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 base::test_utils::check_can_sudo;
19
use crc32fast::hash;
20
use delegate::wire_format::DelegateMessage;
21
use delegate::wire_format::ExitStatus;
22
use delegate::wire_format::GuestToHostMessage;
23
use delegate::wire_format::HostToGuestMessage;
24
use delegate::wire_format::ProgramExit;
25
use log::info;
26
use log::Level;
27
use prebuilts::download_file;
28
use readclock::ClockValues;
29
use url::Url;
30
31
use crate::sys::SerialArgs;
32
use crate::sys::TestVmSys;
33
use crate::utils::run_with_timeout;
34
35
const PREBUILT_URL: &str = "https://storage.googleapis.com/crosvm/integration_tests";
36
37
#[cfg(target_arch = "x86_64")]
38
const ARCH: &str = "x86_64";
39
#[cfg(target_arch = "aarch64")]
40
const ARCH: &str = "aarch64";
41
#[cfg(target_arch = "riscv64")]
42
const ARCH: &str = "riscv64";
43
44
/// Timeout when waiting for pipes that are expected to be ready.
45
const COMMUNICATION_TIMEOUT: Duration = Duration::from_secs(5);
46
47
/// Timeout for the VM to boot and the delegate to report that it's ready.
48
const BOOT_TIMEOUT: Duration = Duration::from_secs(60);
49
50
/// Default timeout when waiting for guest commands to execute
51
const DEFAULT_COMMAND_TIMEOUT: Duration = Duration::from_secs(10);
52
53
fn prebuilt_version() -> &'static str {
54
include_str!("../../guest_under_test/PREBUILT_VERSION").trim()
55
}
56
57
fn 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
67
fn 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
77
pub(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]
93
pub struct GuestProcess {
94
command: String,
95
timeout: Duration,
96
}
97
98
impl 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`.
140
pub 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
175
impl 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
193
impl 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
285
static 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.
291
pub 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
300
impl 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
634
impl 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
}
643
644