1 // Copyright © 2020 Intel Corporation 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 #![allow(clippy::undocumented_unsafe_blocks)] 6 // When enabling the `mshv` feature, we skip quite some tests and 7 // hence have known dead-code. This annotation silences dead-code 8 // related warnings for our quality workflow to pass. 9 #![allow(dead_code)] 10 11 extern crate test_infra; 12 13 use std::collections::HashMap; 14 use std::io::{BufRead, Read, Seek, Write}; 15 use std::os::unix::io::AsRawFd; 16 use std::path::PathBuf; 17 use std::process::{Child, Command, Stdio}; 18 use std::string::String; 19 use std::sync::mpsc::Receiver; 20 use std::sync::{mpsc, Mutex}; 21 use std::{fs, io, thread}; 22 23 use net_util::MacAddr; 24 use test_infra::*; 25 use vmm_sys_util::tempdir::TempDir; 26 use vmm_sys_util::tempfile::TempFile; 27 use wait_timeout::ChildExt; 28 29 // Constant taken from the VMM crate. 30 const MAX_NUM_PCI_SEGMENTS: u16 = 96; 31 32 #[cfg(target_arch = "x86_64")] 33 mod x86_64 { 34 pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-amd64-custom-20210609-0.raw"; 35 pub const JAMMY_VFIO_IMAGE_NAME: &str = 36 "jammy-server-cloudimg-amd64-custom-vfio-20241012-0.raw"; 37 pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-amd64-custom-20210609-0.qcow2"; 38 pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str = 39 "focal-server-cloudimg-amd64-custom-20210609-0-backing.qcow2"; 40 pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhd"; 41 pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-amd64-custom-20210609-0.vhdx"; 42 pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-amd64-custom-20241017-0.raw"; 43 pub const WINDOWS_IMAGE_NAME: &str = "windows-server-2022-amd64-2.raw"; 44 pub const OVMF_NAME: &str = "CLOUDHV.fd"; 45 pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'IO-APIC.*ttyS0' /proc/interrupts || true"; 46 } 47 48 #[cfg(target_arch = "x86_64")] 49 use x86_64::*; 50 51 #[cfg(target_arch = "aarch64")] 52 mod aarch64 { 53 pub const FOCAL_IMAGE_NAME: &str = "focal-server-cloudimg-arm64-custom-20210929-0.raw"; 54 pub const FOCAL_IMAGE_UPDATE_KERNEL_NAME: &str = 55 "focal-server-cloudimg-arm64-custom-20210929-0-update-kernel.raw"; 56 pub const FOCAL_IMAGE_NAME_QCOW2: &str = "focal-server-cloudimg-arm64-custom-20210929-0.qcow2"; 57 pub const FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE: &str = 58 "focal-server-cloudimg-arm64-custom-20210929-0-backing.qcow2"; 59 pub const FOCAL_IMAGE_NAME_VHD: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhd"; 60 pub const FOCAL_IMAGE_NAME_VHDX: &str = "focal-server-cloudimg-arm64-custom-20210929-0.vhdx"; 61 pub const JAMMY_IMAGE_NAME: &str = "jammy-server-cloudimg-arm64-custom-20220329-0.raw"; 62 pub const WINDOWS_IMAGE_NAME: &str = "windows-11-iot-enterprise-aarch64.raw"; 63 pub const OVMF_NAME: &str = "CLOUDHV_EFI.fd"; 64 pub const GREP_SERIAL_IRQ_CMD: &str = "grep -c 'GICv3.*uart-pl011' /proc/interrupts || true"; 65 pub const GREP_PMU_IRQ_CMD: &str = "grep -c 'GICv3.*arm-pmu' /proc/interrupts || true"; 66 } 67 68 #[cfg(target_arch = "aarch64")] 69 use aarch64::*; 70 71 const DIRECT_KERNEL_BOOT_CMDLINE: &str = 72 "root=/dev/vda1 console=hvc0 rw systemd.journald.forward_to_console=1"; 73 74 const CONSOLE_TEST_STRING: &str = "Started OpenBSD Secure Shell server"; 75 76 // This enum exists to make it more convenient to 77 // implement test for both D-Bus and REST APIs. 78 enum TargetApi { 79 // API socket 80 HttpApi(String), 81 // well known service name, object path 82 DBusApi(String, String), 83 } 84 85 impl TargetApi { 86 fn new_http_api(tmp_dir: &TempDir) -> Self { 87 Self::HttpApi(temp_api_path(tmp_dir)) 88 } 89 90 fn new_dbus_api(tmp_dir: &TempDir) -> Self { 91 // `tmp_dir` is in the form of "/tmp/chXXXXXX" 92 // and we take the `chXXXXXX` part as a unique identifier for the guest 93 let id = tmp_dir.as_path().file_name().unwrap().to_str().unwrap(); 94 95 Self::DBusApi( 96 format!("org.cloudhypervisor.{id}"), 97 format!("/org/cloudhypervisor/{id}"), 98 ) 99 } 100 101 fn guest_args(&self) -> Vec<String> { 102 match self { 103 TargetApi::HttpApi(api_socket) => { 104 vec![format!("--api-socket={}", api_socket.as_str())] 105 } 106 TargetApi::DBusApi(service_name, object_path) => { 107 vec![ 108 format!("--dbus-service-name={}", service_name.as_str()), 109 format!("--dbus-object-path={}", object_path.as_str()), 110 ] 111 } 112 } 113 } 114 115 fn remote_args(&self) -> Vec<String> { 116 // `guest_args` and `remote_args` are consistent with each other 117 self.guest_args() 118 } 119 120 fn remote_command(&self, command: &str, arg: Option<&str>) -> bool { 121 let mut cmd = Command::new(clh_command("ch-remote")); 122 cmd.args(self.remote_args()); 123 cmd.arg(command); 124 125 if let Some(arg) = arg { 126 cmd.arg(arg); 127 } 128 129 let output = cmd.output().unwrap(); 130 if output.status.success() { 131 true 132 } else { 133 eprintln!("Error running ch-remote command: {:?}", &cmd); 134 let stderr = String::from_utf8_lossy(&output.stderr); 135 eprintln!("stderr: {stderr}"); 136 false 137 } 138 } 139 } 140 141 // Start cloud-hypervisor with no VM parameters, only the API server running. 142 // From the API: Create a VM, boot it and check that it looks as expected. 143 fn _test_api_create_boot(target_api: TargetApi, guest: Guest) { 144 let mut child = GuestCommand::new(&guest) 145 .args(target_api.guest_args()) 146 .capture_output() 147 .spawn() 148 .unwrap(); 149 150 thread::sleep(std::time::Duration::new(1, 0)); 151 152 // Verify API server is running 153 assert!(target_api.remote_command("ping", None)); 154 155 // Create the VM first 156 let cpu_count: u8 = 4; 157 let request_body = guest.api_create_body( 158 cpu_count, 159 direct_kernel_boot_path().to_str().unwrap(), 160 DIRECT_KERNEL_BOOT_CMDLINE, 161 ); 162 163 let temp_config_path = guest.tmp_dir.as_path().join("config"); 164 std::fs::write(&temp_config_path, request_body).unwrap(); 165 let create_config = temp_config_path.as_os_str().to_str().unwrap(); 166 167 assert!(target_api.remote_command("create", Some(create_config),)); 168 169 // Then boot it 170 assert!(target_api.remote_command("boot", None)); 171 thread::sleep(std::time::Duration::new(20, 0)); 172 173 let r = std::panic::catch_unwind(|| { 174 // Check that the VM booted as expected 175 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 176 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 177 }); 178 179 kill_child(&mut child); 180 let output = child.wait_with_output().unwrap(); 181 182 handle_child_output(r, &output); 183 } 184 185 // Start cloud-hypervisor with no VM parameters, only the API server running. 186 // From the API: Create a VM, boot it and check it can be shutdown and then 187 // booted again 188 fn _test_api_shutdown(target_api: TargetApi, guest: Guest) { 189 let mut child = GuestCommand::new(&guest) 190 .args(target_api.guest_args()) 191 .capture_output() 192 .spawn() 193 .unwrap(); 194 195 thread::sleep(std::time::Duration::new(1, 0)); 196 197 // Verify API server is running 198 assert!(target_api.remote_command("ping", None)); 199 200 // Create the VM first 201 let cpu_count: u8 = 4; 202 let request_body = guest.api_create_body( 203 cpu_count, 204 direct_kernel_boot_path().to_str().unwrap(), 205 DIRECT_KERNEL_BOOT_CMDLINE, 206 ); 207 208 let temp_config_path = guest.tmp_dir.as_path().join("config"); 209 std::fs::write(&temp_config_path, request_body).unwrap(); 210 let create_config = temp_config_path.as_os_str().to_str().unwrap(); 211 212 let r = std::panic::catch_unwind(|| { 213 assert!(target_api.remote_command("create", Some(create_config))); 214 215 // Then boot it 216 assert!(target_api.remote_command("boot", None)); 217 218 guest.wait_vm_boot(None).unwrap(); 219 220 // Check that the VM booted as expected 221 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 222 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 223 224 // Sync and shutdown without powering off to prevent filesystem 225 // corruption. 226 guest.ssh_command("sync").unwrap(); 227 guest.ssh_command("sudo shutdown -H now").unwrap(); 228 229 // Wait for the guest to be fully shutdown 230 thread::sleep(std::time::Duration::new(20, 0)); 231 232 // Then shut it down 233 assert!(target_api.remote_command("shutdown", None)); 234 235 // Then boot it again 236 assert!(target_api.remote_command("boot", None)); 237 238 guest.wait_vm_boot(None).unwrap(); 239 240 // Check that the VM booted as expected 241 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 242 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 243 }); 244 245 kill_child(&mut child); 246 let output = child.wait_with_output().unwrap(); 247 248 handle_child_output(r, &output); 249 } 250 251 // Start cloud-hypervisor with no VM parameters, only the API server running. 252 // From the API: Create a VM, boot it and check it can be deleted and then recreated 253 // booted again. 254 fn _test_api_delete(target_api: TargetApi, guest: Guest) { 255 let mut child = GuestCommand::new(&guest) 256 .args(target_api.guest_args()) 257 .capture_output() 258 .spawn() 259 .unwrap(); 260 261 thread::sleep(std::time::Duration::new(1, 0)); 262 263 // Verify API server is running 264 assert!(target_api.remote_command("ping", None)); 265 266 // Create the VM first 267 let cpu_count: u8 = 4; 268 let request_body = guest.api_create_body( 269 cpu_count, 270 direct_kernel_boot_path().to_str().unwrap(), 271 DIRECT_KERNEL_BOOT_CMDLINE, 272 ); 273 let temp_config_path = guest.tmp_dir.as_path().join("config"); 274 std::fs::write(&temp_config_path, request_body).unwrap(); 275 let create_config = temp_config_path.as_os_str().to_str().unwrap(); 276 277 let r = std::panic::catch_unwind(|| { 278 assert!(target_api.remote_command("create", Some(create_config))); 279 280 // Then boot it 281 assert!(target_api.remote_command("boot", None)); 282 283 guest.wait_vm_boot(None).unwrap(); 284 285 // Check that the VM booted as expected 286 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 287 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 288 289 // Sync and shutdown without powering off to prevent filesystem 290 // corruption. 291 guest.ssh_command("sync").unwrap(); 292 guest.ssh_command("sudo shutdown -H now").unwrap(); 293 294 // Wait for the guest to be fully shutdown 295 thread::sleep(std::time::Duration::new(20, 0)); 296 297 // Then delete it 298 assert!(target_api.remote_command("delete", None)); 299 300 assert!(target_api.remote_command("create", Some(create_config))); 301 302 // Then boot it again 303 assert!(target_api.remote_command("boot", None)); 304 305 guest.wait_vm_boot(None).unwrap(); 306 307 // Check that the VM booted as expected 308 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 309 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 310 }); 311 312 kill_child(&mut child); 313 let output = child.wait_with_output().unwrap(); 314 315 handle_child_output(r, &output); 316 } 317 318 // Start cloud-hypervisor with no VM parameters, only the API server running. 319 // From the API: Create a VM, boot it and check that it looks as expected. 320 // Then we pause the VM, check that it's no longer available. 321 // Finally we resume the VM and check that it's available. 322 fn _test_api_pause_resume(target_api: TargetApi, guest: Guest) { 323 let mut child = GuestCommand::new(&guest) 324 .args(target_api.guest_args()) 325 .capture_output() 326 .spawn() 327 .unwrap(); 328 329 thread::sleep(std::time::Duration::new(1, 0)); 330 331 // Verify API server is running 332 assert!(target_api.remote_command("ping", None)); 333 334 // Create the VM first 335 let cpu_count: u8 = 4; 336 let request_body = guest.api_create_body( 337 cpu_count, 338 direct_kernel_boot_path().to_str().unwrap(), 339 DIRECT_KERNEL_BOOT_CMDLINE, 340 ); 341 342 let temp_config_path = guest.tmp_dir.as_path().join("config"); 343 std::fs::write(&temp_config_path, request_body).unwrap(); 344 let create_config = temp_config_path.as_os_str().to_str().unwrap(); 345 346 assert!(target_api.remote_command("create", Some(create_config))); 347 348 // Then boot it 349 assert!(target_api.remote_command("boot", None)); 350 thread::sleep(std::time::Duration::new(20, 0)); 351 352 let r = std::panic::catch_unwind(|| { 353 // Check that the VM booted as expected 354 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 355 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 356 357 // We now pause the VM 358 assert!(target_api.remote_command("pause", None)); 359 360 // Check pausing again fails 361 assert!(!target_api.remote_command("pause", None)); 362 363 thread::sleep(std::time::Duration::new(2, 0)); 364 365 // SSH into the VM should fail 366 ssh_command_ip( 367 "grep -c processor /proc/cpuinfo", 368 &guest.network.guest_ip, 369 2, 370 5, 371 ) 372 .unwrap_err(); 373 374 // Resume the VM 375 assert!(target_api.remote_command("resume", None)); 376 377 // Check resuming again fails 378 assert!(!target_api.remote_command("resume", None)); 379 380 thread::sleep(std::time::Duration::new(2, 0)); 381 382 // Now we should be able to SSH back in and get the right number of CPUs 383 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 384 }); 385 386 kill_child(&mut child); 387 let output = child.wait_with_output().unwrap(); 388 389 handle_child_output(r, &output); 390 } 391 392 fn _test_pty_interaction(pty_path: PathBuf) { 393 let mut cf = std::fs::OpenOptions::new() 394 .write(true) 395 .read(true) 396 .open(pty_path) 397 .unwrap(); 398 399 // Some dumb sleeps but we don't want to write 400 // before the console is up and we don't want 401 // to try and write the next line before the 402 // login process is ready. 403 thread::sleep(std::time::Duration::new(5, 0)); 404 assert_eq!(cf.write(b"cloud\n").unwrap(), 6); 405 thread::sleep(std::time::Duration::new(2, 0)); 406 assert_eq!(cf.write(b"cloud123\n").unwrap(), 9); 407 thread::sleep(std::time::Duration::new(2, 0)); 408 assert_eq!(cf.write(b"echo test_pty_console\n").unwrap(), 22); 409 thread::sleep(std::time::Duration::new(2, 0)); 410 411 // read pty and ensure they have a login shell 412 // some fairly hacky workarounds to avoid looping 413 // forever in case the channel is blocked getting output 414 let ptyc = pty_read(cf); 415 let mut empty = 0; 416 let mut prev = String::new(); 417 loop { 418 thread::sleep(std::time::Duration::new(2, 0)); 419 match ptyc.try_recv() { 420 Ok(line) => { 421 empty = 0; 422 prev = prev + &line; 423 if prev.contains("test_pty_console") { 424 break; 425 } 426 } 427 Err(mpsc::TryRecvError::Empty) => { 428 empty += 1; 429 assert!(empty <= 5, "No login on pty"); 430 } 431 _ => { 432 panic!("No login on pty") 433 } 434 } 435 } 436 } 437 438 fn prepare_virtiofsd(tmp_dir: &TempDir, shared_dir: &str) -> (std::process::Child, String) { 439 let mut workload_path = dirs::home_dir().unwrap(); 440 workload_path.push("workloads"); 441 442 let mut virtiofsd_path = workload_path; 443 virtiofsd_path.push("virtiofsd"); 444 let virtiofsd_path = String::from(virtiofsd_path.to_str().unwrap()); 445 446 let virtiofsd_socket_path = 447 String::from(tmp_dir.as_path().join("virtiofs.sock").to_str().unwrap()); 448 449 // Start the daemon 450 let child = Command::new(virtiofsd_path.as_str()) 451 .args(["--shared-dir", shared_dir]) 452 .args(["--socket-path", virtiofsd_socket_path.as_str()]) 453 .args(["--cache", "never"]) 454 .spawn() 455 .unwrap(); 456 457 thread::sleep(std::time::Duration::new(10, 0)); 458 459 (child, virtiofsd_socket_path) 460 } 461 462 fn prepare_vubd( 463 tmp_dir: &TempDir, 464 blk_img: &str, 465 num_queues: usize, 466 rdonly: bool, 467 direct: bool, 468 ) -> (std::process::Child, String) { 469 let mut workload_path = dirs::home_dir().unwrap(); 470 workload_path.push("workloads"); 471 472 let mut blk_file_path = workload_path; 473 blk_file_path.push(blk_img); 474 let blk_file_path = String::from(blk_file_path.to_str().unwrap()); 475 476 let vubd_socket_path = String::from(tmp_dir.as_path().join("vub.sock").to_str().unwrap()); 477 478 // Start the daemon 479 let child = Command::new(clh_command("vhost_user_block")) 480 .args([ 481 "--block-backend", 482 format!( 483 "path={blk_file_path},socket={vubd_socket_path},num_queues={num_queues},readonly={rdonly},direct={direct}" 484 ) 485 .as_str(), 486 ]) 487 .spawn() 488 .unwrap(); 489 490 thread::sleep(std::time::Duration::new(10, 0)); 491 492 (child, vubd_socket_path) 493 } 494 495 fn temp_vsock_path(tmp_dir: &TempDir) -> String { 496 String::from(tmp_dir.as_path().join("vsock").to_str().unwrap()) 497 } 498 499 fn temp_api_path(tmp_dir: &TempDir) -> String { 500 String::from( 501 tmp_dir 502 .as_path() 503 .join("cloud-hypervisor.sock") 504 .to_str() 505 .unwrap(), 506 ) 507 } 508 509 fn temp_event_monitor_path(tmp_dir: &TempDir) -> String { 510 String::from(tmp_dir.as_path().join("event.json").to_str().unwrap()) 511 } 512 513 // Creates the directory and returns the path. 514 fn temp_snapshot_dir_path(tmp_dir: &TempDir) -> String { 515 let snapshot_dir = String::from(tmp_dir.as_path().join("snapshot").to_str().unwrap()); 516 std::fs::create_dir(&snapshot_dir).unwrap(); 517 snapshot_dir 518 } 519 520 fn temp_vmcore_file_path(tmp_dir: &TempDir) -> String { 521 let vmcore_file = String::from(tmp_dir.as_path().join("vmcore").to_str().unwrap()); 522 vmcore_file 523 } 524 525 // Creates the path for direct kernel boot and return the path. 526 // For x86_64, this function returns the vmlinux kernel path. 527 // For AArch64, this function returns the PE kernel path. 528 fn direct_kernel_boot_path() -> PathBuf { 529 let mut workload_path = dirs::home_dir().unwrap(); 530 workload_path.push("workloads"); 531 532 let mut kernel_path = workload_path; 533 #[cfg(target_arch = "x86_64")] 534 kernel_path.push("vmlinux"); 535 #[cfg(target_arch = "aarch64")] 536 kernel_path.push("Image"); 537 538 kernel_path 539 } 540 541 fn edk2_path() -> PathBuf { 542 let mut workload_path = dirs::home_dir().unwrap(); 543 workload_path.push("workloads"); 544 let mut edk2_path = workload_path; 545 edk2_path.push(OVMF_NAME); 546 547 edk2_path 548 } 549 550 fn cloud_hypervisor_release_path() -> String { 551 let mut workload_path = dirs::home_dir().unwrap(); 552 workload_path.push("workloads"); 553 554 let mut ch_release_path = workload_path; 555 #[cfg(target_arch = "x86_64")] 556 ch_release_path.push("cloud-hypervisor-static"); 557 #[cfg(target_arch = "aarch64")] 558 ch_release_path.push("cloud-hypervisor-static-aarch64"); 559 560 ch_release_path.into_os_string().into_string().unwrap() 561 } 562 563 fn prepare_vhost_user_net_daemon( 564 tmp_dir: &TempDir, 565 ip: &str, 566 tap: Option<&str>, 567 mtu: Option<u16>, 568 num_queues: usize, 569 client_mode: bool, 570 ) -> (std::process::Command, String) { 571 let vunet_socket_path = String::from(tmp_dir.as_path().join("vunet.sock").to_str().unwrap()); 572 573 // Start the daemon 574 let mut net_params = format!( 575 "ip={ip},mask=255.255.255.0,socket={vunet_socket_path},num_queues={num_queues},queue_size=1024,client={client_mode}" 576 ); 577 578 if let Some(tap) = tap { 579 net_params.push_str(format!(",tap={tap}").as_str()); 580 } 581 582 if let Some(mtu) = mtu { 583 net_params.push_str(format!(",mtu={mtu}").as_str()); 584 } 585 586 let mut command = Command::new(clh_command("vhost_user_net")); 587 command.args(["--net-backend", net_params.as_str()]); 588 589 (command, vunet_socket_path) 590 } 591 592 fn prepare_swtpm_daemon(tmp_dir: &TempDir) -> (std::process::Command, String) { 593 let swtpm_tpm_dir = String::from(tmp_dir.as_path().join("swtpm").to_str().unwrap()); 594 let swtpm_socket_path = String::from( 595 tmp_dir 596 .as_path() 597 .join("swtpm") 598 .join("swtpm.sock") 599 .to_str() 600 .unwrap(), 601 ); 602 std::fs::create_dir(&swtpm_tpm_dir).unwrap(); 603 604 let mut swtpm_command = Command::new("swtpm"); 605 let swtpm_args = [ 606 "socket", 607 "--tpmstate", 608 &format!("dir={swtpm_tpm_dir}"), 609 "--ctrl", 610 &format!("type=unixio,path={swtpm_socket_path}"), 611 "--flags", 612 "startup-clear", 613 "--tpm2", 614 ]; 615 swtpm_command.args(swtpm_args); 616 617 (swtpm_command, swtpm_socket_path) 618 } 619 620 fn remote_command(api_socket: &str, command: &str, arg: Option<&str>) -> bool { 621 let mut cmd = Command::new(clh_command("ch-remote")); 622 cmd.args([&format!("--api-socket={api_socket}"), command]); 623 624 if let Some(arg) = arg { 625 cmd.arg(arg); 626 } 627 let output = cmd.output().unwrap(); 628 if output.status.success() { 629 true 630 } else { 631 eprintln!("Error running ch-remote command: {:?}", &cmd); 632 let stderr = String::from_utf8_lossy(&output.stderr); 633 eprintln!("stderr: {stderr}"); 634 false 635 } 636 } 637 638 fn remote_command_w_output(api_socket: &str, command: &str, arg: Option<&str>) -> (bool, Vec<u8>) { 639 let mut cmd = Command::new(clh_command("ch-remote")); 640 cmd.args([&format!("--api-socket={api_socket}"), command]); 641 642 if let Some(arg) = arg { 643 cmd.arg(arg); 644 } 645 646 let output = cmd.output().expect("Failed to launch ch-remote"); 647 648 (output.status.success(), output.stdout) 649 } 650 651 fn resize_command( 652 api_socket: &str, 653 desired_vcpus: Option<u8>, 654 desired_ram: Option<usize>, 655 desired_balloon: Option<usize>, 656 event_file: Option<&str>, 657 ) -> bool { 658 let mut cmd = Command::new(clh_command("ch-remote")); 659 cmd.args([&format!("--api-socket={api_socket}"), "resize"]); 660 661 if let Some(desired_vcpus) = desired_vcpus { 662 cmd.arg(format!("--cpus={desired_vcpus}")); 663 } 664 665 if let Some(desired_ram) = desired_ram { 666 cmd.arg(format!("--memory={desired_ram}")); 667 } 668 669 if let Some(desired_balloon) = desired_balloon { 670 cmd.arg(format!("--balloon={desired_balloon}")); 671 } 672 673 let ret = cmd.status().expect("Failed to launch ch-remote").success(); 674 675 if let Some(event_path) = event_file { 676 let latest_events = [ 677 &MetaEvent { 678 event: "resizing".to_string(), 679 device_id: None, 680 }, 681 &MetaEvent { 682 event: "resized".to_string(), 683 device_id: None, 684 }, 685 ]; 686 // See: #5938 687 thread::sleep(std::time::Duration::new(1, 0)); 688 assert!(check_latest_events_exact(&latest_events, event_path)); 689 } 690 691 ret 692 } 693 694 fn resize_zone_command(api_socket: &str, id: &str, desired_size: &str) -> bool { 695 let mut cmd = Command::new(clh_command("ch-remote")); 696 cmd.args([ 697 &format!("--api-socket={api_socket}"), 698 "resize-zone", 699 &format!("--id={id}"), 700 &format!("--size={desired_size}"), 701 ]); 702 703 cmd.status().expect("Failed to launch ch-remote").success() 704 } 705 706 // setup OVS-DPDK bridge and ports 707 fn setup_ovs_dpdk() { 708 // setup OVS-DPDK 709 assert!(exec_host_command_status("service openvswitch-switch start").success()); 710 assert!(exec_host_command_status("ovs-vsctl init").success()); 711 assert!( 712 exec_host_command_status("ovs-vsctl set Open_vSwitch . other_config:dpdk-init=true") 713 .success() 714 ); 715 assert!(exec_host_command_status("service openvswitch-switch restart").success()); 716 717 // Create OVS-DPDK bridge and ports 718 assert!(exec_host_command_status( 719 "ovs-vsctl add-br ovsbr0 -- set bridge ovsbr0 datapath_type=netdev", 720 ) 721 .success()); 722 assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); 723 assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user2 -- set Interface vhost-user2 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient2").success()); 724 assert!(exec_host_command_status("ip link set up dev ovsbr0").success()); 725 assert!(exec_host_command_status("service openvswitch-switch restart").success()); 726 } 727 fn cleanup_ovs_dpdk() { 728 assert!(exec_host_command_status("ovs-vsctl del-br ovsbr0").success()); 729 exec_host_command_status("rm -f ovs-vsctl /tmp/dpdkvhostclient1 /tmp/dpdkvhostclient2"); 730 } 731 // Setup two guests and ensure they are connected through ovs-dpdk 732 fn setup_ovs_dpdk_guests( 733 guest1: &Guest, 734 guest2: &Guest, 735 api_socket: &str, 736 release_binary: bool, 737 ) -> (Child, Child) { 738 setup_ovs_dpdk(); 739 740 let clh_path = if !release_binary { 741 clh_command("cloud-hypervisor") 742 } else { 743 cloud_hypervisor_release_path() 744 }; 745 746 let mut child1 = GuestCommand::new_with_binary_path(guest1, &clh_path) 747 .args(["--cpus", "boot=2"]) 748 .args(["--memory", "size=0,shared=on"]) 749 .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) 750 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 751 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 752 .default_disks() 753 .args(["--net", guest1.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient1,num_queues=2,queue_size=256,vhost_mode=server"]) 754 .capture_output() 755 .spawn() 756 .unwrap(); 757 758 #[cfg(target_arch = "x86_64")] 759 let guest_net_iface = "ens5"; 760 #[cfg(target_arch = "aarch64")] 761 let guest_net_iface = "enp0s5"; 762 763 let r = std::panic::catch_unwind(|| { 764 guest1.wait_vm_boot(None).unwrap(); 765 766 guest1 767 .ssh_command(&format!( 768 "sudo ip addr add 172.100.0.1/24 dev {guest_net_iface}" 769 )) 770 .unwrap(); 771 guest1 772 .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) 773 .unwrap(); 774 775 let guest_ip = guest1.network.guest_ip.clone(); 776 thread::spawn(move || { 777 ssh_command_ip( 778 "nc -l 12345", 779 &guest_ip, 780 DEFAULT_SSH_RETRIES, 781 DEFAULT_SSH_TIMEOUT, 782 ) 783 .unwrap(); 784 }); 785 }); 786 if r.is_err() { 787 cleanup_ovs_dpdk(); 788 789 let _ = child1.kill(); 790 let output = child1.wait_with_output().unwrap(); 791 handle_child_output(r, &output); 792 panic!("Test should already be failed/panicked"); // To explicitly mark this block never return 793 } 794 795 let mut child2 = GuestCommand::new_with_binary_path(guest2, &clh_path) 796 .args(["--api-socket", api_socket]) 797 .args(["--cpus", "boot=2"]) 798 .args(["--memory", "size=0,shared=on"]) 799 .args(["--memory-zone", "id=mem0,size=1G,shared=on,host_numa_node=0"]) 800 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 801 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 802 .default_disks() 803 .args(["--net", guest2.default_net_string().as_str(), "vhost_user=true,socket=/tmp/dpdkvhostclient2,num_queues=2,queue_size=256,vhost_mode=server"]) 804 .capture_output() 805 .spawn() 806 .unwrap(); 807 808 let r = std::panic::catch_unwind(|| { 809 guest2.wait_vm_boot(None).unwrap(); 810 811 guest2 812 .ssh_command(&format!( 813 "sudo ip addr add 172.100.0.2/24 dev {guest_net_iface}" 814 )) 815 .unwrap(); 816 guest2 817 .ssh_command(&format!("sudo ip link set up dev {guest_net_iface}")) 818 .unwrap(); 819 820 // Check the connection works properly between the two VMs 821 guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); 822 }); 823 if r.is_err() { 824 cleanup_ovs_dpdk(); 825 826 let _ = child1.kill(); 827 let _ = child2.kill(); 828 let output = child2.wait_with_output().unwrap(); 829 handle_child_output(r, &output); 830 panic!("Test should already be failed/panicked"); // To explicitly mark this block never return 831 } 832 833 (child1, child2) 834 } 835 836 enum FwType { 837 Ovmf, 838 RustHypervisorFirmware, 839 } 840 841 fn fw_path(_fw_type: FwType) -> String { 842 let mut workload_path = dirs::home_dir().unwrap(); 843 workload_path.push("workloads"); 844 845 let mut fw_path = workload_path; 846 #[cfg(target_arch = "aarch64")] 847 fw_path.push("CLOUDHV_EFI.fd"); 848 #[cfg(target_arch = "x86_64")] 849 { 850 match _fw_type { 851 FwType::Ovmf => fw_path.push(OVMF_NAME), 852 FwType::RustHypervisorFirmware => fw_path.push("hypervisor-fw"), 853 } 854 } 855 856 fw_path.to_str().unwrap().to_string() 857 } 858 859 #[derive(Debug)] 860 struct MetaEvent { 861 event: String, 862 device_id: Option<String>, 863 } 864 865 impl MetaEvent { 866 pub fn match_with_json_event(&self, v: &serde_json::Value) -> bool { 867 let mut matched = false; 868 if v["event"].as_str().unwrap() == self.event { 869 if let Some(device_id) = &self.device_id { 870 if v["properties"]["id"].as_str().unwrap() == device_id { 871 matched = true 872 } 873 } else { 874 matched = true; 875 } 876 } 877 matched 878 } 879 } 880 881 // Parse the event_monitor file based on the format that each event 882 // is followed by a double newline 883 fn parse_event_file(event_file: &str) -> Vec<serde_json::Value> { 884 let content = fs::read(event_file).unwrap(); 885 let mut ret = Vec::new(); 886 for entry in String::from_utf8_lossy(&content) 887 .trim() 888 .split("\n\n") 889 .collect::<Vec<&str>>() 890 { 891 ret.push(serde_json::from_str(entry).unwrap()); 892 } 893 894 ret 895 } 896 897 // Return true if all events from the input 'expected_events' are matched sequentially 898 // with events from the 'event_file' 899 fn check_sequential_events(expected_events: &[&MetaEvent], event_file: &str) -> bool { 900 let json_events = parse_event_file(event_file); 901 let len = expected_events.len(); 902 let mut idx = 0; 903 for e in &json_events { 904 if idx == len { 905 break; 906 } 907 if expected_events[idx].match_with_json_event(e) { 908 idx += 1; 909 } 910 } 911 912 let ret = idx == len; 913 914 if !ret { 915 eprintln!( 916 "\n\n==== Start 'check_sequential_events' failed ==== \ 917 \n\nexpected_events={:?}\nactual_events={:?} \ 918 \n\n==== End 'check_sequential_events' failed ====", 919 expected_events, json_events, 920 ); 921 } 922 923 ret 924 } 925 926 // Return true if all events from the input 'expected_events' are matched exactly 927 // with events from the 'event_file' 928 fn check_sequential_events_exact(expected_events: &[&MetaEvent], event_file: &str) -> bool { 929 let json_events = parse_event_file(event_file); 930 assert!(expected_events.len() <= json_events.len()); 931 let json_events = &json_events[..expected_events.len()]; 932 933 for (idx, e) in json_events.iter().enumerate() { 934 if !expected_events[idx].match_with_json_event(e) { 935 eprintln!( 936 "\n\n==== Start 'check_sequential_events_exact' failed ==== \ 937 \n\nexpected_events={:?}\nactual_events={:?} \ 938 \n\n==== End 'check_sequential_events_exact' failed ====", 939 expected_events, json_events, 940 ); 941 942 return false; 943 } 944 } 945 946 true 947 } 948 949 // Return true if events from the input 'latest_events' are matched exactly 950 // with the most recent events from the 'event_file' 951 fn check_latest_events_exact(latest_events: &[&MetaEvent], event_file: &str) -> bool { 952 let json_events = parse_event_file(event_file); 953 assert!(latest_events.len() <= json_events.len()); 954 let json_events = &json_events[(json_events.len() - latest_events.len())..]; 955 956 for (idx, e) in json_events.iter().enumerate() { 957 if !latest_events[idx].match_with_json_event(e) { 958 eprintln!( 959 "\n\n==== Start 'check_latest_events_exact' failed ==== \ 960 \n\nexpected_events={:?}\nactual_events={:?} \ 961 \n\n==== End 'check_latest_events_exact' failed ====", 962 latest_events, json_events, 963 ); 964 965 return false; 966 } 967 } 968 969 true 970 } 971 972 fn test_cpu_topology(threads_per_core: u8, cores_per_package: u8, packages: u8, use_fw: bool) { 973 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 974 let guest = Guest::new(Box::new(focal)); 975 let total_vcpus = threads_per_core * cores_per_package * packages; 976 let direct_kernel_boot_path = direct_kernel_boot_path(); 977 let mut kernel_path = direct_kernel_boot_path.to_str().unwrap(); 978 let fw_path = fw_path(FwType::RustHypervisorFirmware); 979 if use_fw { 980 kernel_path = fw_path.as_str(); 981 } 982 983 let mut child = GuestCommand::new(&guest) 984 .args([ 985 "--cpus", 986 &format!( 987 "boot={total_vcpus},topology={threads_per_core}:{cores_per_package}:1:{packages}" 988 ), 989 ]) 990 .args(["--memory", "size=512M"]) 991 .args(["--kernel", kernel_path]) 992 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 993 .default_disks() 994 .default_net() 995 .capture_output() 996 .spawn() 997 .unwrap(); 998 999 let r = std::panic::catch_unwind(|| { 1000 guest.wait_vm_boot(None).unwrap(); 1001 assert_eq!( 1002 guest.get_cpu_count().unwrap_or_default(), 1003 u32::from(total_vcpus) 1004 ); 1005 assert_eq!( 1006 guest 1007 .ssh_command("lscpu | grep \"per core\" | cut -f 2 -d \":\" | sed \"s# *##\"") 1008 .unwrap() 1009 .trim() 1010 .parse::<u8>() 1011 .unwrap_or(0), 1012 threads_per_core 1013 ); 1014 1015 assert_eq!( 1016 guest 1017 .ssh_command("lscpu | grep \"per socket\" | cut -f 2 -d \":\" | sed \"s# *##\"") 1018 .unwrap() 1019 .trim() 1020 .parse::<u8>() 1021 .unwrap_or(0), 1022 cores_per_package 1023 ); 1024 1025 assert_eq!( 1026 guest 1027 .ssh_command("lscpu | grep \"Socket\" | cut -f 2 -d \":\" | sed \"s# *##\"") 1028 .unwrap() 1029 .trim() 1030 .parse::<u8>() 1031 .unwrap_or(0), 1032 packages 1033 ); 1034 1035 #[cfg(target_arch = "x86_64")] 1036 { 1037 let mut cpu_id = 0; 1038 for package_id in 0..packages { 1039 for core_id in 0..cores_per_package { 1040 for _ in 0..threads_per_core { 1041 assert_eq!( 1042 guest 1043 .ssh_command(&format!("cat /sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id")) 1044 .unwrap() 1045 .trim() 1046 .parse::<u8>() 1047 .unwrap_or(0), 1048 package_id 1049 ); 1050 1051 assert_eq!( 1052 guest 1053 .ssh_command(&format!( 1054 "cat /sys/devices/system/cpu/cpu{cpu_id}/topology/core_id" 1055 )) 1056 .unwrap() 1057 .trim() 1058 .parse::<u8>() 1059 .unwrap_or(0), 1060 core_id 1061 ); 1062 1063 cpu_id += 1; 1064 } 1065 } 1066 } 1067 } 1068 }); 1069 1070 kill_child(&mut child); 1071 let output = child.wait_with_output().unwrap(); 1072 1073 handle_child_output(r, &output); 1074 } 1075 1076 #[allow(unused_variables)] 1077 fn _test_guest_numa_nodes(acpi: bool) { 1078 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1079 let guest = Guest::new(Box::new(focal)); 1080 let api_socket = temp_api_path(&guest.tmp_dir); 1081 #[cfg(target_arch = "x86_64")] 1082 let kernel_path = direct_kernel_boot_path(); 1083 #[cfg(target_arch = "aarch64")] 1084 let kernel_path = if acpi { 1085 edk2_path() 1086 } else { 1087 direct_kernel_boot_path() 1088 }; 1089 1090 let mut child = GuestCommand::new(&guest) 1091 .args(["--cpus", "boot=6,max=12"]) 1092 .args(["--memory", "size=0,hotplug_method=virtio-mem"]) 1093 .args([ 1094 "--memory-zone", 1095 "id=mem0,size=1G,hotplug_size=3G", 1096 "id=mem1,size=2G,hotplug_size=3G", 1097 "id=mem2,size=3G,hotplug_size=3G", 1098 ]) 1099 .args([ 1100 "--numa", 1101 "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", 1102 "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", 1103 "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", 1104 ]) 1105 .args(["--kernel", kernel_path.to_str().unwrap()]) 1106 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1107 .args(["--api-socket", &api_socket]) 1108 .capture_output() 1109 .default_disks() 1110 .default_net() 1111 .spawn() 1112 .unwrap(); 1113 1114 let r = std::panic::catch_unwind(|| { 1115 guest.wait_vm_boot(None).unwrap(); 1116 1117 guest.check_numa_common( 1118 Some(&[960_000, 1_920_000, 2_880_000]), 1119 Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), 1120 Some(&["10 15 20", "20 10 25", "25 30 10"]), 1121 ); 1122 1123 // AArch64 currently does not support hotplug, and therefore we only 1124 // test hotplug-related function on x86_64 here. 1125 #[cfg(target_arch = "x86_64")] 1126 { 1127 guest.enable_memory_hotplug(); 1128 1129 // Resize every memory zone and check each associated NUMA node 1130 // has been assigned the right amount of memory. 1131 resize_zone_command(&api_socket, "mem0", "4G"); 1132 resize_zone_command(&api_socket, "mem1", "4G"); 1133 resize_zone_command(&api_socket, "mem2", "4G"); 1134 // Resize to the maximum amount of CPUs and check each NUMA 1135 // node has been assigned the right CPUs set. 1136 resize_command(&api_socket, Some(12), None, None, None); 1137 thread::sleep(std::time::Duration::new(5, 0)); 1138 1139 guest.check_numa_common( 1140 Some(&[3_840_000, 3_840_000, 3_840_000]), 1141 Some(&[vec![0, 1, 2, 9], vec![3, 4, 6, 7, 8], vec![5, 10, 11]]), 1142 None, 1143 ); 1144 } 1145 }); 1146 1147 kill_child(&mut child); 1148 let output = child.wait_with_output().unwrap(); 1149 1150 handle_child_output(r, &output); 1151 } 1152 1153 #[allow(unused_variables)] 1154 fn _test_power_button(acpi: bool) { 1155 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1156 let guest = Guest::new(Box::new(focal)); 1157 let mut cmd = GuestCommand::new(&guest); 1158 let api_socket = temp_api_path(&guest.tmp_dir); 1159 1160 #[cfg(target_arch = "x86_64")] 1161 let kernel_path = direct_kernel_boot_path(); 1162 #[cfg(target_arch = "aarch64")] 1163 let kernel_path = if acpi { 1164 edk2_path() 1165 } else { 1166 direct_kernel_boot_path() 1167 }; 1168 1169 cmd.args(["--cpus", "boot=1"]) 1170 .args(["--memory", "size=512M"]) 1171 .args(["--kernel", kernel_path.to_str().unwrap()]) 1172 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1173 .capture_output() 1174 .default_disks() 1175 .default_net() 1176 .args(["--api-socket", &api_socket]); 1177 1178 let child = cmd.spawn().unwrap(); 1179 1180 let r = std::panic::catch_unwind(|| { 1181 guest.wait_vm_boot(None).unwrap(); 1182 assert!(remote_command(&api_socket, "power-button", None)); 1183 }); 1184 1185 let output = child.wait_with_output().unwrap(); 1186 assert!(output.status.success()); 1187 handle_child_output(r, &output); 1188 } 1189 1190 type PrepareNetDaemon = dyn Fn( 1191 &TempDir, 1192 &str, 1193 Option<&str>, 1194 Option<u16>, 1195 usize, 1196 bool, 1197 ) -> (std::process::Command, String); 1198 1199 fn test_vhost_user_net( 1200 tap: Option<&str>, 1201 num_queues: usize, 1202 prepare_daemon: &PrepareNetDaemon, 1203 generate_host_mac: bool, 1204 client_mode_daemon: bool, 1205 ) { 1206 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1207 let guest = Guest::new(Box::new(focal)); 1208 let api_socket = temp_api_path(&guest.tmp_dir); 1209 1210 let kernel_path = direct_kernel_boot_path(); 1211 1212 let host_mac = if generate_host_mac { 1213 Some(MacAddr::local_random()) 1214 } else { 1215 None 1216 }; 1217 1218 let mtu = Some(3000); 1219 1220 let (mut daemon_command, vunet_socket_path) = prepare_daemon( 1221 &guest.tmp_dir, 1222 &guest.network.host_ip, 1223 tap, 1224 mtu, 1225 num_queues, 1226 client_mode_daemon, 1227 ); 1228 1229 let net_params = format!( 1230 "vhost_user=true,mac={},socket={},num_queues={},queue_size=1024{},vhost_mode={},mtu=3000", 1231 guest.network.guest_mac, 1232 vunet_socket_path, 1233 num_queues, 1234 if let Some(host_mac) = host_mac { 1235 format!(",host_mac={host_mac}") 1236 } else { 1237 "".to_owned() 1238 }, 1239 if client_mode_daemon { 1240 "server" 1241 } else { 1242 "client" 1243 }, 1244 ); 1245 1246 let mut ch_command = GuestCommand::new(&guest); 1247 ch_command 1248 .args(["--cpus", format!("boot={}", num_queues / 2).as_str()]) 1249 .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) 1250 .args(["--kernel", kernel_path.to_str().unwrap()]) 1251 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1252 .default_disks() 1253 .args(["--net", net_params.as_str()]) 1254 .args(["--api-socket", &api_socket]) 1255 .capture_output(); 1256 1257 let mut daemon_child: std::process::Child; 1258 let mut child: std::process::Child; 1259 1260 if client_mode_daemon { 1261 child = ch_command.spawn().unwrap(); 1262 // Make sure the VMM is waiting for the backend to connect 1263 thread::sleep(std::time::Duration::new(10, 0)); 1264 daemon_child = daemon_command.spawn().unwrap(); 1265 } else { 1266 daemon_child = daemon_command.spawn().unwrap(); 1267 // Make sure the backend is waiting for the VMM to connect 1268 thread::sleep(std::time::Duration::new(10, 0)); 1269 child = ch_command.spawn().unwrap(); 1270 } 1271 1272 let r = std::panic::catch_unwind(|| { 1273 guest.wait_vm_boot(None).unwrap(); 1274 1275 if let Some(tap_name) = tap { 1276 let tap_count = exec_host_command_output(&format!("ip link | grep -c {tap_name}")); 1277 assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); 1278 } 1279 1280 if let Some(host_mac) = tap { 1281 let mac_count = exec_host_command_output(&format!("ip link | grep -c {host_mac}")); 1282 assert_eq!(String::from_utf8_lossy(&mac_count.stdout).trim(), "1"); 1283 } 1284 1285 #[cfg(target_arch = "aarch64")] 1286 let iface = "enp0s4"; 1287 #[cfg(target_arch = "x86_64")] 1288 let iface = "ens4"; 1289 1290 assert_eq!( 1291 guest 1292 .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) 1293 .unwrap() 1294 .trim(), 1295 "3000" 1296 ); 1297 1298 // 1 network interface + default localhost ==> 2 interfaces 1299 // It's important to note that this test is fully exercising the 1300 // vhost-user-net implementation and the associated backend since 1301 // it does not define any --net network interface. That means all 1302 // the ssh communication in that test happens through the network 1303 // interface backed by vhost-user-net. 1304 assert_eq!( 1305 guest 1306 .ssh_command("ip -o link | wc -l") 1307 .unwrap() 1308 .trim() 1309 .parse::<u32>() 1310 .unwrap_or_default(), 1311 2 1312 ); 1313 1314 // The following pci devices will appear on guest with PCI-MSI 1315 // interrupt vectors assigned. 1316 // 1 virtio-console with 3 vectors: config, Rx, Tx 1317 // 1 virtio-blk with 2 vectors: config, Request 1318 // 1 virtio-blk with 2 vectors: config, Request 1319 // 1 virtio-rng with 2 vectors: config, Request 1320 // Since virtio-net has 2 queue pairs, its vectors is as follows: 1321 // 1 virtio-net with 5 vectors: config, Rx (2), Tx (2) 1322 // Based on the above, the total vectors should 14. 1323 #[cfg(target_arch = "x86_64")] 1324 let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; 1325 #[cfg(target_arch = "aarch64")] 1326 let grep_cmd = "grep -c ITS-MSI /proc/interrupts"; 1327 assert_eq!( 1328 guest 1329 .ssh_command(grep_cmd) 1330 .unwrap() 1331 .trim() 1332 .parse::<u32>() 1333 .unwrap_or_default(), 1334 10 + (num_queues as u32) 1335 ); 1336 1337 // ACPI feature is needed. 1338 #[cfg(target_arch = "x86_64")] 1339 { 1340 guest.enable_memory_hotplug(); 1341 1342 // Add RAM to the VM 1343 let desired_ram = 1024 << 20; 1344 resize_command(&api_socket, None, Some(desired_ram), None, None); 1345 1346 thread::sleep(std::time::Duration::new(10, 0)); 1347 1348 // Here by simply checking the size (through ssh), we validate 1349 // the connection is still working, which means vhost-user-net 1350 // keeps working after the resize. 1351 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 1352 } 1353 }); 1354 1355 kill_child(&mut child); 1356 let output = child.wait_with_output().unwrap(); 1357 1358 thread::sleep(std::time::Duration::new(5, 0)); 1359 let _ = daemon_child.kill(); 1360 let _ = daemon_child.wait(); 1361 1362 handle_child_output(r, &output); 1363 } 1364 1365 type PrepareBlkDaemon = dyn Fn(&TempDir, &str, usize, bool, bool) -> (std::process::Child, String); 1366 1367 fn test_vhost_user_blk( 1368 num_queues: usize, 1369 readonly: bool, 1370 direct: bool, 1371 prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, 1372 ) { 1373 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1374 let guest = Guest::new(Box::new(focal)); 1375 let api_socket = temp_api_path(&guest.tmp_dir); 1376 1377 let kernel_path = direct_kernel_boot_path(); 1378 1379 let (blk_params, daemon_child) = { 1380 let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); 1381 // Start the daemon 1382 let (daemon_child, vubd_socket_path) = 1383 prepare_daemon(&guest.tmp_dir, "blk.img", num_queues, readonly, direct); 1384 1385 ( 1386 format!( 1387 "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", 1388 ), 1389 Some(daemon_child), 1390 ) 1391 }; 1392 1393 let mut child = GuestCommand::new(&guest) 1394 .args(["--cpus", format!("boot={num_queues}").as_str()]) 1395 .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) 1396 .args(["--kernel", kernel_path.to_str().unwrap()]) 1397 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1398 .args([ 1399 "--disk", 1400 format!( 1401 "path={}", 1402 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 1403 ) 1404 .as_str(), 1405 format!( 1406 "path={}", 1407 guest.disk_config.disk(DiskType::CloudInit).unwrap() 1408 ) 1409 .as_str(), 1410 blk_params.as_str(), 1411 ]) 1412 .default_net() 1413 .args(["--api-socket", &api_socket]) 1414 .capture_output() 1415 .spawn() 1416 .unwrap(); 1417 1418 let r = std::panic::catch_unwind(|| { 1419 guest.wait_vm_boot(None).unwrap(); 1420 1421 // Check both if /dev/vdc exists and if the block size is 16M. 1422 assert_eq!( 1423 guest 1424 .ssh_command("lsblk | grep vdc | grep -c 16M") 1425 .unwrap() 1426 .trim() 1427 .parse::<u32>() 1428 .unwrap_or_default(), 1429 1 1430 ); 1431 1432 // Check if this block is RO or RW. 1433 assert_eq!( 1434 guest 1435 .ssh_command("lsblk | grep vdc | awk '{print $5}'") 1436 .unwrap() 1437 .trim() 1438 .parse::<u32>() 1439 .unwrap_or_default(), 1440 readonly as u32 1441 ); 1442 1443 // Check if the number of queues in /sys/block/vdc/mq matches the 1444 // expected num_queues. 1445 assert_eq!( 1446 guest 1447 .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") 1448 .unwrap() 1449 .trim() 1450 .parse::<u32>() 1451 .unwrap_or_default(), 1452 num_queues as u32 1453 ); 1454 1455 // Mount the device 1456 let mount_ro_rw_flag = if readonly { "ro,noload" } else { "rw" }; 1457 guest.ssh_command("mkdir mount_image").unwrap(); 1458 guest 1459 .ssh_command( 1460 format!("sudo mount -o {mount_ro_rw_flag} -t ext4 /dev/vdc mount_image/").as_str(), 1461 ) 1462 .unwrap(); 1463 1464 // Check the content of the block device. The file "foo" should 1465 // contain "bar". 1466 assert_eq!( 1467 guest.ssh_command("cat mount_image/foo").unwrap().trim(), 1468 "bar" 1469 ); 1470 1471 // ACPI feature is needed. 1472 #[cfg(target_arch = "x86_64")] 1473 { 1474 guest.enable_memory_hotplug(); 1475 1476 // Add RAM to the VM 1477 let desired_ram = 1024 << 20; 1478 resize_command(&api_socket, None, Some(desired_ram), None, None); 1479 1480 thread::sleep(std::time::Duration::new(10, 0)); 1481 1482 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 1483 1484 // Check again the content of the block device after the resize 1485 // has been performed. 1486 assert_eq!( 1487 guest.ssh_command("cat mount_image/foo").unwrap().trim(), 1488 "bar" 1489 ); 1490 } 1491 1492 // Unmount the device 1493 guest.ssh_command("sudo umount /dev/vdc").unwrap(); 1494 guest.ssh_command("rm -r mount_image").unwrap(); 1495 }); 1496 1497 kill_child(&mut child); 1498 let output = child.wait_with_output().unwrap(); 1499 1500 if let Some(mut daemon_child) = daemon_child { 1501 thread::sleep(std::time::Duration::new(5, 0)); 1502 let _ = daemon_child.kill(); 1503 let _ = daemon_child.wait(); 1504 } 1505 1506 handle_child_output(r, &output); 1507 } 1508 1509 fn test_boot_from_vhost_user_blk( 1510 num_queues: usize, 1511 readonly: bool, 1512 direct: bool, 1513 prepare_vhost_user_blk_daemon: Option<&PrepareBlkDaemon>, 1514 ) { 1515 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1516 let guest = Guest::new(Box::new(focal)); 1517 1518 let kernel_path = direct_kernel_boot_path(); 1519 1520 let disk_path = guest.disk_config.disk(DiskType::OperatingSystem).unwrap(); 1521 1522 let (blk_boot_params, daemon_child) = { 1523 let prepare_daemon = prepare_vhost_user_blk_daemon.unwrap(); 1524 // Start the daemon 1525 let (daemon_child, vubd_socket_path) = prepare_daemon( 1526 &guest.tmp_dir, 1527 disk_path.as_str(), 1528 num_queues, 1529 readonly, 1530 direct, 1531 ); 1532 1533 ( 1534 format!( 1535 "vhost_user=true,socket={vubd_socket_path},num_queues={num_queues},queue_size=128", 1536 ), 1537 Some(daemon_child), 1538 ) 1539 }; 1540 1541 let mut child = GuestCommand::new(&guest) 1542 .args(["--cpus", format!("boot={num_queues}").as_str()]) 1543 .args(["--memory", "size=512M,shared=on"]) 1544 .args(["--kernel", kernel_path.to_str().unwrap()]) 1545 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1546 .args([ 1547 "--disk", 1548 blk_boot_params.as_str(), 1549 format!( 1550 "path={}", 1551 guest.disk_config.disk(DiskType::CloudInit).unwrap() 1552 ) 1553 .as_str(), 1554 ]) 1555 .default_net() 1556 .capture_output() 1557 .spawn() 1558 .unwrap(); 1559 1560 let r = std::panic::catch_unwind(|| { 1561 guest.wait_vm_boot(None).unwrap(); 1562 1563 // Just check the VM booted correctly. 1564 assert_eq!(guest.get_cpu_count().unwrap_or_default(), num_queues as u32); 1565 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 1566 }); 1567 kill_child(&mut child); 1568 let output = child.wait_with_output().unwrap(); 1569 1570 if let Some(mut daemon_child) = daemon_child { 1571 thread::sleep(std::time::Duration::new(5, 0)); 1572 let _ = daemon_child.kill(); 1573 let _ = daemon_child.wait(); 1574 } 1575 1576 handle_child_output(r, &output); 1577 } 1578 1579 fn _test_virtio_fs( 1580 prepare_daemon: &dyn Fn(&TempDir, &str) -> (std::process::Child, String), 1581 hotplug: bool, 1582 pci_segment: Option<u16>, 1583 ) { 1584 #[cfg(target_arch = "aarch64")] 1585 let focal_image = if hotplug { 1586 FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string() 1587 } else { 1588 FOCAL_IMAGE_NAME.to_string() 1589 }; 1590 #[cfg(target_arch = "x86_64")] 1591 let focal_image = FOCAL_IMAGE_NAME.to_string(); 1592 let focal = UbuntuDiskConfig::new(focal_image); 1593 let guest = Guest::new(Box::new(focal)); 1594 let api_socket = temp_api_path(&guest.tmp_dir); 1595 1596 let mut workload_path = dirs::home_dir().unwrap(); 1597 workload_path.push("workloads"); 1598 1599 let mut shared_dir = workload_path; 1600 shared_dir.push("shared_dir"); 1601 1602 #[cfg(target_arch = "x86_64")] 1603 let kernel_path = direct_kernel_boot_path(); 1604 #[cfg(target_arch = "aarch64")] 1605 let kernel_path = if hotplug { 1606 edk2_path() 1607 } else { 1608 direct_kernel_boot_path() 1609 }; 1610 1611 let (mut daemon_child, virtiofsd_socket_path) = 1612 prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); 1613 1614 let mut guest_command = GuestCommand::new(&guest); 1615 guest_command 1616 .args(["--cpus", "boot=1"]) 1617 .args(["--memory", "size=512M,hotplug_size=2048M,shared=on"]) 1618 .args(["--kernel", kernel_path.to_str().unwrap()]) 1619 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1620 .default_disks() 1621 .default_net() 1622 .args(["--api-socket", &api_socket]); 1623 if pci_segment.is_some() { 1624 guest_command.args([ 1625 "--platform", 1626 &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), 1627 ]); 1628 } 1629 1630 let fs_params = format!( 1631 "id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}", 1632 virtiofsd_socket_path, 1633 if let Some(pci_segment) = pci_segment { 1634 format!(",pci_segment={pci_segment}") 1635 } else { 1636 "".to_owned() 1637 } 1638 ); 1639 1640 if !hotplug { 1641 guest_command.args(["--fs", fs_params.as_str()]); 1642 } 1643 1644 let mut child = guest_command.capture_output().spawn().unwrap(); 1645 1646 let r = std::panic::catch_unwind(|| { 1647 guest.wait_vm_boot(None).unwrap(); 1648 1649 if hotplug { 1650 // Add fs to the VM 1651 let (cmd_success, cmd_output) = 1652 remote_command_w_output(&api_socket, "add-fs", Some(&fs_params)); 1653 assert!(cmd_success); 1654 1655 if let Some(pci_segment) = pci_segment { 1656 assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( 1657 "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" 1658 ))); 1659 } else { 1660 assert!(String::from_utf8_lossy(&cmd_output) 1661 .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")); 1662 } 1663 1664 thread::sleep(std::time::Duration::new(10, 0)); 1665 } 1666 1667 // Mount shared directory through virtio_fs filesystem 1668 guest 1669 .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") 1670 .unwrap(); 1671 1672 // Check file1 exists and its content is "foo" 1673 assert_eq!( 1674 guest.ssh_command("cat mount_dir/file1").unwrap().trim(), 1675 "foo" 1676 ); 1677 // Check file2 does not exist 1678 guest 1679 .ssh_command("[ ! -f 'mount_dir/file2' ] || true") 1680 .unwrap(); 1681 1682 // Check file3 exists and its content is "bar" 1683 assert_eq!( 1684 guest.ssh_command("cat mount_dir/file3").unwrap().trim(), 1685 "bar" 1686 ); 1687 1688 // ACPI feature is needed. 1689 #[cfg(target_arch = "x86_64")] 1690 { 1691 guest.enable_memory_hotplug(); 1692 1693 // Add RAM to the VM 1694 let desired_ram = 1024 << 20; 1695 resize_command(&api_socket, None, Some(desired_ram), None, None); 1696 1697 thread::sleep(std::time::Duration::new(30, 0)); 1698 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 1699 1700 // After the resize, check again that file1 exists and its 1701 // content is "foo". 1702 assert_eq!( 1703 guest.ssh_command("cat mount_dir/file1").unwrap().trim(), 1704 "foo" 1705 ); 1706 } 1707 1708 if hotplug { 1709 // Remove from VM 1710 guest.ssh_command("sudo umount mount_dir").unwrap(); 1711 assert!(remote_command(&api_socket, "remove-device", Some("myfs0"))); 1712 } 1713 }); 1714 1715 let (r, hotplug_daemon_child) = if r.is_ok() && hotplug { 1716 thread::sleep(std::time::Duration::new(10, 0)); 1717 let (daemon_child, virtiofsd_socket_path) = 1718 prepare_daemon(&guest.tmp_dir, shared_dir.to_str().unwrap()); 1719 1720 let r = std::panic::catch_unwind(|| { 1721 thread::sleep(std::time::Duration::new(10, 0)); 1722 let fs_params = format!( 1723 "id=myfs0,tag=myfs,socket={},num_queues=1,queue_size=1024{}", 1724 virtiofsd_socket_path, 1725 if let Some(pci_segment) = pci_segment { 1726 format!(",pci_segment={pci_segment}") 1727 } else { 1728 "".to_owned() 1729 } 1730 ); 1731 1732 // Add back and check it works 1733 let (cmd_success, cmd_output) = 1734 remote_command_w_output(&api_socket, "add-fs", Some(&fs_params)); 1735 assert!(cmd_success); 1736 if let Some(pci_segment) = pci_segment { 1737 assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( 1738 "{{\"id\":\"myfs0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" 1739 ))); 1740 } else { 1741 assert!(String::from_utf8_lossy(&cmd_output) 1742 .contains("{\"id\":\"myfs0\",\"bdf\":\"0000:00:06.0\"}")); 1743 } 1744 1745 thread::sleep(std::time::Duration::new(10, 0)); 1746 // Mount shared directory through virtio_fs filesystem 1747 guest 1748 .ssh_command("mkdir -p mount_dir && sudo mount -t virtiofs myfs mount_dir/") 1749 .unwrap(); 1750 1751 // Check file1 exists and its content is "foo" 1752 assert_eq!( 1753 guest.ssh_command("cat mount_dir/file1").unwrap().trim(), 1754 "foo" 1755 ); 1756 }); 1757 1758 (r, Some(daemon_child)) 1759 } else { 1760 (r, None) 1761 }; 1762 1763 kill_child(&mut child); 1764 let output = child.wait_with_output().unwrap(); 1765 1766 let _ = daemon_child.kill(); 1767 let _ = daemon_child.wait(); 1768 1769 if let Some(mut daemon_child) = hotplug_daemon_child { 1770 let _ = daemon_child.kill(); 1771 let _ = daemon_child.wait(); 1772 } 1773 1774 handle_child_output(r, &output); 1775 } 1776 1777 fn test_virtio_pmem(discard_writes: bool, specify_size: bool) { 1778 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1779 let guest = Guest::new(Box::new(focal)); 1780 1781 let kernel_path = direct_kernel_boot_path(); 1782 1783 let pmem_temp_file = TempFile::new().unwrap(); 1784 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 1785 1786 std::process::Command::new("mkfs.ext4") 1787 .arg(pmem_temp_file.as_path()) 1788 .output() 1789 .expect("Expect creating disk image to succeed"); 1790 1791 let mut child = GuestCommand::new(&guest) 1792 .args(["--cpus", "boot=1"]) 1793 .args(["--memory", "size=512M"]) 1794 .args(["--kernel", kernel_path.to_str().unwrap()]) 1795 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1796 .default_disks() 1797 .default_net() 1798 .args([ 1799 "--pmem", 1800 format!( 1801 "file={}{}{}", 1802 pmem_temp_file.as_path().to_str().unwrap(), 1803 if specify_size { ",size=128M" } else { "" }, 1804 if discard_writes { 1805 ",discard_writes=on" 1806 } else { 1807 "" 1808 } 1809 ) 1810 .as_str(), 1811 ]) 1812 .capture_output() 1813 .spawn() 1814 .unwrap(); 1815 1816 let r = std::panic::catch_unwind(|| { 1817 guest.wait_vm_boot(None).unwrap(); 1818 1819 // Check for the presence of /dev/pmem0 1820 assert_eq!( 1821 guest.ssh_command("ls /dev/pmem0").unwrap().trim(), 1822 "/dev/pmem0" 1823 ); 1824 1825 // Check changes persist after reboot 1826 assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); 1827 assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); 1828 guest 1829 .ssh_command("echo test123 | sudo tee /mnt/test") 1830 .unwrap(); 1831 assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); 1832 assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); 1833 1834 guest.reboot_linux(0, None); 1835 assert_eq!(guest.ssh_command("sudo mount /dev/pmem0 /mnt").unwrap(), ""); 1836 assert_eq!( 1837 guest 1838 .ssh_command("sudo cat /mnt/test || true") 1839 .unwrap() 1840 .trim(), 1841 if discard_writes { "" } else { "test123" } 1842 ); 1843 }); 1844 1845 kill_child(&mut child); 1846 let output = child.wait_with_output().unwrap(); 1847 1848 handle_child_output(r, &output); 1849 } 1850 1851 fn get_fd_count(pid: u32) -> usize { 1852 fs::read_dir(format!("/proc/{pid}/fd")).unwrap().count() 1853 } 1854 1855 fn _test_virtio_vsock(hotplug: bool) { 1856 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1857 let guest = Guest::new(Box::new(focal)); 1858 1859 #[cfg(target_arch = "x86_64")] 1860 let kernel_path = direct_kernel_boot_path(); 1861 #[cfg(target_arch = "aarch64")] 1862 let kernel_path = if hotplug { 1863 edk2_path() 1864 } else { 1865 direct_kernel_boot_path() 1866 }; 1867 1868 let socket = temp_vsock_path(&guest.tmp_dir); 1869 let api_socket = temp_api_path(&guest.tmp_dir); 1870 1871 let mut cmd = GuestCommand::new(&guest); 1872 cmd.args(["--api-socket", &api_socket]); 1873 cmd.args(["--cpus", "boot=1"]); 1874 cmd.args(["--memory", "size=512M"]); 1875 cmd.args(["--kernel", kernel_path.to_str().unwrap()]); 1876 cmd.args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]); 1877 cmd.default_disks(); 1878 cmd.default_net(); 1879 1880 if !hotplug { 1881 cmd.args(["--vsock", format!("cid=3,socket={socket}").as_str()]); 1882 } 1883 1884 let mut child = cmd.capture_output().spawn().unwrap(); 1885 1886 let r = std::panic::catch_unwind(|| { 1887 guest.wait_vm_boot(None).unwrap(); 1888 1889 if hotplug { 1890 let (cmd_success, cmd_output) = remote_command_w_output( 1891 &api_socket, 1892 "add-vsock", 1893 Some(format!("cid=3,socket={socket},id=test0").as_str()), 1894 ); 1895 assert!(cmd_success); 1896 assert!(String::from_utf8_lossy(&cmd_output) 1897 .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); 1898 thread::sleep(std::time::Duration::new(10, 0)); 1899 // Check adding a second one fails 1900 assert!(!remote_command( 1901 &api_socket, 1902 "add-vsock", 1903 Some("cid=1234,socket=/tmp/fail") 1904 )); 1905 } 1906 1907 // Validate vsock works as expected. 1908 guest.check_vsock(socket.as_str()); 1909 guest.reboot_linux(0, None); 1910 // Validate vsock still works after a reboot. 1911 guest.check_vsock(socket.as_str()); 1912 1913 if hotplug { 1914 assert!(remote_command(&api_socket, "remove-device", Some("test0"))); 1915 } 1916 }); 1917 1918 kill_child(&mut child); 1919 let output = child.wait_with_output().unwrap(); 1920 1921 handle_child_output(r, &output); 1922 } 1923 1924 fn get_ksm_pages_shared() -> u32 { 1925 fs::read_to_string("/sys/kernel/mm/ksm/pages_shared") 1926 .unwrap() 1927 .trim() 1928 .parse::<u32>() 1929 .unwrap() 1930 } 1931 1932 fn test_memory_mergeable(mergeable: bool) { 1933 let memory_param = if mergeable { 1934 "mergeable=on" 1935 } else { 1936 "mergeable=off" 1937 }; 1938 1939 // We are assuming the rest of the system in our CI is not using mergeable memory 1940 let ksm_ps_init = get_ksm_pages_shared(); 1941 assert!(ksm_ps_init == 0); 1942 1943 let focal1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1944 let guest1 = Guest::new(Box::new(focal1)); 1945 let mut child1 = GuestCommand::new(&guest1) 1946 .args(["--cpus", "boot=1"]) 1947 .args(["--memory", format!("size=512M,{memory_param}").as_str()]) 1948 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 1949 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1950 .default_disks() 1951 .args(["--net", guest1.default_net_string().as_str()]) 1952 .args(["--serial", "tty", "--console", "off"]) 1953 .capture_output() 1954 .spawn() 1955 .unwrap(); 1956 1957 let r = std::panic::catch_unwind(|| { 1958 guest1.wait_vm_boot(None).unwrap(); 1959 }); 1960 if r.is_err() { 1961 kill_child(&mut child1); 1962 let output = child1.wait_with_output().unwrap(); 1963 handle_child_output(r, &output); 1964 panic!("Test should already be failed/panicked"); // To explicitly mark this block never return 1965 } 1966 1967 let ksm_ps_guest1 = get_ksm_pages_shared(); 1968 1969 let focal2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 1970 let guest2 = Guest::new(Box::new(focal2)); 1971 let mut child2 = GuestCommand::new(&guest2) 1972 .args(["--cpus", "boot=1"]) 1973 .args(["--memory", format!("size=512M,{memory_param}").as_str()]) 1974 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 1975 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 1976 .default_disks() 1977 .args(["--net", guest2.default_net_string().as_str()]) 1978 .args(["--serial", "tty", "--console", "off"]) 1979 .capture_output() 1980 .spawn() 1981 .unwrap(); 1982 1983 let r = std::panic::catch_unwind(|| { 1984 guest2.wait_vm_boot(None).unwrap(); 1985 let ksm_ps_guest2 = get_ksm_pages_shared(); 1986 1987 if mergeable { 1988 println!( 1989 "ksm pages_shared after vm1 booted '{ksm_ps_guest1}', ksm pages_shared after vm2 booted '{ksm_ps_guest2}'" 1990 ); 1991 // We are expecting the number of shared pages to increase as the number of VM increases 1992 assert!(ksm_ps_guest1 < ksm_ps_guest2); 1993 } else { 1994 assert!(ksm_ps_guest1 == 0); 1995 assert!(ksm_ps_guest2 == 0); 1996 } 1997 }); 1998 1999 kill_child(&mut child1); 2000 kill_child(&mut child2); 2001 2002 let output = child1.wait_with_output().unwrap(); 2003 child2.wait().unwrap(); 2004 2005 handle_child_output(r, &output); 2006 } 2007 2008 fn _get_vmm_overhead(pid: u32, guest_memory_size: u32) -> HashMap<String, u32> { 2009 let smaps = fs::File::open(format!("/proc/{pid}/smaps")).unwrap(); 2010 let reader = io::BufReader::new(smaps); 2011 2012 let mut skip_map: bool = false; 2013 let mut region_name: String = "".to_string(); 2014 let mut region_maps = HashMap::new(); 2015 for line in reader.lines() { 2016 let l = line.unwrap(); 2017 2018 if l.contains('-') { 2019 let values: Vec<&str> = l.split_whitespace().collect(); 2020 region_name = values.last().unwrap().trim().to_string(); 2021 if region_name == "0" { 2022 region_name = "anonymous".to_string() 2023 } 2024 } 2025 2026 // Each section begins with something that looks like: 2027 // Size: 2184 kB 2028 if l.starts_with("Size:") { 2029 let values: Vec<&str> = l.split_whitespace().collect(); 2030 let map_size = values[1].parse::<u32>().unwrap(); 2031 // We skip the assigned guest RAM map, its RSS is only 2032 // dependent on the guest actual memory usage. 2033 // Everything else can be added to the VMM overhead. 2034 skip_map = map_size >= guest_memory_size; 2035 continue; 2036 } 2037 2038 // If this is a map we're taking into account, then we only 2039 // count the RSS. The sum of all counted RSS is the VMM overhead. 2040 if !skip_map && l.starts_with("Rss:") { 2041 let values: Vec<&str> = l.split_whitespace().collect(); 2042 let value = values[1].trim().parse::<u32>().unwrap(); 2043 *region_maps.entry(region_name.clone()).or_insert(0) += value; 2044 } 2045 } 2046 2047 region_maps 2048 } 2049 2050 fn get_vmm_overhead(pid: u32, guest_memory_size: u32) -> u32 { 2051 let mut total = 0; 2052 2053 for (region_name, value) in &_get_vmm_overhead(pid, guest_memory_size) { 2054 eprintln!("{region_name}: {value}"); 2055 total += value; 2056 } 2057 2058 total 2059 } 2060 2061 fn process_rss_kib(pid: u32) -> usize { 2062 let command = format!("ps -q {pid} -o rss="); 2063 let rss = exec_host_command_output(&command); 2064 String::from_utf8_lossy(&rss.stdout).trim().parse().unwrap() 2065 } 2066 2067 // 10MB is our maximum accepted overhead. 2068 const MAXIMUM_VMM_OVERHEAD_KB: u32 = 10 * 1024; 2069 2070 #[derive(PartialEq, Eq, PartialOrd)] 2071 struct Counters { 2072 rx_bytes: u64, 2073 rx_frames: u64, 2074 tx_bytes: u64, 2075 tx_frames: u64, 2076 read_bytes: u64, 2077 write_bytes: u64, 2078 read_ops: u64, 2079 write_ops: u64, 2080 } 2081 2082 fn get_counters(api_socket: &str) -> Counters { 2083 // Get counters 2084 let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "counters", None); 2085 assert!(cmd_success); 2086 2087 let counters: HashMap<&str, HashMap<&str, u64>> = 2088 serde_json::from_slice(&cmd_output).unwrap_or_default(); 2089 2090 let rx_bytes = *counters.get("_net2").unwrap().get("rx_bytes").unwrap(); 2091 let rx_frames = *counters.get("_net2").unwrap().get("rx_frames").unwrap(); 2092 let tx_bytes = *counters.get("_net2").unwrap().get("tx_bytes").unwrap(); 2093 let tx_frames = *counters.get("_net2").unwrap().get("tx_frames").unwrap(); 2094 2095 let read_bytes = *counters.get("_disk0").unwrap().get("read_bytes").unwrap(); 2096 let write_bytes = *counters.get("_disk0").unwrap().get("write_bytes").unwrap(); 2097 let read_ops = *counters.get("_disk0").unwrap().get("read_ops").unwrap(); 2098 let write_ops = *counters.get("_disk0").unwrap().get("write_ops").unwrap(); 2099 2100 Counters { 2101 rx_bytes, 2102 rx_frames, 2103 tx_bytes, 2104 tx_frames, 2105 read_bytes, 2106 write_bytes, 2107 read_ops, 2108 write_ops, 2109 } 2110 } 2111 2112 fn pty_read(mut pty: std::fs::File) -> Receiver<String> { 2113 let (tx, rx) = mpsc::channel::<String>(); 2114 thread::spawn(move || loop { 2115 thread::sleep(std::time::Duration::new(1, 0)); 2116 let mut buf = [0; 512]; 2117 match pty.read(&mut buf) { 2118 Ok(_bytes) => { 2119 let output = std::str::from_utf8(&buf).unwrap().to_string(); 2120 match tx.send(output) { 2121 Ok(_) => (), 2122 Err(_) => break, 2123 } 2124 } 2125 Err(_) => break, 2126 } 2127 }); 2128 rx 2129 } 2130 2131 fn get_pty_path(api_socket: &str, pty_type: &str) -> PathBuf { 2132 let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); 2133 assert!(cmd_success); 2134 let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); 2135 assert_eq!("Pty", info["config"][pty_type]["mode"]); 2136 PathBuf::from( 2137 info["config"][pty_type]["file"] 2138 .as_str() 2139 .expect("Missing pty path"), 2140 ) 2141 } 2142 2143 // VFIO test network setup. 2144 // We reserve a different IP class for it: 172.18.0.0/24. 2145 #[cfg(target_arch = "x86_64")] 2146 fn setup_vfio_network_interfaces() { 2147 // 'vfio-br0' 2148 assert!(exec_host_command_status("sudo ip link add name vfio-br0 type bridge").success()); 2149 assert!(exec_host_command_status("sudo ip link set vfio-br0 up").success()); 2150 assert!(exec_host_command_status("sudo ip addr add 172.18.0.1/24 dev vfio-br0").success()); 2151 // 'vfio-tap0' 2152 assert!(exec_host_command_status("sudo ip tuntap add vfio-tap0 mode tap").success()); 2153 assert!(exec_host_command_status("sudo ip link set vfio-tap0 master vfio-br0").success()); 2154 assert!(exec_host_command_status("sudo ip link set vfio-tap0 up").success()); 2155 // 'vfio-tap1' 2156 assert!(exec_host_command_status("sudo ip tuntap add vfio-tap1 mode tap").success()); 2157 assert!(exec_host_command_status("sudo ip link set vfio-tap1 master vfio-br0").success()); 2158 assert!(exec_host_command_status("sudo ip link set vfio-tap1 up").success()); 2159 // 'vfio-tap2' 2160 assert!(exec_host_command_status("sudo ip tuntap add vfio-tap2 mode tap").success()); 2161 assert!(exec_host_command_status("sudo ip link set vfio-tap2 master vfio-br0").success()); 2162 assert!(exec_host_command_status("sudo ip link set vfio-tap2 up").success()); 2163 // 'vfio-tap3' 2164 assert!(exec_host_command_status("sudo ip tuntap add vfio-tap3 mode tap").success()); 2165 assert!(exec_host_command_status("sudo ip link set vfio-tap3 master vfio-br0").success()); 2166 assert!(exec_host_command_status("sudo ip link set vfio-tap3 up").success()); 2167 } 2168 2169 // Tear VFIO test network down 2170 #[cfg(target_arch = "x86_64")] 2171 fn cleanup_vfio_network_interfaces() { 2172 assert!(exec_host_command_status("sudo ip link del vfio-br0").success()); 2173 assert!(exec_host_command_status("sudo ip link del vfio-tap0").success()); 2174 assert!(exec_host_command_status("sudo ip link del vfio-tap1").success()); 2175 assert!(exec_host_command_status("sudo ip link del vfio-tap2").success()); 2176 assert!(exec_host_command_status("sudo ip link del vfio-tap3").success()); 2177 } 2178 2179 fn balloon_size(api_socket: &str) -> u64 { 2180 let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); 2181 assert!(cmd_success); 2182 2183 let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); 2184 let total_mem = &info["config"]["memory"]["size"] 2185 .to_string() 2186 .parse::<u64>() 2187 .unwrap(); 2188 let actual_mem = &info["memory_actual_size"] 2189 .to_string() 2190 .parse::<u64>() 2191 .unwrap(); 2192 total_mem - actual_mem 2193 } 2194 2195 fn vm_state(api_socket: &str) -> String { 2196 let (cmd_success, cmd_output) = remote_command_w_output(api_socket, "info", None); 2197 assert!(cmd_success); 2198 2199 let info: serde_json::Value = serde_json::from_slice(&cmd_output).unwrap_or_default(); 2200 let state = &info["state"].as_str().unwrap(); 2201 2202 state.to_string() 2203 } 2204 2205 // This test validates that it can find the virtio-iommu device at first. 2206 // It also verifies that both disks and the network card are attached to 2207 // the virtual IOMMU by looking at /sys/kernel/iommu_groups directory. 2208 // The last interesting part of this test is that it exercises the network 2209 // interface attached to the virtual IOMMU since this is the one used to 2210 // send all commands through SSH. 2211 fn _test_virtio_iommu(acpi: bool) { 2212 // Virtio-iommu support is ready in recent kernel (v5.14). But the kernel in 2213 // Focal image is still old. 2214 // So if ACPI is enabled on AArch64, we use a modified Focal image in which 2215 // the kernel binary has been updated. 2216 #[cfg(target_arch = "aarch64")] 2217 let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); 2218 #[cfg(target_arch = "x86_64")] 2219 let focal_image = FOCAL_IMAGE_NAME.to_string(); 2220 let focal = UbuntuDiskConfig::new(focal_image); 2221 let guest = Guest::new(Box::new(focal)); 2222 2223 #[cfg(target_arch = "x86_64")] 2224 let kernel_path = direct_kernel_boot_path(); 2225 #[cfg(target_arch = "aarch64")] 2226 let kernel_path = if acpi { 2227 edk2_path() 2228 } else { 2229 direct_kernel_boot_path() 2230 }; 2231 2232 let mut child = GuestCommand::new(&guest) 2233 .args(["--cpus", "boot=1"]) 2234 .args(["--memory", "size=512M"]) 2235 .args(["--kernel", kernel_path.to_str().unwrap()]) 2236 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2237 .args([ 2238 "--disk", 2239 format!( 2240 "path={},iommu=on", 2241 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 2242 ) 2243 .as_str(), 2244 format!( 2245 "path={},iommu=on", 2246 guest.disk_config.disk(DiskType::CloudInit).unwrap() 2247 ) 2248 .as_str(), 2249 ]) 2250 .args(["--net", guest.default_net_string_w_iommu().as_str()]) 2251 .capture_output() 2252 .spawn() 2253 .unwrap(); 2254 2255 let r = std::panic::catch_unwind(|| { 2256 guest.wait_vm_boot(None).unwrap(); 2257 2258 // Verify the virtio-iommu device is present. 2259 assert!(guest 2260 .does_device_vendor_pair_match("0x1057", "0x1af4") 2261 .unwrap_or_default()); 2262 2263 // On AArch64, if the guest system boots from FDT, the behavior of IOMMU is a bit 2264 // different with ACPI. 2265 // All devices on the PCI bus will be attached to the virtual IOMMU, except the 2266 // virtio-iommu device itself. So these devices will all be added to IOMMU groups, 2267 // and appear under folder '/sys/kernel/iommu_groups/'. 2268 // The result is, in the case of FDT, IOMMU group '0' contains "0000:00:01.0" 2269 // which is the console. The first disk "0000:00:02.0" is in group '1'. 2270 // While on ACPI, console device is not attached to IOMMU. So the IOMMU group '0' 2271 // contains "0000:00:02.0" which is the first disk. 2272 // 2273 // Verify the iommu group of the first disk. 2274 let iommu_group = !acpi as i32; 2275 assert_eq!( 2276 guest 2277 .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) 2278 .unwrap() 2279 .trim(), 2280 "0000:00:02.0" 2281 ); 2282 2283 // Verify the iommu group of the second disk. 2284 let iommu_group = if acpi { 1 } else { 2 }; 2285 assert_eq!( 2286 guest 2287 .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) 2288 .unwrap() 2289 .trim(), 2290 "0000:00:03.0" 2291 ); 2292 2293 // Verify the iommu group of the network card. 2294 let iommu_group = if acpi { 2 } else { 3 }; 2295 assert_eq!( 2296 guest 2297 .ssh_command(format!("ls /sys/kernel/iommu_groups/{iommu_group}/devices").as_str()) 2298 .unwrap() 2299 .trim(), 2300 "0000:00:04.0" 2301 ); 2302 }); 2303 2304 kill_child(&mut child); 2305 let output = child.wait_with_output().unwrap(); 2306 2307 handle_child_output(r, &output); 2308 } 2309 2310 fn get_reboot_count(guest: &Guest) -> u32 { 2311 guest 2312 .ssh_command("sudo last | grep -c reboot") 2313 .unwrap() 2314 .trim() 2315 .parse::<u32>() 2316 .unwrap_or_default() 2317 } 2318 2319 fn enable_guest_watchdog(guest: &Guest, watchdog_sec: u32) { 2320 // Check for PCI device 2321 assert!(guest 2322 .does_device_vendor_pair_match("0x1063", "0x1af4") 2323 .unwrap_or_default()); 2324 2325 // Enable systemd watchdog 2326 guest 2327 .ssh_command(&format!( 2328 "echo RuntimeWatchdogSec={watchdog_sec}s | sudo tee -a /etc/systemd/system.conf" 2329 )) 2330 .unwrap(); 2331 2332 guest.ssh_command("sudo systemctl daemon-reexec").unwrap(); 2333 } 2334 2335 fn make_guest_panic(guest: &Guest) { 2336 // Check for pvpanic device 2337 assert!(guest 2338 .does_device_vendor_pair_match("0x0011", "0x1b36") 2339 .unwrap_or_default()); 2340 2341 // Trigger guest a panic 2342 guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); 2343 } 2344 2345 mod common_parallel { 2346 use std::fs::OpenOptions; 2347 use std::io::SeekFrom; 2348 2349 use crate::*; 2350 2351 #[test] 2352 #[cfg(target_arch = "x86_64")] 2353 fn test_focal_hypervisor_fw() { 2354 test_simple_launch(fw_path(FwType::RustHypervisorFirmware), FOCAL_IMAGE_NAME) 2355 } 2356 2357 #[test] 2358 #[cfg(target_arch = "x86_64")] 2359 fn test_focal_ovmf() { 2360 test_simple_launch(fw_path(FwType::Ovmf), FOCAL_IMAGE_NAME) 2361 } 2362 2363 #[cfg(target_arch = "x86_64")] 2364 fn test_simple_launch(fw_path: String, disk_path: &str) { 2365 let disk_config = Box::new(UbuntuDiskConfig::new(disk_path.to_string())); 2366 let guest = Guest::new(disk_config); 2367 let event_path = temp_event_monitor_path(&guest.tmp_dir); 2368 2369 let mut child = GuestCommand::new(&guest) 2370 .args(["--cpus", "boot=1"]) 2371 .args(["--memory", "size=512M"]) 2372 .args(["--kernel", fw_path.as_str()]) 2373 .default_disks() 2374 .default_net() 2375 .args(["--serial", "tty", "--console", "off"]) 2376 .args(["--event-monitor", format!("path={event_path}").as_str()]) 2377 .capture_output() 2378 .spawn() 2379 .unwrap(); 2380 2381 let r = std::panic::catch_unwind(|| { 2382 guest.wait_vm_boot(Some(120)).unwrap(); 2383 2384 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 2385 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 2386 assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000"); 2387 2388 let expected_sequential_events = [ 2389 &MetaEvent { 2390 event: "starting".to_string(), 2391 device_id: None, 2392 }, 2393 &MetaEvent { 2394 event: "booting".to_string(), 2395 device_id: None, 2396 }, 2397 &MetaEvent { 2398 event: "booted".to_string(), 2399 device_id: None, 2400 }, 2401 &MetaEvent { 2402 event: "activated".to_string(), 2403 device_id: Some("_disk0".to_string()), 2404 }, 2405 &MetaEvent { 2406 event: "reset".to_string(), 2407 device_id: Some("_disk0".to_string()), 2408 }, 2409 ]; 2410 assert!(check_sequential_events( 2411 &expected_sequential_events, 2412 &event_path 2413 )); 2414 2415 // It's been observed on the Bionic image that udev and snapd 2416 // services can cause some delay in the VM's shutdown. Disabling 2417 // them improves the reliability of this test. 2418 let _ = guest.ssh_command("sudo systemctl disable udev"); 2419 let _ = guest.ssh_command("sudo systemctl stop udev"); 2420 let _ = guest.ssh_command("sudo systemctl disable snapd"); 2421 let _ = guest.ssh_command("sudo systemctl stop snapd"); 2422 2423 guest.ssh_command("sudo poweroff").unwrap(); 2424 thread::sleep(std::time::Duration::new(20, 0)); 2425 let latest_events = [ 2426 &MetaEvent { 2427 event: "shutdown".to_string(), 2428 device_id: None, 2429 }, 2430 &MetaEvent { 2431 event: "deleted".to_string(), 2432 device_id: None, 2433 }, 2434 &MetaEvent { 2435 event: "shutdown".to_string(), 2436 device_id: None, 2437 }, 2438 ]; 2439 assert!(check_latest_events_exact(&latest_events, &event_path)); 2440 }); 2441 2442 kill_child(&mut child); 2443 let output = child.wait_with_output().unwrap(); 2444 2445 handle_child_output(r, &output); 2446 } 2447 2448 #[test] 2449 fn test_multi_cpu() { 2450 let jammy_image = JAMMY_IMAGE_NAME.to_string(); 2451 let jammy = UbuntuDiskConfig::new(jammy_image); 2452 let guest = Guest::new(Box::new(jammy)); 2453 2454 let mut cmd = GuestCommand::new(&guest); 2455 cmd.args(["--cpus", "boot=2,max=4"]) 2456 .args(["--memory", "size=512M"]) 2457 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2458 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2459 .capture_output() 2460 .default_disks() 2461 .default_net(); 2462 2463 let mut child = cmd.spawn().unwrap(); 2464 2465 let r = std::panic::catch_unwind(|| { 2466 guest.wait_vm_boot(Some(120)).unwrap(); 2467 2468 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 2469 2470 assert_eq!( 2471 guest 2472 .ssh_command( 2473 r#"sudo dmesg | grep "smp: Brought up" | sed "s/\[\ *[0-9.]*\] //""# 2474 ) 2475 .unwrap() 2476 .trim(), 2477 "smp: Brought up 1 node, 2 CPUs" 2478 ); 2479 }); 2480 2481 kill_child(&mut child); 2482 let output = child.wait_with_output().unwrap(); 2483 2484 handle_child_output(r, &output); 2485 } 2486 2487 #[test] 2488 fn test_cpu_topology_421() { 2489 test_cpu_topology(4, 2, 1, false); 2490 } 2491 2492 #[test] 2493 fn test_cpu_topology_142() { 2494 test_cpu_topology(1, 4, 2, false); 2495 } 2496 2497 #[test] 2498 fn test_cpu_topology_262() { 2499 test_cpu_topology(2, 6, 2, false); 2500 } 2501 2502 #[test] 2503 #[cfg(target_arch = "x86_64")] 2504 #[cfg(not(feature = "mshv"))] 2505 fn test_cpu_physical_bits() { 2506 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2507 let guest = Guest::new(Box::new(focal)); 2508 let max_phys_bits: u8 = 36; 2509 let mut child = GuestCommand::new(&guest) 2510 .args(["--cpus", &format!("max_phys_bits={max_phys_bits}")]) 2511 .args(["--memory", "size=512M"]) 2512 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2513 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2514 .default_disks() 2515 .default_net() 2516 .capture_output() 2517 .spawn() 2518 .unwrap(); 2519 2520 let r = std::panic::catch_unwind(|| { 2521 guest.wait_vm_boot(None).unwrap(); 2522 2523 assert!( 2524 guest 2525 .ssh_command("lscpu | grep \"Address sizes:\" | cut -f 2 -d \":\" | sed \"s# *##\" | cut -f 1 -d \" \"") 2526 .unwrap() 2527 .trim() 2528 .parse::<u8>() 2529 .unwrap_or(max_phys_bits + 1) <= max_phys_bits, 2530 ); 2531 }); 2532 2533 kill_child(&mut child); 2534 let output = child.wait_with_output().unwrap(); 2535 2536 handle_child_output(r, &output); 2537 } 2538 2539 #[test] 2540 fn test_cpu_affinity() { 2541 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2542 let guest = Guest::new(Box::new(focal)); 2543 2544 // We need the host to have at least 4 CPUs if we want to be able 2545 // to run this test. 2546 let host_cpus_count = exec_host_command_output("nproc"); 2547 assert!( 2548 String::from_utf8_lossy(&host_cpus_count.stdout) 2549 .trim() 2550 .parse::<u16>() 2551 .unwrap_or(0) 2552 >= 4 2553 ); 2554 2555 let mut child = GuestCommand::new(&guest) 2556 .args(["--cpus", "boot=2,affinity=[0@[0,2],1@[1,3]]"]) 2557 .args(["--memory", "size=512M"]) 2558 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2559 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2560 .default_disks() 2561 .default_net() 2562 .capture_output() 2563 .spawn() 2564 .unwrap(); 2565 2566 let r = std::panic::catch_unwind(|| { 2567 guest.wait_vm_boot(None).unwrap(); 2568 let pid = child.id(); 2569 let taskset_vcpu0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2570 assert_eq!(String::from_utf8_lossy(&taskset_vcpu0.stdout).trim(), "0,2"); 2571 let taskset_vcpu1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep vcpu1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2572 assert_eq!(String::from_utf8_lossy(&taskset_vcpu1.stdout).trim(), "1,3"); 2573 }); 2574 2575 kill_child(&mut child); 2576 let output = child.wait_with_output().unwrap(); 2577 handle_child_output(r, &output); 2578 } 2579 2580 #[test] 2581 fn test_virtio_queue_affinity() { 2582 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2583 let guest = Guest::new(Box::new(focal)); 2584 2585 // We need the host to have at least 4 CPUs if we want to be able 2586 // to run this test. 2587 let host_cpus_count = exec_host_command_output("nproc"); 2588 assert!( 2589 String::from_utf8_lossy(&host_cpus_count.stdout) 2590 .trim() 2591 .parse::<u16>() 2592 .unwrap_or(0) 2593 >= 4 2594 ); 2595 2596 let mut child = GuestCommand::new(&guest) 2597 .args(["--cpus", "boot=4"]) 2598 .args(["--memory", "size=512M"]) 2599 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2600 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2601 .args([ 2602 "--disk", 2603 format!( 2604 "path={}", 2605 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 2606 ) 2607 .as_str(), 2608 format!( 2609 "path={},num_queues=4,queue_affinity=[0@[0,2],1@[1,3],2@[1],3@[3]]", 2610 guest.disk_config.disk(DiskType::CloudInit).unwrap() 2611 ) 2612 .as_str(), 2613 ]) 2614 .default_net() 2615 .capture_output() 2616 .spawn() 2617 .unwrap(); 2618 2619 let r = std::panic::catch_unwind(|| { 2620 guest.wait_vm_boot(None).unwrap(); 2621 let pid = child.id(); 2622 let taskset_q0 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q0 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2623 assert_eq!(String::from_utf8_lossy(&taskset_q0.stdout).trim(), "0,2"); 2624 let taskset_q1 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q1 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2625 assert_eq!(String::from_utf8_lossy(&taskset_q1.stdout).trim(), "1,3"); 2626 let taskset_q2 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q2 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2627 assert_eq!(String::from_utf8_lossy(&taskset_q2.stdout).trim(), "1"); 2628 let taskset_q3 = exec_host_command_output(format!("taskset -pc $(ps -T -p {pid} | grep disk1_q3 | xargs | cut -f 2 -d \" \") | cut -f 6 -d \" \"").as_str()); 2629 assert_eq!(String::from_utf8_lossy(&taskset_q3.stdout).trim(), "3"); 2630 }); 2631 2632 kill_child(&mut child); 2633 let output = child.wait_with_output().unwrap(); 2634 handle_child_output(r, &output); 2635 } 2636 2637 #[test] 2638 #[cfg(not(feature = "mshv"))] 2639 fn test_large_vm() { 2640 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2641 let guest = Guest::new(Box::new(focal)); 2642 let mut cmd = GuestCommand::new(&guest); 2643 cmd.args(["--cpus", "boot=48"]) 2644 .args(["--memory", "size=5120M"]) 2645 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2646 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2647 .args(["--serial", "tty"]) 2648 .args(["--console", "off"]) 2649 .capture_output() 2650 .default_disks() 2651 .default_net(); 2652 2653 let mut child = cmd.spawn().unwrap(); 2654 2655 guest.wait_vm_boot(None).unwrap(); 2656 2657 let r = std::panic::catch_unwind(|| { 2658 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 48); 2659 assert_eq!( 2660 guest 2661 .ssh_command("lscpu | grep \"On-line\" | cut -f 2 -d \":\" | sed \"s# *##\"") 2662 .unwrap() 2663 .trim(), 2664 "0-47" 2665 ); 2666 2667 assert!(guest.get_total_memory().unwrap_or_default() > 5_000_000); 2668 }); 2669 2670 kill_child(&mut child); 2671 let output = child.wait_with_output().unwrap(); 2672 2673 handle_child_output(r, &output); 2674 } 2675 2676 #[test] 2677 #[cfg(not(feature = "mshv"))] 2678 fn test_huge_memory() { 2679 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2680 let guest = Guest::new(Box::new(focal)); 2681 let mut cmd = GuestCommand::new(&guest); 2682 cmd.args(["--cpus", "boot=1"]) 2683 .args(["--memory", "size=128G"]) 2684 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2685 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2686 .capture_output() 2687 .default_disks() 2688 .default_net(); 2689 2690 let mut child = cmd.spawn().unwrap(); 2691 2692 guest.wait_vm_boot(Some(120)).unwrap(); 2693 2694 let r = std::panic::catch_unwind(|| { 2695 assert!(guest.get_total_memory().unwrap_or_default() > 128_000_000); 2696 }); 2697 2698 kill_child(&mut child); 2699 let output = child.wait_with_output().unwrap(); 2700 2701 handle_child_output(r, &output); 2702 } 2703 2704 #[test] 2705 fn test_power_button() { 2706 _test_power_button(false); 2707 } 2708 2709 #[test] 2710 #[cfg(not(feature = "mshv"))] 2711 fn test_user_defined_memory_regions() { 2712 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2713 let guest = Guest::new(Box::new(focal)); 2714 let api_socket = temp_api_path(&guest.tmp_dir); 2715 2716 let kernel_path = direct_kernel_boot_path(); 2717 2718 let mut child = GuestCommand::new(&guest) 2719 .args(["--cpus", "boot=1"]) 2720 .args(["--memory", "size=0,hotplug_method=virtio-mem"]) 2721 .args([ 2722 "--memory-zone", 2723 "id=mem0,size=1G,hotplug_size=2G", 2724 "id=mem1,size=1G,shared=on", 2725 "id=mem2,size=1G,host_numa_node=0,hotplug_size=2G", 2726 ]) 2727 .args(["--kernel", kernel_path.to_str().unwrap()]) 2728 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2729 .args(["--api-socket", &api_socket]) 2730 .capture_output() 2731 .default_disks() 2732 .default_net() 2733 .spawn() 2734 .unwrap(); 2735 2736 let r = std::panic::catch_unwind(|| { 2737 guest.wait_vm_boot(None).unwrap(); 2738 2739 assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); 2740 2741 guest.enable_memory_hotplug(); 2742 2743 resize_zone_command(&api_socket, "mem0", "3G"); 2744 thread::sleep(std::time::Duration::new(5, 0)); 2745 assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); 2746 resize_zone_command(&api_socket, "mem2", "3G"); 2747 thread::sleep(std::time::Duration::new(5, 0)); 2748 assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000); 2749 resize_zone_command(&api_socket, "mem0", "2G"); 2750 thread::sleep(std::time::Duration::new(5, 0)); 2751 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 2752 resize_zone_command(&api_socket, "mem2", "2G"); 2753 thread::sleep(std::time::Duration::new(5, 0)); 2754 assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); 2755 2756 guest.reboot_linux(0, None); 2757 2758 // Check the amount of RAM after reboot 2759 assert!(guest.get_total_memory().unwrap_or_default() > 4_800_000); 2760 assert!(guest.get_total_memory().unwrap_or_default() < 5_760_000); 2761 2762 // Check if we can still resize down to the initial 'boot'size 2763 resize_zone_command(&api_socket, "mem0", "1G"); 2764 thread::sleep(std::time::Duration::new(5, 0)); 2765 assert!(guest.get_total_memory().unwrap_or_default() < 4_800_000); 2766 resize_zone_command(&api_socket, "mem2", "1G"); 2767 thread::sleep(std::time::Duration::new(5, 0)); 2768 assert!(guest.get_total_memory().unwrap_or_default() < 3_840_000); 2769 }); 2770 2771 kill_child(&mut child); 2772 let output = child.wait_with_output().unwrap(); 2773 2774 handle_child_output(r, &output); 2775 } 2776 2777 #[test] 2778 #[cfg(not(feature = "mshv"))] 2779 fn test_guest_numa_nodes() { 2780 _test_guest_numa_nodes(false); 2781 } 2782 2783 #[test] 2784 #[cfg(target_arch = "x86_64")] 2785 fn test_iommu_segments() { 2786 let focal_image = FOCAL_IMAGE_NAME.to_string(); 2787 let focal = UbuntuDiskConfig::new(focal_image); 2788 let guest = Guest::new(Box::new(focal)); 2789 2790 // Prepare another disk file for the virtio-disk device 2791 let test_disk_path = String::from( 2792 guest 2793 .tmp_dir 2794 .as_path() 2795 .join("test-disk.raw") 2796 .to_str() 2797 .unwrap(), 2798 ); 2799 assert!( 2800 exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() 2801 ); 2802 assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); 2803 2804 let api_socket = temp_api_path(&guest.tmp_dir); 2805 let mut cmd = GuestCommand::new(&guest); 2806 2807 cmd.args(["--cpus", "boot=1"]) 2808 .args(["--api-socket", &api_socket]) 2809 .args(["--memory", "size=512M"]) 2810 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2811 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2812 .args([ 2813 "--platform", 2814 &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS},iommu_segments=[1]"), 2815 ]) 2816 .default_disks() 2817 .capture_output() 2818 .default_net(); 2819 2820 let mut child = cmd.spawn().unwrap(); 2821 2822 guest.wait_vm_boot(None).unwrap(); 2823 2824 let r = std::panic::catch_unwind(|| { 2825 let (cmd_success, cmd_output) = remote_command_w_output( 2826 &api_socket, 2827 "add-disk", 2828 Some( 2829 format!( 2830 "path={},id=test0,pci_segment=1,iommu=on", 2831 test_disk_path.as_str() 2832 ) 2833 .as_str(), 2834 ), 2835 ); 2836 assert!(cmd_success); 2837 assert!(String::from_utf8_lossy(&cmd_output) 2838 .contains("{\"id\":\"test0\",\"bdf\":\"0001:00:01.0\"}")); 2839 2840 // Check IOMMU setup 2841 assert!(guest 2842 .does_device_vendor_pair_match("0x1057", "0x1af4") 2843 .unwrap_or_default()); 2844 assert_eq!( 2845 guest 2846 .ssh_command("ls /sys/kernel/iommu_groups/0/devices") 2847 .unwrap() 2848 .trim(), 2849 "0001:00:01.0" 2850 ); 2851 }); 2852 2853 kill_child(&mut child); 2854 let output = child.wait_with_output().unwrap(); 2855 2856 handle_child_output(r, &output); 2857 } 2858 2859 #[test] 2860 fn test_pci_msi() { 2861 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2862 let guest = Guest::new(Box::new(focal)); 2863 let mut cmd = GuestCommand::new(&guest); 2864 cmd.args(["--cpus", "boot=1"]) 2865 .args(["--memory", "size=512M"]) 2866 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2867 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2868 .capture_output() 2869 .default_disks() 2870 .default_net(); 2871 2872 let mut child = cmd.spawn().unwrap(); 2873 2874 guest.wait_vm_boot(None).unwrap(); 2875 2876 #[cfg(target_arch = "x86_64")] 2877 let grep_cmd = "grep -c PCI-MSI /proc/interrupts"; 2878 #[cfg(target_arch = "aarch64")] 2879 let grep_cmd = "grep -c ITS-MSI /proc/interrupts"; 2880 2881 let r = std::panic::catch_unwind(|| { 2882 assert_eq!( 2883 guest 2884 .ssh_command(grep_cmd) 2885 .unwrap() 2886 .trim() 2887 .parse::<u32>() 2888 .unwrap_or_default(), 2889 12 2890 ); 2891 }); 2892 2893 kill_child(&mut child); 2894 let output = child.wait_with_output().unwrap(); 2895 2896 handle_child_output(r, &output); 2897 } 2898 2899 #[test] 2900 fn test_virtio_net_ctrl_queue() { 2901 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2902 let guest = Guest::new(Box::new(focal)); 2903 let mut cmd = GuestCommand::new(&guest); 2904 cmd.args(["--cpus", "boot=1"]) 2905 .args(["--memory", "size=512M"]) 2906 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2907 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2908 .args(["--net", guest.default_net_string_w_mtu(3000).as_str()]) 2909 .capture_output() 2910 .default_disks(); 2911 2912 let mut child = cmd.spawn().unwrap(); 2913 2914 guest.wait_vm_boot(None).unwrap(); 2915 2916 #[cfg(target_arch = "aarch64")] 2917 let iface = "enp0s4"; 2918 #[cfg(target_arch = "x86_64")] 2919 let iface = "ens4"; 2920 2921 let r = std::panic::catch_unwind(|| { 2922 assert_eq!( 2923 guest 2924 .ssh_command( 2925 format!("sudo ethtool -K {iface} rx-gro-hw off && echo success").as_str() 2926 ) 2927 .unwrap() 2928 .trim(), 2929 "success" 2930 ); 2931 assert_eq!( 2932 guest 2933 .ssh_command(format!("cat /sys/class/net/{iface}/mtu").as_str()) 2934 .unwrap() 2935 .trim(), 2936 "3000" 2937 ); 2938 }); 2939 2940 kill_child(&mut child); 2941 let output = child.wait_with_output().unwrap(); 2942 2943 handle_child_output(r, &output); 2944 } 2945 2946 #[test] 2947 fn test_pci_multiple_segments() { 2948 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 2949 let guest = Guest::new(Box::new(focal)); 2950 2951 // Prepare another disk file for the virtio-disk device 2952 let test_disk_path = String::from( 2953 guest 2954 .tmp_dir 2955 .as_path() 2956 .join("test-disk.raw") 2957 .to_str() 2958 .unwrap(), 2959 ); 2960 assert!( 2961 exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() 2962 ); 2963 assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); 2964 2965 let mut cmd = GuestCommand::new(&guest); 2966 cmd.args(["--cpus", "boot=1"]) 2967 .args(["--memory", "size=512M"]) 2968 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 2969 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 2970 .args([ 2971 "--platform", 2972 &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), 2973 ]) 2974 .args([ 2975 "--disk", 2976 format!( 2977 "path={}", 2978 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 2979 ) 2980 .as_str(), 2981 format!( 2982 "path={}", 2983 guest.disk_config.disk(DiskType::CloudInit).unwrap() 2984 ) 2985 .as_str(), 2986 format!("path={test_disk_path},pci_segment=15").as_str(), 2987 ]) 2988 .capture_output() 2989 .default_net(); 2990 2991 let mut child = cmd.spawn().unwrap(); 2992 2993 guest.wait_vm_boot(None).unwrap(); 2994 2995 let grep_cmd = "lspci | grep \"Host bridge\" | wc -l"; 2996 2997 let r = std::panic::catch_unwind(|| { 2998 // There should be MAX_NUM_PCI_SEGMENTS PCI host bridges in the guest. 2999 assert_eq!( 3000 guest 3001 .ssh_command(grep_cmd) 3002 .unwrap() 3003 .trim() 3004 .parse::<u16>() 3005 .unwrap_or_default(), 3006 MAX_NUM_PCI_SEGMENTS 3007 ); 3008 3009 // Check both if /dev/vdc exists and if the block size is 4M. 3010 assert_eq!( 3011 guest 3012 .ssh_command("lsblk | grep vdc | grep -c 4M") 3013 .unwrap() 3014 .trim() 3015 .parse::<u32>() 3016 .unwrap_or_default(), 3017 1 3018 ); 3019 3020 // Mount the device. 3021 guest.ssh_command("mkdir mount_image").unwrap(); 3022 guest 3023 .ssh_command("sudo mount -o rw -t ext4 /dev/vdc mount_image/") 3024 .unwrap(); 3025 // Grant all users with write permission. 3026 guest.ssh_command("sudo chmod a+w mount_image/").unwrap(); 3027 3028 // Write something to the device. 3029 guest 3030 .ssh_command("sudo echo \"bar\" >> mount_image/foo") 3031 .unwrap(); 3032 3033 // Check the content of the block device. The file "foo" should 3034 // contain "bar". 3035 assert_eq!( 3036 guest 3037 .ssh_command("sudo cat mount_image/foo") 3038 .unwrap() 3039 .trim(), 3040 "bar" 3041 ); 3042 }); 3043 3044 kill_child(&mut child); 3045 let output = child.wait_with_output().unwrap(); 3046 3047 handle_child_output(r, &output); 3048 } 3049 3050 #[test] 3051 fn test_pci_multiple_segments_numa_node() { 3052 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3053 let guest = Guest::new(Box::new(focal)); 3054 let api_socket = temp_api_path(&guest.tmp_dir); 3055 #[cfg(target_arch = "x86_64")] 3056 let kernel_path = direct_kernel_boot_path(); 3057 #[cfg(target_arch = "aarch64")] 3058 let kernel_path = edk2_path(); 3059 3060 // Prepare another disk file for the virtio-disk device 3061 let test_disk_path = String::from( 3062 guest 3063 .tmp_dir 3064 .as_path() 3065 .join("test-disk.raw") 3066 .to_str() 3067 .unwrap(), 3068 ); 3069 assert!( 3070 exec_host_command_status(format!("truncate {test_disk_path} -s 4M").as_str()).success() 3071 ); 3072 assert!(exec_host_command_status(format!("mkfs.ext4 {test_disk_path}").as_str()).success()); 3073 const TEST_DISK_NODE: u16 = 1; 3074 3075 let mut child = GuestCommand::new(&guest) 3076 .args(["--platform", "num_pci_segments=2"]) 3077 .args(["--cpus", "boot=2"]) 3078 .args(["--memory", "size=0"]) 3079 .args(["--memory-zone", "id=mem0,size=256M", "id=mem1,size=256M"]) 3080 .args([ 3081 "--numa", 3082 "guest_numa_id=0,cpus=[0],distances=[1@20],memory_zones=mem0,pci_segments=[0]", 3083 "guest_numa_id=1,cpus=[1],distances=[0@20],memory_zones=mem1,pci_segments=[1]", 3084 ]) 3085 .args(["--kernel", kernel_path.to_str().unwrap()]) 3086 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3087 .args(["--api-socket", &api_socket]) 3088 .capture_output() 3089 .args([ 3090 "--disk", 3091 format!( 3092 "path={}", 3093 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 3094 ) 3095 .as_str(), 3096 format!( 3097 "path={}", 3098 guest.disk_config.disk(DiskType::CloudInit).unwrap() 3099 ) 3100 .as_str(), 3101 format!("path={test_disk_path},pci_segment={TEST_DISK_NODE}").as_str(), 3102 ]) 3103 .default_net() 3104 .spawn() 3105 .unwrap(); 3106 3107 let cmd = "cat /sys/block/vdc/device/../numa_node"; 3108 3109 let r = std::panic::catch_unwind(|| { 3110 guest.wait_vm_boot(None).unwrap(); 3111 3112 assert_eq!( 3113 guest 3114 .ssh_command(cmd) 3115 .unwrap() 3116 .trim() 3117 .parse::<u16>() 3118 .unwrap_or_default(), 3119 TEST_DISK_NODE 3120 ); 3121 }); 3122 3123 kill_child(&mut child); 3124 let output = child.wait_with_output().unwrap(); 3125 3126 handle_child_output(r, &output); 3127 } 3128 3129 #[test] 3130 fn test_direct_kernel_boot() { 3131 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3132 let guest = Guest::new(Box::new(focal)); 3133 3134 let kernel_path = direct_kernel_boot_path(); 3135 3136 let mut child = GuestCommand::new(&guest) 3137 .args(["--cpus", "boot=1"]) 3138 .args(["--memory", "size=512M"]) 3139 .args(["--kernel", kernel_path.to_str().unwrap()]) 3140 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3141 .default_disks() 3142 .default_net() 3143 .capture_output() 3144 .spawn() 3145 .unwrap(); 3146 3147 let r = std::panic::catch_unwind(|| { 3148 guest.wait_vm_boot(None).unwrap(); 3149 3150 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 3151 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 3152 3153 let grep_cmd = if cfg!(target_arch = "x86_64") { 3154 "grep -c PCI-MSI /proc/interrupts" 3155 } else { 3156 "grep -c ITS-MSI /proc/interrupts" 3157 }; 3158 assert_eq!( 3159 guest 3160 .ssh_command(grep_cmd) 3161 .unwrap() 3162 .trim() 3163 .parse::<u32>() 3164 .unwrap_or_default(), 3165 12 3166 ); 3167 }); 3168 3169 kill_child(&mut child); 3170 let output = child.wait_with_output().unwrap(); 3171 3172 handle_child_output(r, &output); 3173 } 3174 3175 #[test] 3176 #[cfg(target_arch = "x86_64")] 3177 fn test_direct_kernel_boot_bzimage() { 3178 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3179 let guest = Guest::new(Box::new(focal)); 3180 3181 let mut kernel_path = direct_kernel_boot_path(); 3182 // Replace the default kernel with the bzImage. 3183 kernel_path.pop(); 3184 kernel_path.push("bzImage"); 3185 3186 let mut child = GuestCommand::new(&guest) 3187 .args(["--cpus", "boot=1"]) 3188 .args(["--memory", "size=512M"]) 3189 .args(["--kernel", kernel_path.to_str().unwrap()]) 3190 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3191 .default_disks() 3192 .default_net() 3193 .capture_output() 3194 .spawn() 3195 .unwrap(); 3196 3197 let r = std::panic::catch_unwind(|| { 3198 guest.wait_vm_boot(None).unwrap(); 3199 3200 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 3201 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 3202 3203 let grep_cmd = if cfg!(target_arch = "x86_64") { 3204 "grep -c PCI-MSI /proc/interrupts" 3205 } else { 3206 "grep -c ITS-MSI /proc/interrupts" 3207 }; 3208 assert_eq!( 3209 guest 3210 .ssh_command(grep_cmd) 3211 .unwrap() 3212 .trim() 3213 .parse::<u32>() 3214 .unwrap_or_default(), 3215 12 3216 ); 3217 }); 3218 3219 kill_child(&mut child); 3220 let output = child.wait_with_output().unwrap(); 3221 3222 handle_child_output(r, &output); 3223 } 3224 3225 fn _test_virtio_block(image_name: &str, disable_io_uring: bool, disable_aio: bool) { 3226 let focal = UbuntuDiskConfig::new(image_name.to_string()); 3227 let guest = Guest::new(Box::new(focal)); 3228 3229 let mut workload_path = dirs::home_dir().unwrap(); 3230 workload_path.push("workloads"); 3231 3232 let mut blk_file_path = workload_path; 3233 blk_file_path.push("blk.img"); 3234 3235 let kernel_path = direct_kernel_boot_path(); 3236 3237 let mut cloud_child = GuestCommand::new(&guest) 3238 .args(["--cpus", "boot=4"]) 3239 .args(["--memory", "size=512M,shared=on"]) 3240 .args(["--kernel", kernel_path.to_str().unwrap()]) 3241 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3242 .args([ 3243 "--disk", 3244 format!( 3245 "path={}", 3246 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 3247 ) 3248 .as_str(), 3249 format!( 3250 "path={}", 3251 guest.disk_config.disk(DiskType::CloudInit).unwrap() 3252 ) 3253 .as_str(), 3254 format!( 3255 "path={},readonly=on,direct=on,num_queues=4,_disable_io_uring={},_disable_aio={}", 3256 blk_file_path.to_str().unwrap(), 3257 disable_io_uring, 3258 disable_aio, 3259 ) 3260 .as_str(), 3261 ]) 3262 .default_net() 3263 .capture_output() 3264 .spawn() 3265 .unwrap(); 3266 3267 let r = std::panic::catch_unwind(|| { 3268 guest.wait_vm_boot(None).unwrap(); 3269 3270 // Check both if /dev/vdc exists and if the block size is 16M. 3271 assert_eq!( 3272 guest 3273 .ssh_command("lsblk | grep vdc | grep -c 16M") 3274 .unwrap() 3275 .trim() 3276 .parse::<u32>() 3277 .unwrap_or_default(), 3278 1 3279 ); 3280 3281 // Check both if /dev/vdc exists and if this block is RO. 3282 assert_eq!( 3283 guest 3284 .ssh_command("lsblk | grep vdc | awk '{print $5}'") 3285 .unwrap() 3286 .trim() 3287 .parse::<u32>() 3288 .unwrap_or_default(), 3289 1 3290 ); 3291 3292 // Check if the number of queues is 4. 3293 assert_eq!( 3294 guest 3295 .ssh_command("ls -ll /sys/block/vdc/mq | grep ^d | wc -l") 3296 .unwrap() 3297 .trim() 3298 .parse::<u32>() 3299 .unwrap_or_default(), 3300 4 3301 ); 3302 }); 3303 3304 let _ = cloud_child.kill(); 3305 let output = cloud_child.wait_with_output().unwrap(); 3306 3307 handle_child_output(r, &output); 3308 } 3309 3310 #[test] 3311 fn test_virtio_block_io_uring() { 3312 _test_virtio_block(FOCAL_IMAGE_NAME, false, true) 3313 } 3314 3315 #[test] 3316 fn test_virtio_block_aio() { 3317 _test_virtio_block(FOCAL_IMAGE_NAME, true, false) 3318 } 3319 3320 #[test] 3321 fn test_virtio_block_sync() { 3322 _test_virtio_block(FOCAL_IMAGE_NAME, true, true) 3323 } 3324 3325 #[test] 3326 fn test_virtio_block_qcow2() { 3327 _test_virtio_block(FOCAL_IMAGE_NAME_QCOW2, false, false) 3328 } 3329 3330 #[test] 3331 fn test_virtio_block_qcow2_backing_file() { 3332 _test_virtio_block(FOCAL_IMAGE_NAME_QCOW2_BACKING_FILE, false, false) 3333 } 3334 3335 #[test] 3336 fn test_virtio_block_vhd() { 3337 let mut workload_path = dirs::home_dir().unwrap(); 3338 workload_path.push("workloads"); 3339 3340 let mut raw_file_path = workload_path.clone(); 3341 let mut vhd_file_path = workload_path; 3342 raw_file_path.push(FOCAL_IMAGE_NAME); 3343 vhd_file_path.push(FOCAL_IMAGE_NAME_VHD); 3344 3345 // Generate VHD file from RAW file 3346 std::process::Command::new("qemu-img") 3347 .arg("convert") 3348 .arg("-p") 3349 .args(["-f", "raw"]) 3350 .args(["-O", "vpc"]) 3351 .args(["-o", "subformat=fixed"]) 3352 .arg(raw_file_path.to_str().unwrap()) 3353 .arg(vhd_file_path.to_str().unwrap()) 3354 .output() 3355 .expect("Expect generating VHD image from RAW image"); 3356 3357 _test_virtio_block(FOCAL_IMAGE_NAME_VHD, false, false) 3358 } 3359 3360 #[test] 3361 fn test_virtio_block_vhdx() { 3362 let mut workload_path = dirs::home_dir().unwrap(); 3363 workload_path.push("workloads"); 3364 3365 let mut raw_file_path = workload_path.clone(); 3366 let mut vhdx_file_path = workload_path; 3367 raw_file_path.push(FOCAL_IMAGE_NAME); 3368 vhdx_file_path.push(FOCAL_IMAGE_NAME_VHDX); 3369 3370 // Generate dynamic VHDX file from RAW file 3371 std::process::Command::new("qemu-img") 3372 .arg("convert") 3373 .arg("-p") 3374 .args(["-f", "raw"]) 3375 .args(["-O", "vhdx"]) 3376 .arg(raw_file_path.to_str().unwrap()) 3377 .arg(vhdx_file_path.to_str().unwrap()) 3378 .output() 3379 .expect("Expect generating dynamic VHDx image from RAW image"); 3380 3381 _test_virtio_block(FOCAL_IMAGE_NAME_VHDX, false, false) 3382 } 3383 3384 #[test] 3385 fn test_virtio_block_dynamic_vhdx_expand() { 3386 const VIRTUAL_DISK_SIZE: u64 = 100 << 20; 3387 const EMPTY_VHDX_FILE_SIZE: u64 = 8 << 20; 3388 const FULL_VHDX_FILE_SIZE: u64 = 112 << 20; 3389 const DYNAMIC_VHDX_NAME: &str = "dynamic.vhdx"; 3390 3391 let mut workload_path = dirs::home_dir().unwrap(); 3392 workload_path.push("workloads"); 3393 3394 let mut vhdx_file_path = workload_path; 3395 vhdx_file_path.push(DYNAMIC_VHDX_NAME); 3396 let vhdx_path = vhdx_file_path.to_str().unwrap(); 3397 3398 // Generate a 100 MiB dynamic VHDX file 3399 std::process::Command::new("qemu-img") 3400 .arg("create") 3401 .args(["-f", "vhdx"]) 3402 .arg(vhdx_path) 3403 .arg(VIRTUAL_DISK_SIZE.to_string()) 3404 .output() 3405 .expect("Expect generating dynamic VHDx image from RAW image"); 3406 3407 // Check if the size matches with empty VHDx file size 3408 assert_eq!(vhdx_image_size(vhdx_path), EMPTY_VHDX_FILE_SIZE); 3409 3410 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3411 let guest = Guest::new(Box::new(focal)); 3412 let kernel_path = direct_kernel_boot_path(); 3413 3414 let mut cloud_child = GuestCommand::new(&guest) 3415 .args(["--cpus", "boot=1"]) 3416 .args(["--memory", "size=512M"]) 3417 .args(["--kernel", kernel_path.to_str().unwrap()]) 3418 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3419 .args([ 3420 "--disk", 3421 format!( 3422 "path={}", 3423 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 3424 ) 3425 .as_str(), 3426 format!( 3427 "path={}", 3428 guest.disk_config.disk(DiskType::CloudInit).unwrap() 3429 ) 3430 .as_str(), 3431 format!("path={vhdx_path}").as_str(), 3432 ]) 3433 .default_net() 3434 .capture_output() 3435 .spawn() 3436 .unwrap(); 3437 3438 let r = std::panic::catch_unwind(|| { 3439 guest.wait_vm_boot(None).unwrap(); 3440 3441 // Check both if /dev/vdc exists and if the block size is 100 MiB. 3442 assert_eq!( 3443 guest 3444 .ssh_command("lsblk | grep vdc | grep -c 100M") 3445 .unwrap() 3446 .trim() 3447 .parse::<u32>() 3448 .unwrap_or_default(), 3449 1 3450 ); 3451 3452 // Write 100 MB of data to the VHDx disk 3453 guest 3454 .ssh_command("sudo dd if=/dev/urandom of=/dev/vdc bs=1M count=100") 3455 .unwrap(); 3456 }); 3457 3458 // Check if the size matches with expected expanded VHDx file size 3459 assert_eq!(vhdx_image_size(vhdx_path), FULL_VHDX_FILE_SIZE); 3460 3461 kill_child(&mut cloud_child); 3462 let output = cloud_child.wait_with_output().unwrap(); 3463 3464 handle_child_output(r, &output); 3465 } 3466 3467 fn vhdx_image_size(disk_name: &str) -> u64 { 3468 std::fs::File::open(disk_name) 3469 .unwrap() 3470 .seek(SeekFrom::End(0)) 3471 .unwrap() 3472 } 3473 3474 #[test] 3475 fn test_virtio_block_direct_and_firmware() { 3476 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3477 let guest = Guest::new(Box::new(focal)); 3478 3479 // The OS disk must be copied to a location that is not backed by 3480 // tmpfs, otherwise the syscall openat(2) with O_DIRECT simply fails 3481 // with EINVAL because tmpfs doesn't support this flag. 3482 let mut workloads_path = dirs::home_dir().unwrap(); 3483 workloads_path.push("workloads"); 3484 let os_dir = TempDir::new_in(workloads_path.as_path()).unwrap(); 3485 let mut os_path = os_dir.as_path().to_path_buf(); 3486 os_path.push("osdisk.img"); 3487 rate_limited_copy( 3488 guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), 3489 os_path.as_path(), 3490 ) 3491 .expect("copying of OS disk failed"); 3492 3493 let mut child = GuestCommand::new(&guest) 3494 .args(["--cpus", "boot=1"]) 3495 .args(["--memory", "size=512M"]) 3496 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 3497 .args([ 3498 "--disk", 3499 format!("path={},direct=on", os_path.as_path().to_str().unwrap()).as_str(), 3500 format!( 3501 "path={}", 3502 guest.disk_config.disk(DiskType::CloudInit).unwrap() 3503 ) 3504 .as_str(), 3505 ]) 3506 .default_net() 3507 .capture_output() 3508 .spawn() 3509 .unwrap(); 3510 3511 let r = std::panic::catch_unwind(|| { 3512 guest.wait_vm_boot(Some(120)).unwrap(); 3513 }); 3514 3515 kill_child(&mut child); 3516 let output = child.wait_with_output().unwrap(); 3517 3518 handle_child_output(r, &output); 3519 } 3520 3521 #[test] 3522 fn test_vhost_user_net_default() { 3523 test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, false) 3524 } 3525 3526 #[test] 3527 fn test_vhost_user_net_named_tap() { 3528 test_vhost_user_net( 3529 Some("mytap0"), 3530 2, 3531 &prepare_vhost_user_net_daemon, 3532 false, 3533 false, 3534 ) 3535 } 3536 3537 #[test] 3538 fn test_vhost_user_net_existing_tap() { 3539 test_vhost_user_net( 3540 Some("vunet-tap0"), 3541 2, 3542 &prepare_vhost_user_net_daemon, 3543 false, 3544 false, 3545 ) 3546 } 3547 3548 #[test] 3549 fn test_vhost_user_net_multiple_queues() { 3550 test_vhost_user_net(None, 4, &prepare_vhost_user_net_daemon, false, false) 3551 } 3552 3553 #[test] 3554 fn test_vhost_user_net_tap_multiple_queues() { 3555 test_vhost_user_net( 3556 Some("vunet-tap1"), 3557 4, 3558 &prepare_vhost_user_net_daemon, 3559 false, 3560 false, 3561 ) 3562 } 3563 3564 #[test] 3565 fn test_vhost_user_net_host_mac() { 3566 test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, true, false) 3567 } 3568 3569 #[test] 3570 fn test_vhost_user_net_client_mode() { 3571 test_vhost_user_net(None, 2, &prepare_vhost_user_net_daemon, false, true) 3572 } 3573 3574 #[test] 3575 #[cfg(not(target_arch = "aarch64"))] 3576 fn test_vhost_user_blk_default() { 3577 test_vhost_user_blk(2, false, false, Some(&prepare_vubd)) 3578 } 3579 3580 #[test] 3581 #[cfg(not(target_arch = "aarch64"))] 3582 fn test_vhost_user_blk_readonly() { 3583 test_vhost_user_blk(1, true, false, Some(&prepare_vubd)) 3584 } 3585 3586 #[test] 3587 #[cfg(not(target_arch = "aarch64"))] 3588 fn test_vhost_user_blk_direct() { 3589 test_vhost_user_blk(1, false, true, Some(&prepare_vubd)) 3590 } 3591 3592 #[test] 3593 fn test_boot_from_vhost_user_blk_default() { 3594 test_boot_from_vhost_user_blk(1, false, false, Some(&prepare_vubd)) 3595 } 3596 3597 #[test] 3598 #[cfg(target_arch = "x86_64")] 3599 fn test_split_irqchip() { 3600 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3601 let guest = Guest::new(Box::new(focal)); 3602 3603 let mut child = GuestCommand::new(&guest) 3604 .args(["--cpus", "boot=1"]) 3605 .args(["--memory", "size=512M"]) 3606 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3607 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3608 .default_disks() 3609 .default_net() 3610 .capture_output() 3611 .spawn() 3612 .unwrap(); 3613 3614 let r = std::panic::catch_unwind(|| { 3615 guest.wait_vm_boot(None).unwrap(); 3616 3617 assert_eq!( 3618 guest 3619 .ssh_command("grep -c IO-APIC.*timer /proc/interrupts || true") 3620 .unwrap() 3621 .trim() 3622 .parse::<u32>() 3623 .unwrap_or(1), 3624 0 3625 ); 3626 assert_eq!( 3627 guest 3628 .ssh_command("grep -c IO-APIC.*cascade /proc/interrupts || true") 3629 .unwrap() 3630 .trim() 3631 .parse::<u32>() 3632 .unwrap_or(1), 3633 0 3634 ); 3635 }); 3636 3637 kill_child(&mut child); 3638 let output = child.wait_with_output().unwrap(); 3639 3640 handle_child_output(r, &output); 3641 } 3642 3643 #[test] 3644 #[cfg(target_arch = "x86_64")] 3645 fn test_dmi_serial_number() { 3646 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3647 let guest = Guest::new(Box::new(focal)); 3648 3649 let mut child = GuestCommand::new(&guest) 3650 .args(["--cpus", "boot=1"]) 3651 .args(["--memory", "size=512M"]) 3652 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3653 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3654 .args(["--platform", "serial_number=a=b;c=d"]) 3655 .default_disks() 3656 .default_net() 3657 .capture_output() 3658 .spawn() 3659 .unwrap(); 3660 3661 let r = std::panic::catch_unwind(|| { 3662 guest.wait_vm_boot(None).unwrap(); 3663 3664 assert_eq!( 3665 guest 3666 .ssh_command("sudo cat /sys/class/dmi/id/product_serial") 3667 .unwrap() 3668 .trim(), 3669 "a=b;c=d" 3670 ); 3671 }); 3672 3673 kill_child(&mut child); 3674 let output = child.wait_with_output().unwrap(); 3675 3676 handle_child_output(r, &output); 3677 } 3678 3679 #[test] 3680 #[cfg(target_arch = "x86_64")] 3681 fn test_dmi_uuid() { 3682 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3683 let guest = Guest::new(Box::new(focal)); 3684 3685 let mut child = GuestCommand::new(&guest) 3686 .args(["--cpus", "boot=1"]) 3687 .args(["--memory", "size=512M"]) 3688 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3689 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3690 .args(["--platform", "uuid=1e8aa28a-435d-4027-87f4-40dceff1fa0a"]) 3691 .default_disks() 3692 .default_net() 3693 .capture_output() 3694 .spawn() 3695 .unwrap(); 3696 3697 let r = std::panic::catch_unwind(|| { 3698 guest.wait_vm_boot(None).unwrap(); 3699 3700 assert_eq!( 3701 guest 3702 .ssh_command("sudo cat /sys/class/dmi/id/product_uuid") 3703 .unwrap() 3704 .trim(), 3705 "1e8aa28a-435d-4027-87f4-40dceff1fa0a" 3706 ); 3707 }); 3708 3709 kill_child(&mut child); 3710 let output = child.wait_with_output().unwrap(); 3711 3712 handle_child_output(r, &output); 3713 } 3714 3715 #[test] 3716 #[cfg(target_arch = "x86_64")] 3717 fn test_dmi_oem_strings() { 3718 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3719 let guest = Guest::new(Box::new(focal)); 3720 3721 let s1 = "io.systemd.credential:xx=yy"; 3722 let s2 = "This is a test string"; 3723 3724 let oem_strings = format!("oem_strings=[{s1},{s2}]"); 3725 3726 let mut child = GuestCommand::new(&guest) 3727 .args(["--cpus", "boot=1"]) 3728 .args(["--memory", "size=512M"]) 3729 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3730 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3731 .args(["--platform", &oem_strings]) 3732 .default_disks() 3733 .default_net() 3734 .capture_output() 3735 .spawn() 3736 .unwrap(); 3737 3738 let r = std::panic::catch_unwind(|| { 3739 guest.wait_vm_boot(None).unwrap(); 3740 3741 assert_eq!( 3742 guest 3743 .ssh_command("sudo dmidecode --oem-string count") 3744 .unwrap() 3745 .trim(), 3746 "2" 3747 ); 3748 3749 assert_eq!( 3750 guest 3751 .ssh_command("sudo dmidecode --oem-string 1") 3752 .unwrap() 3753 .trim(), 3754 s1 3755 ); 3756 3757 assert_eq!( 3758 guest 3759 .ssh_command("sudo dmidecode --oem-string 2") 3760 .unwrap() 3761 .trim(), 3762 s2 3763 ); 3764 }); 3765 3766 kill_child(&mut child); 3767 let output = child.wait_with_output().unwrap(); 3768 3769 handle_child_output(r, &output); 3770 } 3771 3772 #[test] 3773 fn test_virtio_fs() { 3774 _test_virtio_fs(&prepare_virtiofsd, false, None) 3775 } 3776 3777 #[test] 3778 fn test_virtio_fs_hotplug() { 3779 _test_virtio_fs(&prepare_virtiofsd, true, None) 3780 } 3781 3782 #[test] 3783 #[cfg(not(feature = "mshv"))] 3784 fn test_virtio_fs_multi_segment_hotplug() { 3785 _test_virtio_fs(&prepare_virtiofsd, true, Some(15)) 3786 } 3787 3788 #[test] 3789 #[cfg(not(feature = "mshv"))] 3790 fn test_virtio_fs_multi_segment() { 3791 _test_virtio_fs(&prepare_virtiofsd, false, Some(15)) 3792 } 3793 3794 #[test] 3795 fn test_virtio_pmem_persist_writes() { 3796 test_virtio_pmem(false, false) 3797 } 3798 3799 #[test] 3800 fn test_virtio_pmem_discard_writes() { 3801 test_virtio_pmem(true, false) 3802 } 3803 3804 #[test] 3805 fn test_virtio_pmem_with_size() { 3806 test_virtio_pmem(true, true) 3807 } 3808 3809 #[test] 3810 fn test_boot_from_virtio_pmem() { 3811 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3812 let guest = Guest::new(Box::new(focal)); 3813 3814 let kernel_path = direct_kernel_boot_path(); 3815 3816 let mut child = GuestCommand::new(&guest) 3817 .args(["--cpus", "boot=1"]) 3818 .args(["--memory", "size=512M"]) 3819 .args(["--kernel", kernel_path.to_str().unwrap()]) 3820 .args([ 3821 "--disk", 3822 format!( 3823 "path={}", 3824 guest.disk_config.disk(DiskType::CloudInit).unwrap() 3825 ) 3826 .as_str(), 3827 ]) 3828 .default_net() 3829 .args([ 3830 "--pmem", 3831 format!( 3832 "file={},size={}", 3833 guest.disk_config.disk(DiskType::OperatingSystem).unwrap(), 3834 fs::metadata(guest.disk_config.disk(DiskType::OperatingSystem).unwrap()) 3835 .unwrap() 3836 .len() 3837 ) 3838 .as_str(), 3839 ]) 3840 .args([ 3841 "--cmdline", 3842 DIRECT_KERNEL_BOOT_CMDLINE 3843 .replace("vda1", "pmem0p1") 3844 .as_str(), 3845 ]) 3846 .capture_output() 3847 .spawn() 3848 .unwrap(); 3849 3850 let r = std::panic::catch_unwind(|| { 3851 guest.wait_vm_boot(None).unwrap(); 3852 3853 // Simple checks to validate the VM booted properly 3854 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 3855 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 3856 }); 3857 3858 kill_child(&mut child); 3859 let output = child.wait_with_output().unwrap(); 3860 3861 handle_child_output(r, &output); 3862 } 3863 3864 #[test] 3865 fn test_multiple_network_interfaces() { 3866 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3867 let guest = Guest::new(Box::new(focal)); 3868 3869 let kernel_path = direct_kernel_boot_path(); 3870 3871 let mut child = GuestCommand::new(&guest) 3872 .args(["--cpus", "boot=1"]) 3873 .args(["--memory", "size=512M"]) 3874 .args(["--kernel", kernel_path.to_str().unwrap()]) 3875 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3876 .default_disks() 3877 .args([ 3878 "--net", 3879 guest.default_net_string().as_str(), 3880 "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", 3881 "tap=mytap1,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0", 3882 ]) 3883 .capture_output() 3884 .spawn() 3885 .unwrap(); 3886 3887 let r = std::panic::catch_unwind(|| { 3888 guest.wait_vm_boot(None).unwrap(); 3889 3890 let tap_count = exec_host_command_output("ip link | grep -c mytap1"); 3891 assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); 3892 3893 // 3 network interfaces + default localhost ==> 4 interfaces 3894 assert_eq!( 3895 guest 3896 .ssh_command("ip -o link | wc -l") 3897 .unwrap() 3898 .trim() 3899 .parse::<u32>() 3900 .unwrap_or_default(), 3901 4 3902 ); 3903 }); 3904 3905 kill_child(&mut child); 3906 let output = child.wait_with_output().unwrap(); 3907 3908 handle_child_output(r, &output); 3909 } 3910 3911 #[test] 3912 #[cfg(target_arch = "aarch64")] 3913 fn test_pmu_on() { 3914 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3915 let guest = Guest::new(Box::new(focal)); 3916 let mut child = GuestCommand::new(&guest) 3917 .args(["--cpus", "boot=1"]) 3918 .args(["--memory", "size=512M"]) 3919 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3920 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3921 .default_disks() 3922 .default_net() 3923 .capture_output() 3924 .spawn() 3925 .unwrap(); 3926 3927 let r = std::panic::catch_unwind(|| { 3928 guest.wait_vm_boot(None).unwrap(); 3929 3930 // Test that PMU exists. 3931 assert_eq!( 3932 guest 3933 .ssh_command(GREP_PMU_IRQ_CMD) 3934 .unwrap() 3935 .trim() 3936 .parse::<u32>() 3937 .unwrap_or_default(), 3938 1 3939 ); 3940 }); 3941 3942 kill_child(&mut child); 3943 let output = child.wait_with_output().unwrap(); 3944 3945 handle_child_output(r, &output); 3946 } 3947 3948 #[test] 3949 fn test_serial_off() { 3950 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3951 let guest = Guest::new(Box::new(focal)); 3952 let mut child = GuestCommand::new(&guest) 3953 .args(["--cpus", "boot=1"]) 3954 .args(["--memory", "size=512M"]) 3955 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3956 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 3957 .default_disks() 3958 .default_net() 3959 .args(["--serial", "off"]) 3960 .capture_output() 3961 .spawn() 3962 .unwrap(); 3963 3964 let r = std::panic::catch_unwind(|| { 3965 guest.wait_vm_boot(None).unwrap(); 3966 3967 // Test that there is no ttyS0 3968 assert_eq!( 3969 guest 3970 .ssh_command(GREP_SERIAL_IRQ_CMD) 3971 .unwrap() 3972 .trim() 3973 .parse::<u32>() 3974 .unwrap_or(1), 3975 0 3976 ); 3977 }); 3978 3979 kill_child(&mut child); 3980 let output = child.wait_with_output().unwrap(); 3981 3982 handle_child_output(r, &output); 3983 } 3984 3985 #[test] 3986 fn test_serial_null() { 3987 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 3988 let guest = Guest::new(Box::new(focal)); 3989 let mut cmd = GuestCommand::new(&guest); 3990 #[cfg(target_arch = "x86_64")] 3991 let console_str: &str = "console=ttyS0"; 3992 #[cfg(target_arch = "aarch64")] 3993 let console_str: &str = "console=ttyAMA0"; 3994 3995 cmd.args(["--cpus", "boot=1"]) 3996 .args(["--memory", "size=512M"]) 3997 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 3998 .args([ 3999 "--cmdline", 4000 DIRECT_KERNEL_BOOT_CMDLINE 4001 .replace("console=hvc0 ", console_str) 4002 .as_str(), 4003 ]) 4004 .default_disks() 4005 .default_net() 4006 .args(["--serial", "null"]) 4007 .args(["--console", "off"]) 4008 .capture_output(); 4009 4010 let mut child = cmd.spawn().unwrap(); 4011 4012 let r = std::panic::catch_unwind(|| { 4013 guest.wait_vm_boot(None).unwrap(); 4014 4015 // Test that there is a ttyS0 4016 assert_eq!( 4017 guest 4018 .ssh_command(GREP_SERIAL_IRQ_CMD) 4019 .unwrap() 4020 .trim() 4021 .parse::<u32>() 4022 .unwrap_or_default(), 4023 1 4024 ); 4025 }); 4026 4027 kill_child(&mut child); 4028 let output = child.wait_with_output().unwrap(); 4029 handle_child_output(r, &output); 4030 4031 let r = std::panic::catch_unwind(|| { 4032 assert!(!String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); 4033 }); 4034 4035 handle_child_output(r, &output); 4036 } 4037 4038 #[test] 4039 fn test_serial_tty() { 4040 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4041 let guest = Guest::new(Box::new(focal)); 4042 4043 let kernel_path = direct_kernel_boot_path(); 4044 4045 #[cfg(target_arch = "x86_64")] 4046 let console_str: &str = "console=ttyS0"; 4047 #[cfg(target_arch = "aarch64")] 4048 let console_str: &str = "console=ttyAMA0"; 4049 4050 let mut child = GuestCommand::new(&guest) 4051 .args(["--cpus", "boot=1"]) 4052 .args(["--memory", "size=512M"]) 4053 .args(["--kernel", kernel_path.to_str().unwrap()]) 4054 .args([ 4055 "--cmdline", 4056 DIRECT_KERNEL_BOOT_CMDLINE 4057 .replace("console=hvc0 ", console_str) 4058 .as_str(), 4059 ]) 4060 .default_disks() 4061 .default_net() 4062 .args(["--serial", "tty"]) 4063 .args(["--console", "off"]) 4064 .capture_output() 4065 .spawn() 4066 .unwrap(); 4067 4068 let r = std::panic::catch_unwind(|| { 4069 guest.wait_vm_boot(None).unwrap(); 4070 4071 // Test that there is a ttyS0 4072 assert_eq!( 4073 guest 4074 .ssh_command(GREP_SERIAL_IRQ_CMD) 4075 .unwrap() 4076 .trim() 4077 .parse::<u32>() 4078 .unwrap_or_default(), 4079 1 4080 ); 4081 }); 4082 4083 // This sleep is needed to wait for the login prompt 4084 thread::sleep(std::time::Duration::new(2, 0)); 4085 4086 kill_child(&mut child); 4087 let output = child.wait_with_output().unwrap(); 4088 handle_child_output(r, &output); 4089 4090 let r = std::panic::catch_unwind(|| { 4091 assert!(String::from_utf8_lossy(&output.stdout).contains(CONSOLE_TEST_STRING)); 4092 }); 4093 4094 handle_child_output(r, &output); 4095 } 4096 4097 #[test] 4098 fn test_serial_file() { 4099 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4100 let guest = Guest::new(Box::new(focal)); 4101 4102 let serial_path = guest.tmp_dir.as_path().join("serial-output"); 4103 #[cfg(target_arch = "x86_64")] 4104 let console_str: &str = "console=ttyS0"; 4105 #[cfg(target_arch = "aarch64")] 4106 let console_str: &str = "console=ttyAMA0"; 4107 4108 let mut child = GuestCommand::new(&guest) 4109 .args(["--cpus", "boot=1"]) 4110 .args(["--memory", "size=512M"]) 4111 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 4112 .args([ 4113 "--cmdline", 4114 DIRECT_KERNEL_BOOT_CMDLINE 4115 .replace("console=hvc0 ", console_str) 4116 .as_str(), 4117 ]) 4118 .default_disks() 4119 .default_net() 4120 .args([ 4121 "--serial", 4122 format!("file={}", serial_path.to_str().unwrap()).as_str(), 4123 ]) 4124 .capture_output() 4125 .spawn() 4126 .unwrap(); 4127 4128 let r = std::panic::catch_unwind(|| { 4129 guest.wait_vm_boot(None).unwrap(); 4130 4131 // Test that there is a ttyS0 4132 assert_eq!( 4133 guest 4134 .ssh_command(GREP_SERIAL_IRQ_CMD) 4135 .unwrap() 4136 .trim() 4137 .parse::<u32>() 4138 .unwrap_or_default(), 4139 1 4140 ); 4141 4142 guest.ssh_command("sudo shutdown -h now").unwrap(); 4143 }); 4144 4145 let _ = child.wait_timeout(std::time::Duration::from_secs(20)); 4146 kill_child(&mut child); 4147 let output = child.wait_with_output().unwrap(); 4148 handle_child_output(r, &output); 4149 4150 let r = std::panic::catch_unwind(|| { 4151 // Check that the cloud-hypervisor binary actually terminated 4152 assert!(output.status.success()); 4153 4154 // Do this check after shutdown of the VM as an easy way to ensure 4155 // all writes are flushed to disk 4156 let mut f = std::fs::File::open(serial_path).unwrap(); 4157 let mut buf = String::new(); 4158 f.read_to_string(&mut buf).unwrap(); 4159 assert!(buf.contains(CONSOLE_TEST_STRING)); 4160 }); 4161 4162 handle_child_output(r, &output); 4163 } 4164 4165 #[test] 4166 fn test_pty_interaction() { 4167 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4168 let guest = Guest::new(Box::new(focal)); 4169 let api_socket = temp_api_path(&guest.tmp_dir); 4170 let serial_option = if cfg!(target_arch = "x86_64") { 4171 " console=ttyS0" 4172 } else { 4173 " console=ttyAMA0" 4174 }; 4175 let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; 4176 4177 let mut child = GuestCommand::new(&guest) 4178 .args(["--cpus", "boot=1"]) 4179 .args(["--memory", "size=512M"]) 4180 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 4181 .args(["--cmdline", &cmdline]) 4182 .default_disks() 4183 .default_net() 4184 .args(["--serial", "null"]) 4185 .args(["--console", "pty"]) 4186 .args(["--api-socket", &api_socket]) 4187 .spawn() 4188 .unwrap(); 4189 4190 let r = std::panic::catch_unwind(|| { 4191 guest.wait_vm_boot(None).unwrap(); 4192 // Get pty fd for console 4193 let console_path = get_pty_path(&api_socket, "console"); 4194 _test_pty_interaction(console_path); 4195 4196 guest.ssh_command("sudo shutdown -h now").unwrap(); 4197 }); 4198 4199 let _ = child.wait_timeout(std::time::Duration::from_secs(20)); 4200 let _ = child.kill(); 4201 let output = child.wait_with_output().unwrap(); 4202 handle_child_output(r, &output); 4203 4204 let r = std::panic::catch_unwind(|| { 4205 // Check that the cloud-hypervisor binary actually terminated 4206 assert!(output.status.success()) 4207 }); 4208 handle_child_output(r, &output); 4209 } 4210 4211 #[test] 4212 fn test_serial_socket_interaction() { 4213 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4214 let guest = Guest::new(Box::new(focal)); 4215 let serial_socket = guest.tmp_dir.as_path().join("serial.socket"); 4216 let serial_socket_pty = guest.tmp_dir.as_path().join("serial.pty"); 4217 let serial_option = if cfg!(target_arch = "x86_64") { 4218 " console=ttyS0" 4219 } else { 4220 " console=ttyAMA0" 4221 }; 4222 let cmdline = DIRECT_KERNEL_BOOT_CMDLINE.to_owned() + serial_option; 4223 4224 let mut child = GuestCommand::new(&guest) 4225 .args(["--cpus", "boot=1"]) 4226 .args(["--memory", "size=512M"]) 4227 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 4228 .args(["--cmdline", &cmdline]) 4229 .default_disks() 4230 .default_net() 4231 .args(["--console", "null"]) 4232 .args([ 4233 "--serial", 4234 format!("socket={}", serial_socket.to_str().unwrap()).as_str(), 4235 ]) 4236 .spawn() 4237 .unwrap(); 4238 4239 let _ = std::panic::catch_unwind(|| { 4240 guest.wait_vm_boot(None).unwrap(); 4241 }); 4242 4243 let mut socat_command = Command::new("socat"); 4244 let socat_args = [ 4245 &format!("pty,link={},raw", serial_socket_pty.display()), 4246 &format!("UNIX-CONNECT:{}", serial_socket.display()), 4247 ]; 4248 socat_command.args(socat_args); 4249 4250 let mut socat_child = socat_command.spawn().unwrap(); 4251 thread::sleep(std::time::Duration::new(1, 0)); 4252 4253 let _ = std::panic::catch_unwind(|| { 4254 _test_pty_interaction(serial_socket_pty); 4255 }); 4256 4257 let _ = socat_child.kill(); 4258 4259 let r = std::panic::catch_unwind(|| { 4260 guest.ssh_command("sudo shutdown -h now").unwrap(); 4261 }); 4262 4263 let _ = child.wait_timeout(std::time::Duration::from_secs(20)); 4264 kill_child(&mut child); 4265 let output = child.wait_with_output().unwrap(); 4266 handle_child_output(r, &output); 4267 4268 let r = std::panic::catch_unwind(|| { 4269 // Check that the cloud-hypervisor binary actually terminated 4270 if !output.status.success() { 4271 panic!( 4272 "Cloud Hypervisor process failed to terminate gracefully: {:?}", 4273 output.status 4274 ); 4275 } 4276 }); 4277 handle_child_output(r, &output); 4278 } 4279 4280 #[test] 4281 fn test_virtio_console() { 4282 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4283 let guest = Guest::new(Box::new(focal)); 4284 4285 let kernel_path = direct_kernel_boot_path(); 4286 4287 let mut child = GuestCommand::new(&guest) 4288 .args(["--cpus", "boot=1"]) 4289 .args(["--memory", "size=512M"]) 4290 .args(["--kernel", kernel_path.to_str().unwrap()]) 4291 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4292 .default_disks() 4293 .default_net() 4294 .args(["--console", "tty"]) 4295 .args(["--serial", "null"]) 4296 .capture_output() 4297 .spawn() 4298 .unwrap(); 4299 4300 let text = String::from("On a branch floating down river a cricket, singing."); 4301 let cmd = format!("echo {text} | sudo tee /dev/hvc0"); 4302 4303 let r = std::panic::catch_unwind(|| { 4304 guest.wait_vm_boot(None).unwrap(); 4305 4306 assert!(guest 4307 .does_device_vendor_pair_match("0x1043", "0x1af4") 4308 .unwrap_or_default()); 4309 4310 guest.ssh_command(&cmd).unwrap(); 4311 }); 4312 4313 kill_child(&mut child); 4314 let output = child.wait_with_output().unwrap(); 4315 handle_child_output(r, &output); 4316 4317 let r = std::panic::catch_unwind(|| { 4318 assert!(String::from_utf8_lossy(&output.stdout).contains(&text)); 4319 }); 4320 4321 handle_child_output(r, &output); 4322 } 4323 4324 #[test] 4325 fn test_console_file() { 4326 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4327 let guest = Guest::new(Box::new(focal)); 4328 4329 let console_path = guest.tmp_dir.as_path().join("console-output"); 4330 let mut child = GuestCommand::new(&guest) 4331 .args(["--cpus", "boot=1"]) 4332 .args(["--memory", "size=512M"]) 4333 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 4334 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4335 .default_disks() 4336 .default_net() 4337 .args([ 4338 "--console", 4339 format!("file={}", console_path.to_str().unwrap()).as_str(), 4340 ]) 4341 .capture_output() 4342 .spawn() 4343 .unwrap(); 4344 4345 guest.wait_vm_boot(None).unwrap(); 4346 4347 guest.ssh_command("sudo shutdown -h now").unwrap(); 4348 4349 let _ = child.wait_timeout(std::time::Duration::from_secs(20)); 4350 kill_child(&mut child); 4351 let output = child.wait_with_output().unwrap(); 4352 4353 let r = std::panic::catch_unwind(|| { 4354 // Check that the cloud-hypervisor binary actually terminated 4355 assert!(output.status.success()); 4356 4357 // Do this check after shutdown of the VM as an easy way to ensure 4358 // all writes are flushed to disk 4359 let mut f = std::fs::File::open(console_path).unwrap(); 4360 let mut buf = String::new(); 4361 f.read_to_string(&mut buf).unwrap(); 4362 4363 if !buf.contains(CONSOLE_TEST_STRING) { 4364 eprintln!( 4365 "\n\n==== Console file output ====\n\n{buf}\n\n==== End console file output ====" 4366 ); 4367 } 4368 assert!(buf.contains(CONSOLE_TEST_STRING)); 4369 }); 4370 4371 handle_child_output(r, &output); 4372 } 4373 4374 #[test] 4375 #[cfg(target_arch = "x86_64")] 4376 #[cfg(not(feature = "mshv"))] 4377 // The VFIO integration test starts cloud-hypervisor guest with 3 TAP 4378 // backed networking interfaces, bound through a simple bridge on the host. 4379 // So if the nested cloud-hypervisor succeeds in getting a directly 4380 // assigned interface from its cloud-hypervisor host, we should be able to 4381 // ssh into it, and verify that it's running with the right kernel command 4382 // line (We tag the command line from cloud-hypervisor for that purpose). 4383 // The third device is added to validate that hotplug works correctly since 4384 // it is being added to the L2 VM through hotplugging mechanism. 4385 // Also, we pass-through a virtio-blk device to the L2 VM to test the 32-bit 4386 // vfio device support 4387 fn test_vfio() { 4388 setup_vfio_network_interfaces(); 4389 4390 let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); 4391 let guest = Guest::new_from_ip_range(Box::new(jammy), "172.18", 0); 4392 4393 let mut workload_path = dirs::home_dir().unwrap(); 4394 workload_path.push("workloads"); 4395 4396 let kernel_path = direct_kernel_boot_path(); 4397 4398 let mut vfio_path = workload_path.clone(); 4399 vfio_path.push("vfio"); 4400 4401 let mut cloud_init_vfio_base_path = vfio_path.clone(); 4402 cloud_init_vfio_base_path.push("cloudinit.img"); 4403 4404 // We copy our cloudinit into the vfio mount point, for the nested 4405 // cloud-hypervisor guest to use. 4406 rate_limited_copy( 4407 guest.disk_config.disk(DiskType::CloudInit).unwrap(), 4408 &cloud_init_vfio_base_path, 4409 ) 4410 .expect("copying of cloud-init disk failed"); 4411 4412 let mut vfio_disk_path = workload_path.clone(); 4413 vfio_disk_path.push("vfio.img"); 4414 4415 // Create the vfio disk image 4416 let output = Command::new("mkfs.ext4") 4417 .arg("-d") 4418 .arg(vfio_path.to_str().unwrap()) 4419 .arg(vfio_disk_path.to_str().unwrap()) 4420 .arg("2g") 4421 .output() 4422 .unwrap(); 4423 if !output.status.success() { 4424 eprintln!("{}", String::from_utf8_lossy(&output.stderr)); 4425 panic!("mkfs.ext4 command generated an error"); 4426 } 4427 4428 let mut blk_file_path = workload_path; 4429 blk_file_path.push("blk.img"); 4430 4431 let vfio_tap0 = "vfio-tap0"; 4432 let vfio_tap1 = "vfio-tap1"; 4433 let vfio_tap2 = "vfio-tap2"; 4434 let vfio_tap3 = "vfio-tap3"; 4435 4436 let mut child = GuestCommand::new(&guest) 4437 .args(["--cpus", "boot=4"]) 4438 .args(["--memory", "size=2G,hugepages=on,shared=on"]) 4439 .args(["--kernel", kernel_path.to_str().unwrap()]) 4440 .args([ 4441 "--disk", 4442 format!( 4443 "path={}", 4444 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 4445 ) 4446 .as_str(), 4447 format!( 4448 "path={}", 4449 guest.disk_config.disk(DiskType::CloudInit).unwrap() 4450 ) 4451 .as_str(), 4452 format!("path={}", vfio_disk_path.to_str().unwrap()).as_str(), 4453 format!("path={},iommu=on", blk_file_path.to_str().unwrap()).as_str(), 4454 ]) 4455 .args([ 4456 "--cmdline", 4457 format!( 4458 "{DIRECT_KERNEL_BOOT_CMDLINE} kvm-intel.nested=1 vfio_iommu_type1.allow_unsafe_interrupts" 4459 ) 4460 .as_str(), 4461 ]) 4462 .args([ 4463 "--net", 4464 format!("tap={},mac={}", vfio_tap0, guest.network.guest_mac).as_str(), 4465 format!( 4466 "tap={},mac={},iommu=on", 4467 vfio_tap1, guest.network.l2_guest_mac1 4468 ) 4469 .as_str(), 4470 format!( 4471 "tap={},mac={},iommu=on", 4472 vfio_tap2, guest.network.l2_guest_mac2 4473 ) 4474 .as_str(), 4475 format!( 4476 "tap={},mac={},iommu=on", 4477 vfio_tap3, guest.network.l2_guest_mac3 4478 ) 4479 .as_str(), 4480 ]) 4481 .capture_output() 4482 .spawn() 4483 .unwrap(); 4484 4485 thread::sleep(std::time::Duration::new(30, 0)); 4486 4487 let r = std::panic::catch_unwind(|| { 4488 guest.ssh_command_l1("sudo systemctl start vfio").unwrap(); 4489 thread::sleep(std::time::Duration::new(120, 0)); 4490 4491 // We booted our cloud hypervisor L2 guest with a "VFIOTAG" tag 4492 // added to its kernel command line. 4493 // Let's ssh into it and verify that it's there. If it is it means 4494 // we're in the right guest (The L2 one) because the QEMU L1 guest 4495 // does not have this command line tag. 4496 assert!(check_matched_lines_count( 4497 guest.ssh_command_l2_1("cat /proc/cmdline").unwrap().trim(), 4498 vec!["VFIOTAG"], 4499 1 4500 )); 4501 4502 // Let's also verify from the second virtio-net device passed to 4503 // the L2 VM. 4504 assert!(check_matched_lines_count( 4505 guest.ssh_command_l2_2("cat /proc/cmdline").unwrap().trim(), 4506 vec!["VFIOTAG"], 4507 1 4508 )); 4509 4510 // Check the amount of PCI devices appearing in L2 VM. 4511 assert!(check_lines_count( 4512 guest 4513 .ssh_command_l2_1("ls /sys/bus/pci/devices") 4514 .unwrap() 4515 .trim(), 4516 8 4517 )); 4518 4519 // Check both if /dev/vdc exists and if the block size is 16M in L2 VM 4520 assert!(check_matched_lines_count( 4521 guest.ssh_command_l2_1("lsblk").unwrap().trim(), 4522 vec!["vdc", "16M"], 4523 1 4524 )); 4525 4526 // Hotplug an extra virtio-net device through L2 VM. 4527 guest 4528 .ssh_command_l1( 4529 "echo 0000:00:09.0 | sudo tee /sys/bus/pci/devices/0000:00:09.0/driver/unbind", 4530 ) 4531 .unwrap(); 4532 guest 4533 .ssh_command_l1("echo 0000:00:09.0 | sudo tee /sys/bus/pci/drivers/vfio-pci/bind") 4534 .unwrap(); 4535 let vfio_hotplug_output = guest 4536 .ssh_command_l1( 4537 "sudo /mnt/ch-remote \ 4538 --api-socket=/tmp/ch_api.sock \ 4539 add-device path=/sys/bus/pci/devices/0000:00:09.0,id=vfio123", 4540 ) 4541 .unwrap(); 4542 assert!(check_matched_lines_count( 4543 vfio_hotplug_output.trim(), 4544 vec!["{\"id\":\"vfio123\",\"bdf\":\"0000:00:08.0\"}"], 4545 1 4546 )); 4547 4548 thread::sleep(std::time::Duration::new(10, 0)); 4549 4550 // Let's also verify from the third virtio-net device passed to 4551 // the L2 VM. This third device has been hotplugged through the L2 4552 // VM, so this is our way to validate hotplug works for VFIO PCI. 4553 assert!(check_matched_lines_count( 4554 guest.ssh_command_l2_3("cat /proc/cmdline").unwrap().trim(), 4555 vec!["VFIOTAG"], 4556 1 4557 )); 4558 4559 // Check the amount of PCI devices appearing in L2 VM. 4560 // There should be one more device than before, raising the count 4561 // up to 9 PCI devices. 4562 assert!(check_lines_count( 4563 guest 4564 .ssh_command_l2_1("ls /sys/bus/pci/devices") 4565 .unwrap() 4566 .trim(), 4567 9 4568 )); 4569 4570 // Let's now verify that we can correctly remove the virtio-net 4571 // device through the "remove-device" command responsible for 4572 // unplugging VFIO devices. 4573 guest 4574 .ssh_command_l1( 4575 "sudo /mnt/ch-remote \ 4576 --api-socket=/tmp/ch_api.sock \ 4577 remove-device vfio123", 4578 ) 4579 .unwrap(); 4580 thread::sleep(std::time::Duration::new(10, 0)); 4581 4582 // Check the amount of PCI devices appearing in L2 VM is back down 4583 // to 8 devices. 4584 assert!(check_lines_count( 4585 guest 4586 .ssh_command_l2_1("ls /sys/bus/pci/devices") 4587 .unwrap() 4588 .trim(), 4589 8 4590 )); 4591 4592 // Perform memory hotplug in L2 and validate the memory is showing 4593 // up as expected. In order to check, we will use the virtio-net 4594 // device already passed through L2 as a VFIO device, this will 4595 // verify that VFIO devices are functional with memory hotplug. 4596 assert!(guest.get_total_memory_l2().unwrap_or_default() > 480_000); 4597 guest 4598 .ssh_command_l2_1( 4599 "sudo bash -c 'echo online > /sys/devices/system/memory/auto_online_blocks'", 4600 ) 4601 .unwrap(); 4602 guest 4603 .ssh_command_l1( 4604 "sudo /mnt/ch-remote \ 4605 --api-socket=/tmp/ch_api.sock \ 4606 resize --memory=1073741824", 4607 ) 4608 .unwrap(); 4609 assert!(guest.get_total_memory_l2().unwrap_or_default() > 960_000); 4610 }); 4611 4612 kill_child(&mut child); 4613 let output = child.wait_with_output().unwrap(); 4614 4615 cleanup_vfio_network_interfaces(); 4616 4617 handle_child_output(r, &output); 4618 } 4619 4620 #[test] 4621 fn test_direct_kernel_boot_noacpi() { 4622 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4623 let guest = Guest::new(Box::new(focal)); 4624 4625 let kernel_path = direct_kernel_boot_path(); 4626 4627 let mut child = GuestCommand::new(&guest) 4628 .args(["--cpus", "boot=1"]) 4629 .args(["--memory", "size=512M"]) 4630 .args(["--kernel", kernel_path.to_str().unwrap()]) 4631 .args([ 4632 "--cmdline", 4633 format!("{DIRECT_KERNEL_BOOT_CMDLINE} acpi=off").as_str(), 4634 ]) 4635 .default_disks() 4636 .default_net() 4637 .capture_output() 4638 .spawn() 4639 .unwrap(); 4640 4641 let r = std::panic::catch_unwind(|| { 4642 guest.wait_vm_boot(None).unwrap(); 4643 4644 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 4645 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 4646 }); 4647 4648 kill_child(&mut child); 4649 let output = child.wait_with_output().unwrap(); 4650 4651 handle_child_output(r, &output); 4652 } 4653 4654 #[test] 4655 fn test_virtio_vsock() { 4656 _test_virtio_vsock(false) 4657 } 4658 4659 #[test] 4660 fn test_virtio_vsock_hotplug() { 4661 _test_virtio_vsock(true); 4662 } 4663 4664 #[test] 4665 fn test_api_http_shutdown() { 4666 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4667 let guest = Guest::new(Box::new(focal)); 4668 4669 _test_api_shutdown(TargetApi::new_http_api(&guest.tmp_dir), guest) 4670 } 4671 4672 #[test] 4673 fn test_api_http_delete() { 4674 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4675 let guest = Guest::new(Box::new(focal)); 4676 4677 _test_api_delete(TargetApi::new_http_api(&guest.tmp_dir), guest); 4678 } 4679 4680 #[test] 4681 fn test_api_http_pause_resume() { 4682 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4683 let guest = Guest::new(Box::new(focal)); 4684 4685 _test_api_pause_resume(TargetApi::new_http_api(&guest.tmp_dir), guest) 4686 } 4687 4688 #[test] 4689 fn test_api_http_create_boot() { 4690 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4691 let guest = Guest::new(Box::new(focal)); 4692 4693 _test_api_create_boot(TargetApi::new_http_api(&guest.tmp_dir), guest) 4694 } 4695 4696 #[test] 4697 fn test_virtio_iommu() { 4698 _test_virtio_iommu(cfg!(target_arch = "x86_64")) 4699 } 4700 4701 #[test] 4702 // We cannot force the software running in the guest to reprogram the BAR 4703 // with some different addresses, but we have a reliable way of testing it 4704 // with a standard Linux kernel. 4705 // By removing a device from the PCI tree, and then rescanning the tree, 4706 // Linux consistently chooses to reorganize the PCI device BARs to other 4707 // locations in the guest address space. 4708 // This test creates a dedicated PCI network device to be checked as being 4709 // properly probed first, then removing it, and adding it again by doing a 4710 // rescan. 4711 fn test_pci_bar_reprogramming() { 4712 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4713 let guest = Guest::new(Box::new(focal)); 4714 4715 #[cfg(target_arch = "x86_64")] 4716 let kernel_path = direct_kernel_boot_path(); 4717 #[cfg(target_arch = "aarch64")] 4718 let kernel_path = edk2_path(); 4719 4720 let mut child = GuestCommand::new(&guest) 4721 .args(["--cpus", "boot=1"]) 4722 .args(["--memory", "size=512M"]) 4723 .args(["--kernel", kernel_path.to_str().unwrap()]) 4724 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4725 .default_disks() 4726 .args([ 4727 "--net", 4728 guest.default_net_string().as_str(), 4729 "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", 4730 ]) 4731 .capture_output() 4732 .spawn() 4733 .unwrap(); 4734 4735 let r = std::panic::catch_unwind(|| { 4736 guest.wait_vm_boot(None).unwrap(); 4737 4738 // 2 network interfaces + default localhost ==> 3 interfaces 4739 assert_eq!( 4740 guest 4741 .ssh_command("ip -o link | wc -l") 4742 .unwrap() 4743 .trim() 4744 .parse::<u32>() 4745 .unwrap_or_default(), 4746 3 4747 ); 4748 4749 let init_bar_addr = guest 4750 .ssh_command( 4751 "sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource", 4752 ) 4753 .unwrap(); 4754 4755 // Remove the PCI device 4756 guest 4757 .ssh_command("echo 1 | sudo tee /sys/bus/pci/devices/0000:00:05.0/remove") 4758 .unwrap(); 4759 4760 // Only 1 network interface left + default localhost ==> 2 interfaces 4761 assert_eq!( 4762 guest 4763 .ssh_command("ip -o link | wc -l") 4764 .unwrap() 4765 .trim() 4766 .parse::<u32>() 4767 .unwrap_or_default(), 4768 2 4769 ); 4770 4771 // Remove the PCI device 4772 guest 4773 .ssh_command("echo 1 | sudo tee /sys/bus/pci/rescan") 4774 .unwrap(); 4775 4776 // Back to 2 network interface + default localhost ==> 3 interfaces 4777 assert_eq!( 4778 guest 4779 .ssh_command("ip -o link | wc -l") 4780 .unwrap() 4781 .trim() 4782 .parse::<u32>() 4783 .unwrap_or_default(), 4784 3 4785 ); 4786 4787 let new_bar_addr = guest 4788 .ssh_command( 4789 "sudo awk '{print $1; exit}' /sys/bus/pci/devices/0000:00:05.0/resource", 4790 ) 4791 .unwrap(); 4792 4793 // Let's compare the BAR addresses for our virtio-net device. 4794 // They should be different as we expect the BAR reprogramming 4795 // to have happened. 4796 assert_ne!(init_bar_addr, new_bar_addr); 4797 }); 4798 4799 kill_child(&mut child); 4800 let output = child.wait_with_output().unwrap(); 4801 4802 handle_child_output(r, &output); 4803 } 4804 4805 #[test] 4806 fn test_memory_mergeable_off() { 4807 test_memory_mergeable(false) 4808 } 4809 4810 #[test] 4811 #[cfg(target_arch = "x86_64")] 4812 fn test_cpu_hotplug() { 4813 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4814 let guest = Guest::new(Box::new(focal)); 4815 let api_socket = temp_api_path(&guest.tmp_dir); 4816 4817 let kernel_path = direct_kernel_boot_path(); 4818 4819 let mut child = GuestCommand::new(&guest) 4820 .args(["--cpus", "boot=2,max=4"]) 4821 .args(["--memory", "size=512M"]) 4822 .args(["--kernel", kernel_path.to_str().unwrap()]) 4823 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4824 .default_disks() 4825 .default_net() 4826 .args(["--api-socket", &api_socket]) 4827 .capture_output() 4828 .spawn() 4829 .unwrap(); 4830 4831 let r = std::panic::catch_unwind(|| { 4832 guest.wait_vm_boot(None).unwrap(); 4833 4834 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 4835 4836 // Resize the VM 4837 let desired_vcpus = 4; 4838 resize_command(&api_socket, Some(desired_vcpus), None, None, None); 4839 4840 guest 4841 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") 4842 .unwrap(); 4843 guest 4844 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") 4845 .unwrap(); 4846 thread::sleep(std::time::Duration::new(10, 0)); 4847 assert_eq!( 4848 guest.get_cpu_count().unwrap_or_default(), 4849 u32::from(desired_vcpus) 4850 ); 4851 4852 guest.reboot_linux(0, None); 4853 4854 assert_eq!( 4855 guest.get_cpu_count().unwrap_or_default(), 4856 u32::from(desired_vcpus) 4857 ); 4858 4859 // Resize the VM 4860 let desired_vcpus = 2; 4861 resize_command(&api_socket, Some(desired_vcpus), None, None, None); 4862 4863 thread::sleep(std::time::Duration::new(10, 0)); 4864 assert_eq!( 4865 guest.get_cpu_count().unwrap_or_default(), 4866 u32::from(desired_vcpus) 4867 ); 4868 4869 // Resize the VM back up to 4 4870 let desired_vcpus = 4; 4871 resize_command(&api_socket, Some(desired_vcpus), None, None, None); 4872 4873 guest 4874 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") 4875 .unwrap(); 4876 guest 4877 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") 4878 .unwrap(); 4879 thread::sleep(std::time::Duration::new(10, 0)); 4880 assert_eq!( 4881 guest.get_cpu_count().unwrap_or_default(), 4882 u32::from(desired_vcpus) 4883 ); 4884 }); 4885 4886 kill_child(&mut child); 4887 let output = child.wait_with_output().unwrap(); 4888 4889 handle_child_output(r, &output); 4890 } 4891 4892 #[test] 4893 fn test_memory_hotplug() { 4894 #[cfg(target_arch = "aarch64")] 4895 let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); 4896 #[cfg(target_arch = "x86_64")] 4897 let focal_image = FOCAL_IMAGE_NAME.to_string(); 4898 let focal = UbuntuDiskConfig::new(focal_image); 4899 let guest = Guest::new(Box::new(focal)); 4900 let api_socket = temp_api_path(&guest.tmp_dir); 4901 4902 #[cfg(target_arch = "aarch64")] 4903 let kernel_path = edk2_path(); 4904 #[cfg(target_arch = "x86_64")] 4905 let kernel_path = direct_kernel_boot_path(); 4906 4907 let mut child = GuestCommand::new(&guest) 4908 .args(["--cpus", "boot=2,max=4"]) 4909 .args(["--memory", "size=512M,hotplug_size=8192M"]) 4910 .args(["--kernel", kernel_path.to_str().unwrap()]) 4911 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4912 .default_disks() 4913 .default_net() 4914 .args(["--balloon", "size=0"]) 4915 .args(["--api-socket", &api_socket]) 4916 .capture_output() 4917 .spawn() 4918 .unwrap(); 4919 4920 let r = std::panic::catch_unwind(|| { 4921 guest.wait_vm_boot(None).unwrap(); 4922 4923 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 4924 4925 guest.enable_memory_hotplug(); 4926 4927 // Add RAM to the VM 4928 let desired_ram = 1024 << 20; 4929 resize_command(&api_socket, None, Some(desired_ram), None, None); 4930 4931 thread::sleep(std::time::Duration::new(10, 0)); 4932 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 4933 4934 // Use balloon to remove RAM from the VM 4935 let desired_balloon = 512 << 20; 4936 resize_command(&api_socket, None, None, Some(desired_balloon), None); 4937 4938 thread::sleep(std::time::Duration::new(10, 0)); 4939 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 4940 assert!(guest.get_total_memory().unwrap_or_default() < 960_000); 4941 4942 guest.reboot_linux(0, None); 4943 4944 assert!(guest.get_total_memory().unwrap_or_default() < 960_000); 4945 4946 // Use balloon add RAM to the VM 4947 let desired_balloon = 0; 4948 resize_command(&api_socket, None, None, Some(desired_balloon), None); 4949 4950 thread::sleep(std::time::Duration::new(10, 0)); 4951 4952 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 4953 4954 guest.enable_memory_hotplug(); 4955 4956 // Add RAM to the VM 4957 let desired_ram = 2048 << 20; 4958 resize_command(&api_socket, None, Some(desired_ram), None, None); 4959 4960 thread::sleep(std::time::Duration::new(10, 0)); 4961 assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); 4962 4963 // Remove RAM to the VM (only applies after reboot) 4964 let desired_ram = 1024 << 20; 4965 resize_command(&api_socket, None, Some(desired_ram), None, None); 4966 4967 guest.reboot_linux(1, None); 4968 4969 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 4970 assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); 4971 }); 4972 4973 kill_child(&mut child); 4974 let output = child.wait_with_output().unwrap(); 4975 4976 handle_child_output(r, &output); 4977 } 4978 4979 #[test] 4980 #[cfg(not(feature = "mshv"))] 4981 fn test_virtio_mem() { 4982 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 4983 let guest = Guest::new(Box::new(focal)); 4984 let api_socket = temp_api_path(&guest.tmp_dir); 4985 4986 let kernel_path = direct_kernel_boot_path(); 4987 4988 let mut child = GuestCommand::new(&guest) 4989 .args(["--cpus", "boot=2,max=4"]) 4990 .args([ 4991 "--memory", 4992 "size=512M,hotplug_method=virtio-mem,hotplug_size=8192M", 4993 ]) 4994 .args(["--kernel", kernel_path.to_str().unwrap()]) 4995 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 4996 .default_disks() 4997 .default_net() 4998 .args(["--api-socket", &api_socket]) 4999 .capture_output() 5000 .spawn() 5001 .unwrap(); 5002 5003 let r = std::panic::catch_unwind(|| { 5004 guest.wait_vm_boot(None).unwrap(); 5005 5006 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 5007 5008 guest.enable_memory_hotplug(); 5009 5010 // Add RAM to the VM 5011 let desired_ram = 1024 << 20; 5012 resize_command(&api_socket, None, Some(desired_ram), None, None); 5013 5014 thread::sleep(std::time::Duration::new(10, 0)); 5015 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 5016 5017 // Add RAM to the VM 5018 let desired_ram = 2048 << 20; 5019 resize_command(&api_socket, None, Some(desired_ram), None, None); 5020 5021 thread::sleep(std::time::Duration::new(10, 0)); 5022 assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); 5023 5024 // Remove RAM from the VM 5025 let desired_ram = 1024 << 20; 5026 resize_command(&api_socket, None, Some(desired_ram), None, None); 5027 5028 thread::sleep(std::time::Duration::new(10, 0)); 5029 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 5030 assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); 5031 5032 guest.reboot_linux(0, None); 5033 5034 // Check the amount of memory after reboot is 1GiB 5035 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 5036 assert!(guest.get_total_memory().unwrap_or_default() < 1_920_000); 5037 5038 // Check we can still resize to 512MiB 5039 let desired_ram = 512 << 20; 5040 resize_command(&api_socket, None, Some(desired_ram), None, None); 5041 thread::sleep(std::time::Duration::new(10, 0)); 5042 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 5043 assert!(guest.get_total_memory().unwrap_or_default() < 960_000); 5044 }); 5045 5046 kill_child(&mut child); 5047 let output = child.wait_with_output().unwrap(); 5048 5049 handle_child_output(r, &output); 5050 } 5051 5052 #[test] 5053 #[cfg(target_arch = "x86_64")] 5054 #[cfg(not(feature = "mshv"))] 5055 // Test both vCPU and memory resizing together 5056 fn test_resize() { 5057 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5058 let guest = Guest::new(Box::new(focal)); 5059 let api_socket = temp_api_path(&guest.tmp_dir); 5060 5061 let kernel_path = direct_kernel_boot_path(); 5062 5063 let mut child = GuestCommand::new(&guest) 5064 .args(["--cpus", "boot=2,max=4"]) 5065 .args(["--memory", "size=512M,hotplug_size=8192M"]) 5066 .args(["--kernel", kernel_path.to_str().unwrap()]) 5067 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5068 .default_disks() 5069 .default_net() 5070 .args(["--api-socket", &api_socket]) 5071 .capture_output() 5072 .spawn() 5073 .unwrap(); 5074 5075 let r = std::panic::catch_unwind(|| { 5076 guest.wait_vm_boot(None).unwrap(); 5077 5078 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 5079 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 5080 5081 guest.enable_memory_hotplug(); 5082 5083 // Resize the VM 5084 let desired_vcpus = 4; 5085 let desired_ram = 1024 << 20; 5086 resize_command( 5087 &api_socket, 5088 Some(desired_vcpus), 5089 Some(desired_ram), 5090 None, 5091 None, 5092 ); 5093 5094 guest 5095 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu2/online") 5096 .unwrap(); 5097 guest 5098 .ssh_command("echo 1 | sudo tee /sys/bus/cpu/devices/cpu3/online") 5099 .unwrap(); 5100 thread::sleep(std::time::Duration::new(10, 0)); 5101 assert_eq!( 5102 guest.get_cpu_count().unwrap_or_default(), 5103 u32::from(desired_vcpus) 5104 ); 5105 5106 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 5107 }); 5108 5109 kill_child(&mut child); 5110 let output = child.wait_with_output().unwrap(); 5111 5112 handle_child_output(r, &output); 5113 } 5114 5115 #[test] 5116 fn test_memory_overhead() { 5117 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5118 let guest = Guest::new(Box::new(focal)); 5119 5120 let kernel_path = direct_kernel_boot_path(); 5121 5122 let guest_memory_size_kb = 512 * 1024; 5123 5124 let mut child = GuestCommand::new(&guest) 5125 .args(["--cpus", "boot=1"]) 5126 .args(["--memory", format!("size={guest_memory_size_kb}K").as_str()]) 5127 .args(["--kernel", kernel_path.to_str().unwrap()]) 5128 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5129 .default_net() 5130 .default_disks() 5131 .capture_output() 5132 .spawn() 5133 .unwrap(); 5134 5135 guest.wait_vm_boot(None).unwrap(); 5136 5137 let r = std::panic::catch_unwind(|| { 5138 let overhead = get_vmm_overhead(child.id(), guest_memory_size_kb); 5139 eprintln!("Guest memory overhead: {overhead} vs {MAXIMUM_VMM_OVERHEAD_KB}"); 5140 assert!(overhead <= MAXIMUM_VMM_OVERHEAD_KB); 5141 }); 5142 5143 kill_child(&mut child); 5144 let output = child.wait_with_output().unwrap(); 5145 5146 handle_child_output(r, &output); 5147 } 5148 5149 #[test] 5150 #[cfg(target_arch = "x86_64")] 5151 // This test runs a guest with Landlock enabled and hotplugs a new disk. As 5152 // the path for the hotplug disk is not pre-added to Landlock rules, this 5153 // the test will result in a failure. 5154 fn test_landlock() { 5155 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5156 let guest = Guest::new(Box::new(focal)); 5157 5158 #[cfg(target_arch = "x86_64")] 5159 let kernel_path = direct_kernel_boot_path(); 5160 #[cfg(target_arch = "aarch64")] 5161 let kernel_path = edk2_path(); 5162 5163 let api_socket = temp_api_path(&guest.tmp_dir); 5164 5165 let mut child = GuestCommand::new(&guest) 5166 .args(["--api-socket", &api_socket]) 5167 .args(["--cpus", "boot=1"]) 5168 .args(["--memory", "size=512M"]) 5169 .args(["--kernel", kernel_path.to_str().unwrap()]) 5170 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5171 .args(["--landlock"]) 5172 .default_disks() 5173 .default_net() 5174 .capture_output() 5175 .spawn() 5176 .unwrap(); 5177 5178 let r = std::panic::catch_unwind(|| { 5179 guest.wait_vm_boot(None).unwrap(); 5180 5181 // Check /dev/vdc is not there 5182 assert_eq!( 5183 guest 5184 .ssh_command("lsblk | grep -c vdc.*16M || true") 5185 .unwrap() 5186 .trim() 5187 .parse::<u32>() 5188 .unwrap_or(1), 5189 0 5190 ); 5191 5192 // Now let's add the extra disk. 5193 let mut blk_file_path = dirs::home_dir().unwrap(); 5194 blk_file_path.push("workloads"); 5195 blk_file_path.push("blk.img"); 5196 // As the path to the hotplug disk is not pre-added, this remote 5197 // command will fail. 5198 assert!(!remote_command( 5199 &api_socket, 5200 "add-disk", 5201 Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), 5202 )); 5203 }); 5204 5205 let _ = child.kill(); 5206 let output = child.wait_with_output().unwrap(); 5207 5208 handle_child_output(r, &output); 5209 } 5210 5211 fn _test_disk_hotplug(landlock_enabled: bool) { 5212 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5213 let guest = Guest::new(Box::new(focal)); 5214 5215 #[cfg(target_arch = "x86_64")] 5216 let kernel_path = direct_kernel_boot_path(); 5217 #[cfg(target_arch = "aarch64")] 5218 let kernel_path = edk2_path(); 5219 5220 let api_socket = temp_api_path(&guest.tmp_dir); 5221 5222 let mut blk_file_path = dirs::home_dir().unwrap(); 5223 blk_file_path.push("workloads"); 5224 blk_file_path.push("blk.img"); 5225 5226 let mut cmd = GuestCommand::new(&guest); 5227 if landlock_enabled { 5228 cmd.args(["--landlock"]).args([ 5229 "--landlock-rules", 5230 format!("path={:?},access=rw", blk_file_path).as_str(), 5231 ]); 5232 } 5233 5234 cmd.args(["--api-socket", &api_socket]) 5235 .args(["--cpus", "boot=1"]) 5236 .args(["--memory", "size=512M"]) 5237 .args(["--kernel", kernel_path.to_str().unwrap()]) 5238 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5239 .default_disks() 5240 .default_net() 5241 .capture_output(); 5242 5243 let mut child = cmd.spawn().unwrap(); 5244 5245 let r = std::panic::catch_unwind(|| { 5246 guest.wait_vm_boot(None).unwrap(); 5247 5248 // Check /dev/vdc is not there 5249 assert_eq!( 5250 guest 5251 .ssh_command("lsblk | grep -c vdc.*16M || true") 5252 .unwrap() 5253 .trim() 5254 .parse::<u32>() 5255 .unwrap_or(1), 5256 0 5257 ); 5258 5259 // Now let's add the extra disk. 5260 let (cmd_success, cmd_output) = remote_command_w_output( 5261 &api_socket, 5262 "add-disk", 5263 Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), 5264 ); 5265 assert!(cmd_success); 5266 assert!(String::from_utf8_lossy(&cmd_output) 5267 .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); 5268 5269 thread::sleep(std::time::Duration::new(10, 0)); 5270 5271 // Check that /dev/vdc exists and the block size is 16M. 5272 assert_eq!( 5273 guest 5274 .ssh_command("lsblk | grep vdc | grep -c 16M") 5275 .unwrap() 5276 .trim() 5277 .parse::<u32>() 5278 .unwrap_or_default(), 5279 1 5280 ); 5281 // And check the block device can be read. 5282 guest 5283 .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") 5284 .unwrap(); 5285 5286 // Let's remove it the extra disk. 5287 assert!(remote_command(&api_socket, "remove-device", Some("test0"))); 5288 thread::sleep(std::time::Duration::new(5, 0)); 5289 // And check /dev/vdc is not there 5290 assert_eq!( 5291 guest 5292 .ssh_command("lsblk | grep -c vdc.*16M || true") 5293 .unwrap() 5294 .trim() 5295 .parse::<u32>() 5296 .unwrap_or(1), 5297 0 5298 ); 5299 5300 // And add it back to validate unplug did work correctly. 5301 let (cmd_success, cmd_output) = remote_command_w_output( 5302 &api_socket, 5303 "add-disk", 5304 Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), 5305 ); 5306 assert!(cmd_success); 5307 assert!(String::from_utf8_lossy(&cmd_output) 5308 .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); 5309 5310 thread::sleep(std::time::Duration::new(10, 0)); 5311 5312 // Check that /dev/vdc exists and the block size is 16M. 5313 assert_eq!( 5314 guest 5315 .ssh_command("lsblk | grep vdc | grep -c 16M") 5316 .unwrap() 5317 .trim() 5318 .parse::<u32>() 5319 .unwrap_or_default(), 5320 1 5321 ); 5322 // And check the block device can be read. 5323 guest 5324 .ssh_command("sudo dd if=/dev/vdc of=/dev/null bs=1M iflag=direct count=16") 5325 .unwrap(); 5326 5327 // Reboot the VM. 5328 guest.reboot_linux(0, None); 5329 5330 // Check still there after reboot 5331 assert_eq!( 5332 guest 5333 .ssh_command("lsblk | grep vdc | grep -c 16M") 5334 .unwrap() 5335 .trim() 5336 .parse::<u32>() 5337 .unwrap_or_default(), 5338 1 5339 ); 5340 5341 assert!(remote_command(&api_socket, "remove-device", Some("test0"))); 5342 5343 thread::sleep(std::time::Duration::new(20, 0)); 5344 5345 // Check device has gone away 5346 assert_eq!( 5347 guest 5348 .ssh_command("lsblk | grep -c vdc.*16M || true") 5349 .unwrap() 5350 .trim() 5351 .parse::<u32>() 5352 .unwrap_or(1), 5353 0 5354 ); 5355 5356 guest.reboot_linux(1, None); 5357 5358 // Check device still absent 5359 assert_eq!( 5360 guest 5361 .ssh_command("lsblk | grep -c vdc.*16M || true") 5362 .unwrap() 5363 .trim() 5364 .parse::<u32>() 5365 .unwrap_or(1), 5366 0 5367 ); 5368 }); 5369 5370 kill_child(&mut child); 5371 let output = child.wait_with_output().unwrap(); 5372 5373 handle_child_output(r, &output); 5374 } 5375 5376 #[test] 5377 fn test_disk_hotplug() { 5378 _test_disk_hotplug(false) 5379 } 5380 5381 #[test] 5382 #[cfg(target_arch = "x86_64")] 5383 fn test_disk_hotplug_with_landlock() { 5384 _test_disk_hotplug(true) 5385 } 5386 5387 fn create_loop_device(backing_file_path: &str, block_size: u32, num_retries: usize) -> String { 5388 const LOOP_CONFIGURE: u64 = 0x4c0a; 5389 const LOOP_CTL_GET_FREE: u64 = 0x4c82; 5390 const LOOP_CTL_PATH: &str = "/dev/loop-control"; 5391 const LOOP_DEVICE_PREFIX: &str = "/dev/loop"; 5392 5393 #[repr(C)] 5394 struct LoopInfo64 { 5395 lo_device: u64, 5396 lo_inode: u64, 5397 lo_rdevice: u64, 5398 lo_offset: u64, 5399 lo_sizelimit: u64, 5400 lo_number: u32, 5401 lo_encrypt_type: u32, 5402 lo_encrypt_key_size: u32, 5403 lo_flags: u32, 5404 lo_file_name: [u8; 64], 5405 lo_crypt_name: [u8; 64], 5406 lo_encrypt_key: [u8; 32], 5407 lo_init: [u64; 2], 5408 } 5409 5410 impl Default for LoopInfo64 { 5411 fn default() -> Self { 5412 LoopInfo64 { 5413 lo_device: 0, 5414 lo_inode: 0, 5415 lo_rdevice: 0, 5416 lo_offset: 0, 5417 lo_sizelimit: 0, 5418 lo_number: 0, 5419 lo_encrypt_type: 0, 5420 lo_encrypt_key_size: 0, 5421 lo_flags: 0, 5422 lo_file_name: [0; 64], 5423 lo_crypt_name: [0; 64], 5424 lo_encrypt_key: [0; 32], 5425 lo_init: [0; 2], 5426 } 5427 } 5428 } 5429 5430 #[derive(Default)] 5431 #[repr(C)] 5432 struct LoopConfig { 5433 fd: u32, 5434 block_size: u32, 5435 info: LoopInfo64, 5436 _reserved: [u64; 8], 5437 } 5438 5439 // Open loop-control device 5440 let loop_ctl_file = OpenOptions::new() 5441 .read(true) 5442 .write(true) 5443 .open(LOOP_CTL_PATH) 5444 .unwrap(); 5445 5446 // Request a free loop device 5447 let loop_device_number = 5448 unsafe { libc::ioctl(loop_ctl_file.as_raw_fd(), LOOP_CTL_GET_FREE as _) }; 5449 5450 if loop_device_number < 0 { 5451 panic!("Couldn't find a free loop device"); 5452 } 5453 5454 // Create loop device path 5455 let loop_device_path = format!("{LOOP_DEVICE_PREFIX}{loop_device_number}"); 5456 5457 // Open loop device 5458 let loop_device_file = OpenOptions::new() 5459 .read(true) 5460 .write(true) 5461 .open(&loop_device_path) 5462 .unwrap(); 5463 5464 // Open backing file 5465 let backing_file = OpenOptions::new() 5466 .read(true) 5467 .write(true) 5468 .open(backing_file_path) 5469 .unwrap(); 5470 5471 let loop_config = LoopConfig { 5472 fd: backing_file.as_raw_fd() as u32, 5473 block_size, 5474 ..Default::default() 5475 }; 5476 5477 for i in 0..num_retries { 5478 let ret = unsafe { 5479 libc::ioctl( 5480 loop_device_file.as_raw_fd(), 5481 LOOP_CONFIGURE as _, 5482 &loop_config, 5483 ) 5484 }; 5485 if ret != 0 { 5486 if i < num_retries - 1 { 5487 println!( 5488 "Iteration {}: Failed to configure the loop device {}: {}", 5489 i, 5490 loop_device_path, 5491 std::io::Error::last_os_error() 5492 ); 5493 } else { 5494 panic!( 5495 "Failed {} times trying to configure the loop device {}: {}", 5496 num_retries, 5497 loop_device_path, 5498 std::io::Error::last_os_error() 5499 ); 5500 } 5501 } else { 5502 break; 5503 } 5504 5505 // Wait for a bit before retrying 5506 thread::sleep(std::time::Duration::new(5, 0)); 5507 } 5508 5509 loop_device_path 5510 } 5511 5512 #[test] 5513 fn test_virtio_block_topology() { 5514 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5515 let guest = Guest::new(Box::new(focal)); 5516 5517 let kernel_path = direct_kernel_boot_path(); 5518 let test_disk_path = guest.tmp_dir.as_path().join("test.img"); 5519 5520 let output = exec_host_command_output( 5521 format!( 5522 "qemu-img create -f raw {} 16M", 5523 test_disk_path.to_str().unwrap() 5524 ) 5525 .as_str(), 5526 ); 5527 if !output.status.success() { 5528 let stdout = String::from_utf8_lossy(&output.stdout); 5529 let stderr = String::from_utf8_lossy(&output.stderr); 5530 panic!("qemu-img command failed\nstdout\n{stdout}\nstderr\n{stderr}"); 5531 } 5532 5533 let loop_dev = create_loop_device(test_disk_path.to_str().unwrap(), 4096, 5); 5534 5535 let mut child = GuestCommand::new(&guest) 5536 .args(["--cpus", "boot=1"]) 5537 .args(["--memory", "size=512M"]) 5538 .args(["--kernel", kernel_path.to_str().unwrap()]) 5539 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5540 .args([ 5541 "--disk", 5542 format!( 5543 "path={}", 5544 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 5545 ) 5546 .as_str(), 5547 format!( 5548 "path={}", 5549 guest.disk_config.disk(DiskType::CloudInit).unwrap() 5550 ) 5551 .as_str(), 5552 format!("path={}", &loop_dev).as_str(), 5553 ]) 5554 .default_net() 5555 .capture_output() 5556 .spawn() 5557 .unwrap(); 5558 5559 let r = std::panic::catch_unwind(|| { 5560 guest.wait_vm_boot(None).unwrap(); 5561 5562 // MIN-IO column 5563 assert_eq!( 5564 guest 5565 .ssh_command("lsblk -t| grep vdc | awk '{print $3}'") 5566 .unwrap() 5567 .trim() 5568 .parse::<u32>() 5569 .unwrap_or_default(), 5570 4096 5571 ); 5572 // PHY-SEC column 5573 assert_eq!( 5574 guest 5575 .ssh_command("lsblk -t| grep vdc | awk '{print $5}'") 5576 .unwrap() 5577 .trim() 5578 .parse::<u32>() 5579 .unwrap_or_default(), 5580 4096 5581 ); 5582 // LOG-SEC column 5583 assert_eq!( 5584 guest 5585 .ssh_command("lsblk -t| grep vdc | awk '{print $6}'") 5586 .unwrap() 5587 .trim() 5588 .parse::<u32>() 5589 .unwrap_or_default(), 5590 4096 5591 ); 5592 }); 5593 5594 kill_child(&mut child); 5595 let output = child.wait_with_output().unwrap(); 5596 5597 handle_child_output(r, &output); 5598 5599 Command::new("losetup") 5600 .args(["-d", &loop_dev]) 5601 .output() 5602 .expect("loop device not found"); 5603 } 5604 5605 #[test] 5606 fn test_virtio_balloon_deflate_on_oom() { 5607 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5608 let guest = Guest::new(Box::new(focal)); 5609 5610 let kernel_path = direct_kernel_boot_path(); 5611 5612 let api_socket = temp_api_path(&guest.tmp_dir); 5613 5614 //Let's start a 4G guest with balloon occupied 2G memory 5615 let mut child = GuestCommand::new(&guest) 5616 .args(["--api-socket", &api_socket]) 5617 .args(["--cpus", "boot=1"]) 5618 .args(["--memory", "size=4G"]) 5619 .args(["--kernel", kernel_path.to_str().unwrap()]) 5620 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5621 .args(["--balloon", "size=2G,deflate_on_oom=on"]) 5622 .default_disks() 5623 .default_net() 5624 .capture_output() 5625 .spawn() 5626 .unwrap(); 5627 5628 let r = std::panic::catch_unwind(|| { 5629 guest.wait_vm_boot(None).unwrap(); 5630 5631 // Wait for balloon memory's initialization and check its size. 5632 // The virtio-balloon driver might take a few seconds to report the 5633 // balloon effective size back to the VMM. 5634 thread::sleep(std::time::Duration::new(20, 0)); 5635 5636 let orig_balloon = balloon_size(&api_socket); 5637 println!("The original balloon memory size is {orig_balloon} bytes"); 5638 assert!(orig_balloon == 2147483648); 5639 5640 // Two steps to verify if the 'deflate_on_oom' parameter works. 5641 // 1st: run a command to trigger an OOM in the guest. 5642 guest 5643 .ssh_command("echo f | sudo tee /proc/sysrq-trigger") 5644 .unwrap(); 5645 5646 // Give some time for the OOM to happen in the guest and be reported 5647 // back to the host. 5648 thread::sleep(std::time::Duration::new(20, 0)); 5649 5650 // 2nd: check balloon_mem's value to verify balloon has been automatically deflated 5651 let deflated_balloon = balloon_size(&api_socket); 5652 println!("After deflating, balloon memory size is {deflated_balloon} bytes"); 5653 // Verify the balloon size deflated 5654 assert!(deflated_balloon < 2147483648); 5655 }); 5656 5657 kill_child(&mut child); 5658 let output = child.wait_with_output().unwrap(); 5659 5660 handle_child_output(r, &output); 5661 } 5662 5663 #[test] 5664 #[cfg(not(feature = "mshv"))] 5665 fn test_virtio_balloon_free_page_reporting() { 5666 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5667 let guest = Guest::new(Box::new(focal)); 5668 5669 //Let's start a 4G guest with balloon occupied 2G memory 5670 let mut child = GuestCommand::new(&guest) 5671 .args(["--cpus", "boot=1"]) 5672 .args(["--memory", "size=4G"]) 5673 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 5674 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5675 .args(["--balloon", "size=0,free_page_reporting=on"]) 5676 .default_disks() 5677 .default_net() 5678 .capture_output() 5679 .spawn() 5680 .unwrap(); 5681 5682 let pid = child.id(); 5683 let r = std::panic::catch_unwind(|| { 5684 guest.wait_vm_boot(None).unwrap(); 5685 5686 // Check the initial RSS is less than 1GiB 5687 let rss = process_rss_kib(pid); 5688 println!("RSS {rss} < 1048576"); 5689 assert!(rss < 1048576); 5690 5691 // Spawn a command inside the guest to consume 2GiB of RAM for 60 5692 // seconds 5693 let guest_ip = guest.network.guest_ip.clone(); 5694 thread::spawn(move || { 5695 ssh_command_ip( 5696 "stress --vm 1 --vm-bytes 2G --vm-keep --timeout 60", 5697 &guest_ip, 5698 DEFAULT_SSH_RETRIES, 5699 DEFAULT_SSH_TIMEOUT, 5700 ) 5701 .unwrap(); 5702 }); 5703 5704 // Wait for 50 seconds to make sure the stress command is consuming 5705 // the expected amount of memory. 5706 thread::sleep(std::time::Duration::new(50, 0)); 5707 let rss = process_rss_kib(pid); 5708 println!("RSS {rss} >= 2097152"); 5709 assert!(rss >= 2097152); 5710 5711 // Wait for an extra minute to make sure the stress command has 5712 // completed and that the guest reported the free pages to the VMM 5713 // through the virtio-balloon device. We expect the RSS to be under 5714 // 2GiB. 5715 thread::sleep(std::time::Duration::new(60, 0)); 5716 let rss = process_rss_kib(pid); 5717 println!("RSS {rss} < 2097152"); 5718 assert!(rss < 2097152); 5719 }); 5720 5721 kill_child(&mut child); 5722 let output = child.wait_with_output().unwrap(); 5723 5724 handle_child_output(r, &output); 5725 } 5726 5727 #[test] 5728 fn test_pmem_hotplug() { 5729 _test_pmem_hotplug(None) 5730 } 5731 5732 #[test] 5733 fn test_pmem_multi_segment_hotplug() { 5734 _test_pmem_hotplug(Some(15)) 5735 } 5736 5737 fn _test_pmem_hotplug(pci_segment: Option<u16>) { 5738 #[cfg(target_arch = "aarch64")] 5739 let focal_image = FOCAL_IMAGE_UPDATE_KERNEL_NAME.to_string(); 5740 #[cfg(target_arch = "x86_64")] 5741 let focal_image = FOCAL_IMAGE_NAME.to_string(); 5742 let focal = UbuntuDiskConfig::new(focal_image); 5743 let guest = Guest::new(Box::new(focal)); 5744 5745 #[cfg(target_arch = "x86_64")] 5746 let kernel_path = direct_kernel_boot_path(); 5747 #[cfg(target_arch = "aarch64")] 5748 let kernel_path = edk2_path(); 5749 5750 let api_socket = temp_api_path(&guest.tmp_dir); 5751 5752 let mut cmd = GuestCommand::new(&guest); 5753 5754 cmd.args(["--api-socket", &api_socket]) 5755 .args(["--cpus", "boot=1"]) 5756 .args(["--memory", "size=512M"]) 5757 .args(["--kernel", kernel_path.to_str().unwrap()]) 5758 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5759 .default_disks() 5760 .default_net() 5761 .capture_output(); 5762 5763 if pci_segment.is_some() { 5764 cmd.args([ 5765 "--platform", 5766 &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), 5767 ]); 5768 } 5769 5770 let mut child = cmd.spawn().unwrap(); 5771 5772 let r = std::panic::catch_unwind(|| { 5773 guest.wait_vm_boot(None).unwrap(); 5774 5775 // Check /dev/pmem0 is not there 5776 assert_eq!( 5777 guest 5778 .ssh_command("lsblk | grep -c pmem0 || true") 5779 .unwrap() 5780 .trim() 5781 .parse::<u32>() 5782 .unwrap_or(1), 5783 0 5784 ); 5785 5786 let pmem_temp_file = TempFile::new().unwrap(); 5787 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 5788 let (cmd_success, cmd_output) = remote_command_w_output( 5789 &api_socket, 5790 "add-pmem", 5791 Some(&format!( 5792 "file={},id=test0{}", 5793 pmem_temp_file.as_path().to_str().unwrap(), 5794 if let Some(pci_segment) = pci_segment { 5795 format!(",pci_segment={pci_segment}") 5796 } else { 5797 "".to_owned() 5798 } 5799 )), 5800 ); 5801 assert!(cmd_success); 5802 if let Some(pci_segment) = pci_segment { 5803 assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( 5804 "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" 5805 ))); 5806 } else { 5807 assert!(String::from_utf8_lossy(&cmd_output) 5808 .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:06.0\"}")); 5809 } 5810 5811 // Check that /dev/pmem0 exists and the block size is 128M 5812 assert_eq!( 5813 guest 5814 .ssh_command("lsblk | grep pmem0 | grep -c 128M") 5815 .unwrap() 5816 .trim() 5817 .parse::<u32>() 5818 .unwrap_or_default(), 5819 1 5820 ); 5821 5822 guest.reboot_linux(0, None); 5823 5824 // Check still there after reboot 5825 assert_eq!( 5826 guest 5827 .ssh_command("lsblk | grep pmem0 | grep -c 128M") 5828 .unwrap() 5829 .trim() 5830 .parse::<u32>() 5831 .unwrap_or_default(), 5832 1 5833 ); 5834 5835 assert!(remote_command(&api_socket, "remove-device", Some("test0"))); 5836 5837 thread::sleep(std::time::Duration::new(20, 0)); 5838 5839 // Check device has gone away 5840 assert_eq!( 5841 guest 5842 .ssh_command("lsblk | grep -c pmem0.*128M || true") 5843 .unwrap() 5844 .trim() 5845 .parse::<u32>() 5846 .unwrap_or(1), 5847 0 5848 ); 5849 5850 guest.reboot_linux(1, None); 5851 5852 // Check still absent after reboot 5853 assert_eq!( 5854 guest 5855 .ssh_command("lsblk | grep -c pmem0.*128M || true") 5856 .unwrap() 5857 .trim() 5858 .parse::<u32>() 5859 .unwrap_or(1), 5860 0 5861 ); 5862 }); 5863 5864 kill_child(&mut child); 5865 let output = child.wait_with_output().unwrap(); 5866 5867 handle_child_output(r, &output); 5868 } 5869 5870 #[test] 5871 fn test_net_hotplug() { 5872 _test_net_hotplug(None) 5873 } 5874 5875 #[test] 5876 fn test_net_multi_segment_hotplug() { 5877 _test_net_hotplug(Some(15)) 5878 } 5879 5880 fn _test_net_hotplug(pci_segment: Option<u16>) { 5881 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 5882 let guest = Guest::new(Box::new(focal)); 5883 5884 #[cfg(target_arch = "x86_64")] 5885 let kernel_path = direct_kernel_boot_path(); 5886 #[cfg(target_arch = "aarch64")] 5887 let kernel_path = edk2_path(); 5888 5889 let api_socket = temp_api_path(&guest.tmp_dir); 5890 5891 // Boot without network 5892 let mut cmd = GuestCommand::new(&guest); 5893 5894 cmd.args(["--api-socket", &api_socket]) 5895 .args(["--cpus", "boot=1"]) 5896 .args(["--memory", "size=512M"]) 5897 .args(["--kernel", kernel_path.to_str().unwrap()]) 5898 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 5899 .default_disks() 5900 .capture_output(); 5901 5902 if pci_segment.is_some() { 5903 cmd.args([ 5904 "--platform", 5905 &format!("num_pci_segments={MAX_NUM_PCI_SEGMENTS}"), 5906 ]); 5907 } 5908 5909 let mut child = cmd.spawn().unwrap(); 5910 5911 thread::sleep(std::time::Duration::new(20, 0)); 5912 5913 let r = std::panic::catch_unwind(|| { 5914 // Add network 5915 let (cmd_success, cmd_output) = remote_command_w_output( 5916 &api_socket, 5917 "add-net", 5918 Some( 5919 format!( 5920 "{}{},id=test0", 5921 guest.default_net_string(), 5922 if let Some(pci_segment) = pci_segment { 5923 format!(",pci_segment={pci_segment}") 5924 } else { 5925 "".to_owned() 5926 } 5927 ) 5928 .as_str(), 5929 ), 5930 ); 5931 assert!(cmd_success); 5932 5933 if let Some(pci_segment) = pci_segment { 5934 assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( 5935 "{{\"id\":\"test0\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" 5936 ))); 5937 } else { 5938 assert!(String::from_utf8_lossy(&cmd_output) 5939 .contains("{\"id\":\"test0\",\"bdf\":\"0000:00:05.0\"}")); 5940 } 5941 5942 thread::sleep(std::time::Duration::new(5, 0)); 5943 5944 // 1 network interfaces + default localhost ==> 2 interfaces 5945 assert_eq!( 5946 guest 5947 .ssh_command("ip -o link | wc -l") 5948 .unwrap() 5949 .trim() 5950 .parse::<u32>() 5951 .unwrap_or_default(), 5952 2 5953 ); 5954 5955 // Remove network 5956 assert!(remote_command(&api_socket, "remove-device", Some("test0"),)); 5957 thread::sleep(std::time::Duration::new(5, 0)); 5958 5959 let (cmd_success, cmd_output) = remote_command_w_output( 5960 &api_socket, 5961 "add-net", 5962 Some( 5963 format!( 5964 "{}{},id=test1", 5965 guest.default_net_string(), 5966 if let Some(pci_segment) = pci_segment { 5967 format!(",pci_segment={pci_segment}") 5968 } else { 5969 "".to_owned() 5970 } 5971 ) 5972 .as_str(), 5973 ), 5974 ); 5975 assert!(cmd_success); 5976 5977 if let Some(pci_segment) = pci_segment { 5978 assert!(String::from_utf8_lossy(&cmd_output).contains(&format!( 5979 "{{\"id\":\"test1\",\"bdf\":\"{pci_segment:04x}:00:01.0\"}}" 5980 ))); 5981 } else { 5982 assert!(String::from_utf8_lossy(&cmd_output) 5983 .contains("{\"id\":\"test1\",\"bdf\":\"0000:00:05.0\"}")); 5984 } 5985 5986 thread::sleep(std::time::Duration::new(5, 0)); 5987 5988 // 1 network interfaces + default localhost ==> 2 interfaces 5989 assert_eq!( 5990 guest 5991 .ssh_command("ip -o link | wc -l") 5992 .unwrap() 5993 .trim() 5994 .parse::<u32>() 5995 .unwrap_or_default(), 5996 2 5997 ); 5998 5999 guest.reboot_linux(0, None); 6000 6001 // Check still there after reboot 6002 // 1 network interfaces + default localhost ==> 2 interfaces 6003 assert_eq!( 6004 guest 6005 .ssh_command("ip -o link | wc -l") 6006 .unwrap() 6007 .trim() 6008 .parse::<u32>() 6009 .unwrap_or_default(), 6010 2 6011 ); 6012 }); 6013 6014 kill_child(&mut child); 6015 let output = child.wait_with_output().unwrap(); 6016 6017 handle_child_output(r, &output); 6018 } 6019 6020 #[test] 6021 fn test_initramfs() { 6022 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6023 let guest = Guest::new(Box::new(focal)); 6024 let mut workload_path = dirs::home_dir().unwrap(); 6025 workload_path.push("workloads"); 6026 6027 #[cfg(target_arch = "x86_64")] 6028 let mut kernels = vec![direct_kernel_boot_path()]; 6029 #[cfg(target_arch = "aarch64")] 6030 let kernels = [direct_kernel_boot_path()]; 6031 6032 #[cfg(target_arch = "x86_64")] 6033 { 6034 let mut pvh_kernel_path = workload_path.clone(); 6035 pvh_kernel_path.push("vmlinux"); 6036 kernels.push(pvh_kernel_path); 6037 } 6038 6039 let mut initramfs_path = workload_path; 6040 initramfs_path.push("alpine_initramfs.img"); 6041 6042 let test_string = String::from("axz34i9rylotd8n50wbv6kcj7f2qushme1pg"); 6043 let cmdline = format!("console=hvc0 quiet TEST_STRING={test_string}"); 6044 6045 kernels.iter().for_each(|k_path| { 6046 let mut child = GuestCommand::new(&guest) 6047 .args(["--kernel", k_path.to_str().unwrap()]) 6048 .args(["--initramfs", initramfs_path.to_str().unwrap()]) 6049 .args(["--cmdline", &cmdline]) 6050 .capture_output() 6051 .spawn() 6052 .unwrap(); 6053 6054 thread::sleep(std::time::Duration::new(20, 0)); 6055 6056 kill_child(&mut child); 6057 let output = child.wait_with_output().unwrap(); 6058 6059 let r = std::panic::catch_unwind(|| { 6060 let s = String::from_utf8_lossy(&output.stdout); 6061 6062 assert_ne!(s.lines().position(|line| line == test_string), None); 6063 }); 6064 6065 handle_child_output(r, &output); 6066 }); 6067 } 6068 6069 #[test] 6070 fn test_counters() { 6071 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6072 let guest = Guest::new(Box::new(focal)); 6073 let api_socket = temp_api_path(&guest.tmp_dir); 6074 6075 let mut cmd = GuestCommand::new(&guest); 6076 cmd.args(["--cpus", "boot=1"]) 6077 .args(["--memory", "size=512M"]) 6078 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 6079 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6080 .default_disks() 6081 .args(["--net", guest.default_net_string().as_str()]) 6082 .args(["--api-socket", &api_socket]) 6083 .capture_output(); 6084 6085 let mut child = cmd.spawn().unwrap(); 6086 6087 let r = std::panic::catch_unwind(|| { 6088 guest.wait_vm_boot(None).unwrap(); 6089 6090 let orig_counters = get_counters(&api_socket); 6091 guest 6092 .ssh_command("dd if=/dev/zero of=test count=8 bs=1M") 6093 .unwrap(); 6094 6095 let new_counters = get_counters(&api_socket); 6096 6097 // Check that all the counters have increased 6098 assert!(new_counters > orig_counters); 6099 }); 6100 6101 kill_child(&mut child); 6102 let output = child.wait_with_output().unwrap(); 6103 6104 handle_child_output(r, &output); 6105 } 6106 6107 #[test] 6108 #[cfg(feature = "guest_debug")] 6109 fn test_coredump() { 6110 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6111 let guest = Guest::new(Box::new(focal)); 6112 let api_socket = temp_api_path(&guest.tmp_dir); 6113 6114 let mut cmd = GuestCommand::new(&guest); 6115 cmd.args(["--cpus", "boot=4"]) 6116 .args(["--memory", "size=4G"]) 6117 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 6118 .default_disks() 6119 .args(["--net", guest.default_net_string().as_str()]) 6120 .args(["--api-socket", &api_socket]) 6121 .capture_output(); 6122 6123 let mut child = cmd.spawn().unwrap(); 6124 let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); 6125 6126 let r = std::panic::catch_unwind(|| { 6127 guest.wait_vm_boot(None).unwrap(); 6128 6129 assert!(remote_command(&api_socket, "pause", None)); 6130 6131 assert!(remote_command( 6132 &api_socket, 6133 "coredump", 6134 Some(format!("file://{vmcore_file}").as_str()), 6135 )); 6136 6137 // the num of CORE notes should equals to vcpu 6138 let readelf_core_num_cmd = 6139 format!("readelf --all {vmcore_file} |grep CORE |grep -v Type |wc -l"); 6140 let core_num_in_elf = exec_host_command_output(&readelf_core_num_cmd); 6141 assert_eq!(String::from_utf8_lossy(&core_num_in_elf.stdout).trim(), "4"); 6142 6143 // the num of QEMU notes should equals to vcpu 6144 let readelf_vmm_num_cmd = format!("readelf --all {vmcore_file} |grep QEMU |wc -l"); 6145 let vmm_num_in_elf = exec_host_command_output(&readelf_vmm_num_cmd); 6146 assert_eq!(String::from_utf8_lossy(&vmm_num_in_elf.stdout).trim(), "4"); 6147 }); 6148 6149 kill_child(&mut child); 6150 let output = child.wait_with_output().unwrap(); 6151 6152 handle_child_output(r, &output); 6153 } 6154 6155 #[test] 6156 #[cfg(feature = "guest_debug")] 6157 fn test_coredump_no_pause() { 6158 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6159 let guest = Guest::new(Box::new(focal)); 6160 let api_socket = temp_api_path(&guest.tmp_dir); 6161 6162 let mut cmd = GuestCommand::new(&guest); 6163 cmd.args(["--cpus", "boot=4"]) 6164 .args(["--memory", "size=4G"]) 6165 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 6166 .default_disks() 6167 .args(["--net", guest.default_net_string().as_str()]) 6168 .args(["--api-socket", &api_socket]) 6169 .capture_output(); 6170 6171 let mut child = cmd.spawn().unwrap(); 6172 let vmcore_file = temp_vmcore_file_path(&guest.tmp_dir); 6173 6174 let r = std::panic::catch_unwind(|| { 6175 guest.wait_vm_boot(None).unwrap(); 6176 6177 assert!(remote_command( 6178 &api_socket, 6179 "coredump", 6180 Some(format!("file://{vmcore_file}").as_str()), 6181 )); 6182 6183 assert_eq!(vm_state(&api_socket), "Running"); 6184 }); 6185 6186 kill_child(&mut child); 6187 let output = child.wait_with_output().unwrap(); 6188 6189 handle_child_output(r, &output); 6190 } 6191 6192 #[test] 6193 fn test_watchdog() { 6194 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6195 let guest = Guest::new(Box::new(focal)); 6196 let api_socket = temp_api_path(&guest.tmp_dir); 6197 6198 let kernel_path = direct_kernel_boot_path(); 6199 let event_path = temp_event_monitor_path(&guest.tmp_dir); 6200 6201 let mut cmd = GuestCommand::new(&guest); 6202 cmd.args(["--cpus", "boot=1"]) 6203 .args(["--memory", "size=512M"]) 6204 .args(["--kernel", kernel_path.to_str().unwrap()]) 6205 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6206 .default_disks() 6207 .args(["--net", guest.default_net_string().as_str()]) 6208 .args(["--watchdog"]) 6209 .args(["--api-socket", &api_socket]) 6210 .args(["--event-monitor", format!("path={event_path}").as_str()]) 6211 .capture_output(); 6212 6213 let mut child = cmd.spawn().unwrap(); 6214 6215 let r = std::panic::catch_unwind(|| { 6216 guest.wait_vm_boot(None).unwrap(); 6217 6218 let mut expected_reboot_count = 1; 6219 6220 // Enable the watchdog with a 15s timeout 6221 enable_guest_watchdog(&guest, 15); 6222 6223 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 6224 assert_eq!( 6225 guest 6226 .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") 6227 .unwrap() 6228 .trim() 6229 .parse::<u32>() 6230 .unwrap_or_default(), 6231 1 6232 ); 6233 6234 // Allow some normal time to elapse to check we don't get spurious reboots 6235 thread::sleep(std::time::Duration::new(40, 0)); 6236 // Check no reboot 6237 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 6238 6239 // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. 6240 guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); 6241 // Allow some time for the watchdog to trigger (max 30s) and reboot to happen 6242 guest.wait_vm_boot(Some(50)).unwrap(); 6243 // Check a reboot is triggered by the watchdog 6244 expected_reboot_count += 1; 6245 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 6246 6247 #[cfg(target_arch = "x86_64")] 6248 { 6249 // Now pause the VM and remain offline for 30s 6250 assert!(remote_command(&api_socket, "pause", None)); 6251 let latest_events = [ 6252 &MetaEvent { 6253 event: "pausing".to_string(), 6254 device_id: None, 6255 }, 6256 &MetaEvent { 6257 event: "paused".to_string(), 6258 device_id: None, 6259 }, 6260 ]; 6261 assert!(check_latest_events_exact(&latest_events, &event_path)); 6262 assert!(remote_command(&api_socket, "resume", None)); 6263 6264 // Check no reboot 6265 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 6266 } 6267 }); 6268 6269 kill_child(&mut child); 6270 let output = child.wait_with_output().unwrap(); 6271 6272 handle_child_output(r, &output); 6273 } 6274 6275 #[test] 6276 fn test_pvpanic() { 6277 let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); 6278 let guest = Guest::new(Box::new(jammy)); 6279 let api_socket = temp_api_path(&guest.tmp_dir); 6280 let event_path = temp_event_monitor_path(&guest.tmp_dir); 6281 6282 let kernel_path = direct_kernel_boot_path(); 6283 6284 let mut cmd = GuestCommand::new(&guest); 6285 cmd.args(["--cpus", "boot=1"]) 6286 .args(["--memory", "size=512M"]) 6287 .args(["--kernel", kernel_path.to_str().unwrap()]) 6288 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6289 .default_disks() 6290 .args(["--net", guest.default_net_string().as_str()]) 6291 .args(["--pvpanic"]) 6292 .args(["--api-socket", &api_socket]) 6293 .args(["--event-monitor", format!("path={event_path}").as_str()]) 6294 .capture_output(); 6295 6296 let mut child = cmd.spawn().unwrap(); 6297 6298 let r = std::panic::catch_unwind(|| { 6299 guest.wait_vm_boot(None).unwrap(); 6300 6301 // Trigger guest a panic 6302 make_guest_panic(&guest); 6303 6304 // Wait a while for guest 6305 thread::sleep(std::time::Duration::new(10, 0)); 6306 6307 let expected_sequential_events = [&MetaEvent { 6308 event: "panic".to_string(), 6309 device_id: None, 6310 }]; 6311 assert!(check_latest_events_exact( 6312 &expected_sequential_events, 6313 &event_path 6314 )); 6315 }); 6316 6317 kill_child(&mut child); 6318 let output = child.wait_with_output().unwrap(); 6319 6320 handle_child_output(r, &output); 6321 } 6322 6323 #[test] 6324 fn test_tap_from_fd() { 6325 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6326 let guest = Guest::new(Box::new(focal)); 6327 let kernel_path = direct_kernel_boot_path(); 6328 6329 // Create a TAP interface with multi-queue enabled 6330 let num_queue_pairs: usize = 2; 6331 6332 use std::str::FromStr; 6333 let taps = net_util::open_tap( 6334 Some("chtap0"), 6335 Some(std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap()), 6336 None, 6337 &mut None, 6338 None, 6339 num_queue_pairs, 6340 Some(libc::O_RDWR | libc::O_NONBLOCK), 6341 ) 6342 .unwrap(); 6343 6344 let mut child = GuestCommand::new(&guest) 6345 .args(["--cpus", &format!("boot={num_queue_pairs}")]) 6346 .args(["--memory", "size=512M"]) 6347 .args(["--kernel", kernel_path.to_str().unwrap()]) 6348 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6349 .default_disks() 6350 .args([ 6351 "--net", 6352 &format!( 6353 "fd=[{},{}],mac={},num_queues={}", 6354 taps[0].as_raw_fd(), 6355 taps[1].as_raw_fd(), 6356 guest.network.guest_mac, 6357 num_queue_pairs * 2 6358 ), 6359 ]) 6360 .capture_output() 6361 .spawn() 6362 .unwrap(); 6363 6364 let r = std::panic::catch_unwind(|| { 6365 guest.wait_vm_boot(None).unwrap(); 6366 6367 assert_eq!( 6368 guest 6369 .ssh_command("ip -o link | wc -l") 6370 .unwrap() 6371 .trim() 6372 .parse::<u32>() 6373 .unwrap_or_default(), 6374 2 6375 ); 6376 6377 guest.reboot_linux(0, None); 6378 6379 assert_eq!( 6380 guest 6381 .ssh_command("ip -o link | wc -l") 6382 .unwrap() 6383 .trim() 6384 .parse::<u32>() 6385 .unwrap_or_default(), 6386 2 6387 ); 6388 }); 6389 6390 kill_child(&mut child); 6391 let output = child.wait_with_output().unwrap(); 6392 6393 handle_child_output(r, &output); 6394 } 6395 6396 // By design, a guest VM won't be able to connect to the host 6397 // machine when using a macvtap network interface (while it can 6398 // communicate externally). As a workaround, this integration 6399 // test creates two macvtap interfaces in 'bridge' mode on the 6400 // same physical net interface, one for the guest and one for 6401 // the host. With additional setup on the IP address and the 6402 // routing table, it enables the communications between the 6403 // guest VM and the host machine. 6404 // Details: https://wiki.libvirt.org/page/TroubleshootMacvtapHostFail 6405 fn _test_macvtap(hotplug: bool, guest_macvtap_name: &str, host_macvtap_name: &str) { 6406 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6407 let guest = Guest::new(Box::new(focal)); 6408 let api_socket = temp_api_path(&guest.tmp_dir); 6409 6410 #[cfg(target_arch = "x86_64")] 6411 let kernel_path = direct_kernel_boot_path(); 6412 #[cfg(target_arch = "aarch64")] 6413 let kernel_path = edk2_path(); 6414 6415 let phy_net = "eth0"; 6416 6417 // Create a macvtap interface for the guest VM to use 6418 assert!(exec_host_command_status(&format!( 6419 "sudo ip link add link {phy_net} name {guest_macvtap_name} type macvtap mod bridge" 6420 )) 6421 .success()); 6422 assert!(exec_host_command_status(&format!( 6423 "sudo ip link set {} address {} up", 6424 guest_macvtap_name, guest.network.guest_mac 6425 )) 6426 .success()); 6427 assert!( 6428 exec_host_command_status(&format!("sudo ip link show {guest_macvtap_name}")).success() 6429 ); 6430 6431 let tap_index = 6432 fs::read_to_string(format!("/sys/class/net/{guest_macvtap_name}/ifindex")).unwrap(); 6433 let tap_device = format!("/dev/tap{}", tap_index.trim()); 6434 6435 assert!(exec_host_command_status(&format!("sudo chown $UID.$UID {tap_device}")).success()); 6436 6437 let cstr_tap_device = std::ffi::CString::new(tap_device).unwrap(); 6438 let tap_fd1 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; 6439 assert!(tap_fd1 > 0); 6440 let tap_fd2 = unsafe { libc::open(cstr_tap_device.as_ptr(), libc::O_RDWR) }; 6441 assert!(tap_fd2 > 0); 6442 6443 // Create a macvtap on the same physical net interface for 6444 // the host machine to use 6445 assert!(exec_host_command_status(&format!( 6446 "sudo ip link add link {phy_net} name {host_macvtap_name} type macvtap mod bridge" 6447 )) 6448 .success()); 6449 // Use default mask "255.255.255.0" 6450 assert!(exec_host_command_status(&format!( 6451 "sudo ip address add {}/24 dev {}", 6452 guest.network.host_ip, host_macvtap_name 6453 )) 6454 .success()); 6455 assert!( 6456 exec_host_command_status(&format!("sudo ip link set dev {host_macvtap_name} up")) 6457 .success() 6458 ); 6459 6460 let mut guest_command = GuestCommand::new(&guest); 6461 guest_command 6462 .args(["--cpus", "boot=2"]) 6463 .args(["--memory", "size=512M"]) 6464 .args(["--kernel", kernel_path.to_str().unwrap()]) 6465 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6466 .default_disks() 6467 .args(["--api-socket", &api_socket]); 6468 6469 let net_params = format!( 6470 "fd=[{},{}],mac={},num_queues=4", 6471 tap_fd1, tap_fd2, guest.network.guest_mac 6472 ); 6473 6474 if !hotplug { 6475 guest_command.args(["--net", &net_params]); 6476 } 6477 6478 let mut child = guest_command.capture_output().spawn().unwrap(); 6479 6480 if hotplug { 6481 // Give some time to the VMM process to listen to the API 6482 // socket. This is the only requirement to avoid the following 6483 // call to ch-remote from failing. 6484 thread::sleep(std::time::Duration::new(10, 0)); 6485 // Hotplug the virtio-net device 6486 let (cmd_success, cmd_output) = 6487 remote_command_w_output(&api_socket, "add-net", Some(&net_params)); 6488 assert!(cmd_success); 6489 #[cfg(target_arch = "x86_64")] 6490 assert!(String::from_utf8_lossy(&cmd_output) 6491 .contains("{\"id\":\"_net2\",\"bdf\":\"0000:00:05.0\"}")); 6492 #[cfg(target_arch = "aarch64")] 6493 assert!(String::from_utf8_lossy(&cmd_output) 6494 .contains("{\"id\":\"_net0\",\"bdf\":\"0000:00:05.0\"}")); 6495 } 6496 6497 // The functional connectivity provided by the virtio-net device 6498 // gets tested through wait_vm_boot() as it expects to receive a 6499 // HTTP request, and through the SSH command as well. 6500 let r = std::panic::catch_unwind(|| { 6501 guest.wait_vm_boot(None).unwrap(); 6502 6503 assert_eq!( 6504 guest 6505 .ssh_command("ip -o link | wc -l") 6506 .unwrap() 6507 .trim() 6508 .parse::<u32>() 6509 .unwrap_or_default(), 6510 2 6511 ); 6512 6513 guest.reboot_linux(0, None); 6514 6515 assert_eq!( 6516 guest 6517 .ssh_command("ip -o link | wc -l") 6518 .unwrap() 6519 .trim() 6520 .parse::<u32>() 6521 .unwrap_or_default(), 6522 2 6523 ); 6524 }); 6525 6526 kill_child(&mut child); 6527 6528 exec_host_command_status(&format!("sudo ip link del {guest_macvtap_name}")); 6529 exec_host_command_status(&format!("sudo ip link del {host_macvtap_name}")); 6530 6531 let output = child.wait_with_output().unwrap(); 6532 6533 handle_child_output(r, &output); 6534 } 6535 6536 #[test] 6537 #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] 6538 fn test_macvtap() { 6539 _test_macvtap(false, "guestmacvtap0", "hostmacvtap0") 6540 } 6541 6542 #[test] 6543 #[cfg_attr(target_arch = "aarch64", ignore = "See #5443")] 6544 fn test_macvtap_hotplug() { 6545 _test_macvtap(true, "guestmacvtap1", "hostmacvtap1") 6546 } 6547 6548 #[test] 6549 #[cfg(not(feature = "mshv"))] 6550 fn test_ovs_dpdk() { 6551 let focal1 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6552 let guest1 = Guest::new(Box::new(focal1)); 6553 6554 let focal2 = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6555 let guest2 = Guest::new(Box::new(focal2)); 6556 let api_socket_source = format!("{}.1", temp_api_path(&guest2.tmp_dir)); 6557 6558 let (mut child1, mut child2) = 6559 setup_ovs_dpdk_guests(&guest1, &guest2, &api_socket_source, false); 6560 6561 // Create the snapshot directory 6562 let snapshot_dir = temp_snapshot_dir_path(&guest2.tmp_dir); 6563 6564 let r = std::panic::catch_unwind(|| { 6565 // Remove one of the two ports from the OVS bridge 6566 assert!(exec_host_command_status("ovs-vsctl del-port vhost-user1").success()); 6567 6568 // Spawn a new netcat listener in the first VM 6569 let guest_ip = guest1.network.guest_ip.clone(); 6570 thread::spawn(move || { 6571 ssh_command_ip( 6572 "nc -l 12345", 6573 &guest_ip, 6574 DEFAULT_SSH_RETRIES, 6575 DEFAULT_SSH_TIMEOUT, 6576 ) 6577 .unwrap(); 6578 }); 6579 6580 // Wait for the server to be listening 6581 thread::sleep(std::time::Duration::new(5, 0)); 6582 6583 // Check the connection fails this time 6584 guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap_err(); 6585 6586 // Add the OVS port back 6587 assert!(exec_host_command_status("ovs-vsctl add-port ovsbr0 vhost-user1 -- set Interface vhost-user1 type=dpdkvhostuserclient options:vhost-server-path=/tmp/dpdkvhostclient1").success()); 6588 6589 // And finally check the connection is functional again 6590 guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); 6591 6592 // Pause the VM 6593 assert!(remote_command(&api_socket_source, "pause", None)); 6594 6595 // Take a snapshot from the VM 6596 assert!(remote_command( 6597 &api_socket_source, 6598 "snapshot", 6599 Some(format!("file://{snapshot_dir}").as_str()), 6600 )); 6601 6602 // Wait to make sure the snapshot is completed 6603 thread::sleep(std::time::Duration::new(10, 0)); 6604 }); 6605 6606 // Shutdown the source VM 6607 kill_child(&mut child2); 6608 let output = child2.wait_with_output().unwrap(); 6609 handle_child_output(r, &output); 6610 6611 // Remove the vhost-user socket file. 6612 Command::new("rm") 6613 .arg("-f") 6614 .arg("/tmp/dpdkvhostclient2") 6615 .output() 6616 .unwrap(); 6617 6618 let api_socket_restored = format!("{}.2", temp_api_path(&guest2.tmp_dir)); 6619 // Restore the VM from the snapshot 6620 let mut child2 = GuestCommand::new(&guest2) 6621 .args(["--api-socket", &api_socket_restored]) 6622 .args([ 6623 "--restore", 6624 format!("source_url=file://{snapshot_dir}").as_str(), 6625 ]) 6626 .capture_output() 6627 .spawn() 6628 .unwrap(); 6629 6630 // Wait for the VM to be restored 6631 thread::sleep(std::time::Duration::new(10, 0)); 6632 6633 let r = std::panic::catch_unwind(|| { 6634 // Resume the VM 6635 assert!(remote_command(&api_socket_restored, "resume", None)); 6636 6637 // Spawn a new netcat listener in the first VM 6638 let guest_ip = guest1.network.guest_ip.clone(); 6639 thread::spawn(move || { 6640 ssh_command_ip( 6641 "nc -l 12345", 6642 &guest_ip, 6643 DEFAULT_SSH_RETRIES, 6644 DEFAULT_SSH_TIMEOUT, 6645 ) 6646 .unwrap(); 6647 }); 6648 6649 // Wait for the server to be listening 6650 thread::sleep(std::time::Duration::new(5, 0)); 6651 6652 // And check the connection is still functional after restore 6653 guest2.ssh_command("nc -vz 172.100.0.1 12345").unwrap(); 6654 }); 6655 6656 kill_child(&mut child1); 6657 kill_child(&mut child2); 6658 6659 let output = child1.wait_with_output().unwrap(); 6660 child2.wait().unwrap(); 6661 6662 cleanup_ovs_dpdk(); 6663 6664 handle_child_output(r, &output); 6665 } 6666 6667 fn setup_spdk_nvme(nvme_dir: &std::path::Path) { 6668 cleanup_spdk_nvme(); 6669 6670 assert!(exec_host_command_status(&format!( 6671 "mkdir -p {}", 6672 nvme_dir.join("nvme-vfio-user").to_str().unwrap() 6673 )) 6674 .success()); 6675 assert!(exec_host_command_status(&format!( 6676 "truncate {} -s 128M", 6677 nvme_dir.join("test-disk.raw").to_str().unwrap() 6678 )) 6679 .success()); 6680 assert!(exec_host_command_status(&format!( 6681 "mkfs.ext4 {}", 6682 nvme_dir.join("test-disk.raw").to_str().unwrap() 6683 )) 6684 .success()); 6685 6686 // Start the SPDK nvmf_tgt daemon to present NVMe device as a VFIO user device 6687 Command::new("/usr/local/bin/spdk-nvme/nvmf_tgt") 6688 .args(["-i", "0", "-m", "0x1"]) 6689 .spawn() 6690 .unwrap(); 6691 thread::sleep(std::time::Duration::new(2, 0)); 6692 6693 assert!(exec_host_command_with_retries( 6694 "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_transport -t VFIOUSER", 6695 3, 6696 std::time::Duration::new(5, 0), 6697 )); 6698 assert!(exec_host_command_status(&format!( 6699 "/usr/local/bin/spdk-nvme/rpc.py bdev_aio_create {} test 512", 6700 nvme_dir.join("test-disk.raw").to_str().unwrap() 6701 )) 6702 .success()); 6703 assert!(exec_host_command_status( 6704 "/usr/local/bin/spdk-nvme/rpc.py nvmf_create_subsystem nqn.2019-07.io.spdk:cnode -a -s test" 6705 ) 6706 .success()); 6707 assert!(exec_host_command_status( 6708 "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_ns nqn.2019-07.io.spdk:cnode test" 6709 ) 6710 .success()); 6711 assert!(exec_host_command_status(&format!( 6712 "/usr/local/bin/spdk-nvme/rpc.py nvmf_subsystem_add_listener nqn.2019-07.io.spdk:cnode -t VFIOUSER -a {} -s 0", 6713 nvme_dir.join("nvme-vfio-user").to_str().unwrap() 6714 )) 6715 .success()); 6716 } 6717 6718 fn cleanup_spdk_nvme() { 6719 exec_host_command_status("pkill -f nvmf_tgt"); 6720 } 6721 6722 #[test] 6723 fn test_vfio_user() { 6724 let jammy_image = JAMMY_IMAGE_NAME.to_string(); 6725 let jammy = UbuntuDiskConfig::new(jammy_image); 6726 let guest = Guest::new(Box::new(jammy)); 6727 6728 let spdk_nvme_dir = guest.tmp_dir.as_path().join("test-vfio-user"); 6729 setup_spdk_nvme(spdk_nvme_dir.as_path()); 6730 6731 let api_socket = temp_api_path(&guest.tmp_dir); 6732 let mut child = GuestCommand::new(&guest) 6733 .args(["--api-socket", &api_socket]) 6734 .args(["--cpus", "boot=1"]) 6735 .args(["--memory", "size=512M,shared=on,hugepages=on"]) 6736 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 6737 .args(["--serial", "tty", "--console", "off"]) 6738 .default_disks() 6739 .default_net() 6740 .capture_output() 6741 .spawn() 6742 .unwrap(); 6743 6744 let r = std::panic::catch_unwind(|| { 6745 guest.wait_vm_boot(None).unwrap(); 6746 6747 // Hotplug the SPDK-NVMe device to the VM 6748 let (cmd_success, cmd_output) = remote_command_w_output( 6749 &api_socket, 6750 "add-user-device", 6751 Some(&format!( 6752 "socket={},id=vfio_user0", 6753 spdk_nvme_dir 6754 .as_path() 6755 .join("nvme-vfio-user/cntrl") 6756 .to_str() 6757 .unwrap(), 6758 )), 6759 ); 6760 assert!(cmd_success); 6761 assert!(String::from_utf8_lossy(&cmd_output) 6762 .contains("{\"id\":\"vfio_user0\",\"bdf\":\"0000:00:05.0\"}")); 6763 6764 thread::sleep(std::time::Duration::new(10, 0)); 6765 6766 // Check both if /dev/nvme exists and if the block size is 128M. 6767 assert_eq!( 6768 guest 6769 .ssh_command("lsblk | grep nvme0n1 | grep -c 128M") 6770 .unwrap() 6771 .trim() 6772 .parse::<u32>() 6773 .unwrap_or_default(), 6774 1 6775 ); 6776 6777 // Check changes persist after reboot 6778 assert_eq!( 6779 guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), 6780 "" 6781 ); 6782 assert_eq!(guest.ssh_command("ls /mnt").unwrap(), "lost+found\n"); 6783 guest 6784 .ssh_command("echo test123 | sudo tee /mnt/test") 6785 .unwrap(); 6786 assert_eq!(guest.ssh_command("sudo umount /mnt").unwrap(), ""); 6787 assert_eq!(guest.ssh_command("ls /mnt").unwrap(), ""); 6788 6789 guest.reboot_linux(0, None); 6790 assert_eq!( 6791 guest.ssh_command("sudo mount /dev/nvme0n1 /mnt").unwrap(), 6792 "" 6793 ); 6794 assert_eq!( 6795 guest.ssh_command("sudo cat /mnt/test").unwrap().trim(), 6796 "test123" 6797 ); 6798 }); 6799 6800 cleanup_spdk_nvme(); 6801 6802 kill_child(&mut child); 6803 let output = child.wait_with_output().unwrap(); 6804 6805 handle_child_output(r, &output); 6806 } 6807 6808 #[test] 6809 #[cfg(target_arch = "x86_64")] 6810 fn test_vdpa_block() { 6811 // Before trying to run the test, verify the vdpa_sim_blk module is correctly loaded. 6812 assert!(exec_host_command_status("lsmod | grep vdpa_sim_blk").success()); 6813 6814 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6815 let guest = Guest::new(Box::new(focal)); 6816 let api_socket = temp_api_path(&guest.tmp_dir); 6817 6818 let kernel_path = direct_kernel_boot_path(); 6819 6820 let mut child = GuestCommand::new(&guest) 6821 .args(["--cpus", "boot=2"]) 6822 .args(["--memory", "size=512M,hugepages=on"]) 6823 .args(["--kernel", kernel_path.to_str().unwrap()]) 6824 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6825 .default_disks() 6826 .default_net() 6827 .args(["--vdpa", "path=/dev/vhost-vdpa-0,num_queues=1"]) 6828 .args(["--platform", "num_pci_segments=2,iommu_segments=1"]) 6829 .args(["--api-socket", &api_socket]) 6830 .capture_output() 6831 .spawn() 6832 .unwrap(); 6833 6834 let r = std::panic::catch_unwind(|| { 6835 guest.wait_vm_boot(None).unwrap(); 6836 6837 // Check both if /dev/vdc exists and if the block size is 128M. 6838 assert_eq!( 6839 guest 6840 .ssh_command("lsblk | grep vdc | grep -c 128M") 6841 .unwrap() 6842 .trim() 6843 .parse::<u32>() 6844 .unwrap_or_default(), 6845 1 6846 ); 6847 6848 // Check the content of the block device after we wrote to it. 6849 // The vpda-sim-blk should let us read what we previously wrote. 6850 guest 6851 .ssh_command("sudo bash -c 'echo foobar > /dev/vdc'") 6852 .unwrap(); 6853 assert_eq!( 6854 guest.ssh_command("sudo head -1 /dev/vdc").unwrap().trim(), 6855 "foobar" 6856 ); 6857 6858 // Hotplug an extra vDPA block device behind the vIOMMU 6859 // Add a new vDPA device to the VM 6860 let (cmd_success, cmd_output) = remote_command_w_output( 6861 &api_socket, 6862 "add-vdpa", 6863 Some("id=myvdpa0,path=/dev/vhost-vdpa-1,num_queues=1,pci_segment=1,iommu=on"), 6864 ); 6865 assert!(cmd_success); 6866 assert!(String::from_utf8_lossy(&cmd_output) 6867 .contains("{\"id\":\"myvdpa0\",\"bdf\":\"0001:00:01.0\"}")); 6868 6869 thread::sleep(std::time::Duration::new(10, 0)); 6870 6871 // Check IOMMU setup 6872 assert!(guest 6873 .does_device_vendor_pair_match("0x1057", "0x1af4") 6874 .unwrap_or_default()); 6875 assert_eq!( 6876 guest 6877 .ssh_command("ls /sys/kernel/iommu_groups/0/devices") 6878 .unwrap() 6879 .trim(), 6880 "0001:00:01.0" 6881 ); 6882 6883 // Check both if /dev/vdd exists and if the block size is 128M. 6884 assert_eq!( 6885 guest 6886 .ssh_command("lsblk | grep vdd | grep -c 128M") 6887 .unwrap() 6888 .trim() 6889 .parse::<u32>() 6890 .unwrap_or_default(), 6891 1 6892 ); 6893 6894 // Write some content to the block device we've just plugged. 6895 guest 6896 .ssh_command("sudo bash -c 'echo foobar > /dev/vdd'") 6897 .unwrap(); 6898 6899 // Check we can read the content back. 6900 assert_eq!( 6901 guest.ssh_command("sudo head -1 /dev/vdd").unwrap().trim(), 6902 "foobar" 6903 ); 6904 6905 // Unplug the device 6906 let cmd_success = remote_command(&api_socket, "remove-device", Some("myvdpa0")); 6907 assert!(cmd_success); 6908 thread::sleep(std::time::Duration::new(10, 0)); 6909 6910 // Check /dev/vdd doesn't exist anymore 6911 assert_eq!( 6912 guest 6913 .ssh_command("lsblk | grep -c vdd || true") 6914 .unwrap() 6915 .trim() 6916 .parse::<u32>() 6917 .unwrap_or(1), 6918 0 6919 ); 6920 }); 6921 6922 kill_child(&mut child); 6923 let output = child.wait_with_output().unwrap(); 6924 6925 handle_child_output(r, &output); 6926 } 6927 6928 #[test] 6929 #[cfg(target_arch = "x86_64")] 6930 #[ignore = "See #5756"] 6931 fn test_vdpa_net() { 6932 // Before trying to run the test, verify the vdpa_sim_net module is correctly loaded. 6933 if !exec_host_command_status("lsmod | grep vdpa_sim_net").success() { 6934 return; 6935 } 6936 6937 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 6938 let guest = Guest::new(Box::new(focal)); 6939 6940 let kernel_path = direct_kernel_boot_path(); 6941 6942 let mut child = GuestCommand::new(&guest) 6943 .args(["--cpus", "boot=2"]) 6944 .args(["--memory", "size=512M,hugepages=on"]) 6945 .args(["--kernel", kernel_path.to_str().unwrap()]) 6946 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 6947 .default_disks() 6948 .default_net() 6949 .args(["--vdpa", "path=/dev/vhost-vdpa-2,num_queues=2"]) 6950 .capture_output() 6951 .spawn() 6952 .unwrap(); 6953 6954 let r = std::panic::catch_unwind(|| { 6955 guest.wait_vm_boot(None).unwrap(); 6956 6957 // Check we can find network interface related to vDPA device 6958 assert_eq!( 6959 guest 6960 .ssh_command("ip -o link | grep -c ens6") 6961 .unwrap() 6962 .trim() 6963 .parse::<u32>() 6964 .unwrap_or(0), 6965 1 6966 ); 6967 6968 guest 6969 .ssh_command("sudo ip addr add 172.16.1.2/24 dev ens6") 6970 .unwrap(); 6971 guest.ssh_command("sudo ip link set up dev ens6").unwrap(); 6972 6973 // Check there is no packet yet on both TX/RX of the network interface 6974 assert_eq!( 6975 guest 6976 .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 0'") 6977 .unwrap() 6978 .trim() 6979 .parse::<u32>() 6980 .unwrap_or(0), 6981 2 6982 ); 6983 6984 // Send 6 packets with ping command 6985 guest.ssh_command("ping 172.16.1.10 -c 6 || true").unwrap(); 6986 6987 // Check we can find 6 packets on both TX/RX of the network interface 6988 assert_eq!( 6989 guest 6990 .ssh_command("ip -j -p -s link show ens6 | grep -c '\"packets\": 6'") 6991 .unwrap() 6992 .trim() 6993 .parse::<u32>() 6994 .unwrap_or(0), 6995 2 6996 ); 6997 6998 // No need to check for hotplug as we already tested it through 6999 // test_vdpa_block() 7000 }); 7001 7002 kill_child(&mut child); 7003 let output = child.wait_with_output().unwrap(); 7004 7005 handle_child_output(r, &output); 7006 } 7007 7008 #[test] 7009 #[cfg(target_arch = "x86_64")] 7010 fn test_tpm() { 7011 let focal = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); 7012 let guest = Guest::new(Box::new(focal)); 7013 7014 let (mut swtpm_command, swtpm_socket_path) = prepare_swtpm_daemon(&guest.tmp_dir); 7015 7016 let mut guest_cmd = GuestCommand::new(&guest); 7017 guest_cmd 7018 .args(["--cpus", "boot=1"]) 7019 .args(["--memory", "size=512M"]) 7020 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 7021 .args(["--tpm", &format!("socket={swtpm_socket_path}")]) 7022 .capture_output() 7023 .default_disks() 7024 .default_net(); 7025 7026 // Start swtpm daemon 7027 let mut swtpm_child = swtpm_command.spawn().unwrap(); 7028 thread::sleep(std::time::Duration::new(10, 0)); 7029 let mut child = guest_cmd.spawn().unwrap(); 7030 let r = std::panic::catch_unwind(|| { 7031 guest.wait_vm_boot(None).unwrap(); 7032 assert_eq!( 7033 guest.ssh_command("ls /dev/tpm0").unwrap().trim(), 7034 "/dev/tpm0" 7035 ); 7036 guest.ssh_command("sudo tpm2_selftest -f").unwrap(); 7037 guest 7038 .ssh_command("echo 'hello' > /tmp/checksum_test; ") 7039 .unwrap(); 7040 guest.ssh_command("cmp <(sudo tpm2_pcrevent /tmp/checksum_test | grep sha256 | awk '{print $2}') <(sha256sum /tmp/checksum_test| awk '{print $1}')").unwrap(); 7041 }); 7042 7043 let _ = swtpm_child.kill(); 7044 let _d_out = swtpm_child.wait_with_output().unwrap(); 7045 7046 kill_child(&mut child); 7047 let output = child.wait_with_output().unwrap(); 7048 7049 handle_child_output(r, &output); 7050 } 7051 7052 #[test] 7053 #[cfg(target_arch = "x86_64")] 7054 fn test_double_tty() { 7055 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7056 let guest = Guest::new(Box::new(focal)); 7057 let mut cmd = GuestCommand::new(&guest); 7058 let api_socket = temp_api_path(&guest.tmp_dir); 7059 let tty_str: &str = "console=hvc0 earlyprintk=ttyS0 "; 7060 // linux printk module enable console log. 7061 let con_dis_str: &str = "console [hvc0] enabled"; 7062 // linux printk module disable console log. 7063 let con_enb_str: &str = "bootconsole [earlyser0] disabled"; 7064 7065 let kernel_path = direct_kernel_boot_path(); 7066 7067 cmd.args(["--cpus", "boot=1"]) 7068 .args(["--memory", "size=512M"]) 7069 .args(["--kernel", kernel_path.to_str().unwrap()]) 7070 .args([ 7071 "--cmdline", 7072 DIRECT_KERNEL_BOOT_CMDLINE 7073 .replace("console=hvc0 ", tty_str) 7074 .as_str(), 7075 ]) 7076 .capture_output() 7077 .default_disks() 7078 .default_net() 7079 .args(["--serial", "tty"]) 7080 .args(["--console", "tty"]) 7081 .args(["--api-socket", &api_socket]); 7082 7083 let mut child = cmd.spawn().unwrap(); 7084 7085 let mut r = std::panic::catch_unwind(|| { 7086 guest.wait_vm_boot(None).unwrap(); 7087 }); 7088 7089 kill_child(&mut child); 7090 let output = child.wait_with_output().unwrap(); 7091 7092 if r.is_ok() { 7093 r = std::panic::catch_unwind(|| { 7094 let s = String::from_utf8_lossy(&output.stdout); 7095 assert!(s.contains(tty_str)); 7096 assert!(s.contains(con_dis_str)); 7097 assert!(s.contains(con_enb_str)); 7098 }); 7099 } 7100 7101 handle_child_output(r, &output); 7102 } 7103 7104 #[test] 7105 #[cfg(target_arch = "x86_64")] 7106 fn test_nmi() { 7107 let jammy = UbuntuDiskConfig::new(JAMMY_IMAGE_NAME.to_string()); 7108 let guest = Guest::new(Box::new(jammy)); 7109 let api_socket = temp_api_path(&guest.tmp_dir); 7110 let event_path = temp_event_monitor_path(&guest.tmp_dir); 7111 7112 let kernel_path = direct_kernel_boot_path(); 7113 let cmd_line = format!("{} {}", DIRECT_KERNEL_BOOT_CMDLINE, "unknown_nmi_panic=1"); 7114 7115 let mut cmd = GuestCommand::new(&guest); 7116 cmd.args(["--cpus", "boot=4"]) 7117 .args(["--memory", "size=512M"]) 7118 .args(["--kernel", kernel_path.to_str().unwrap()]) 7119 .args(["--cmdline", cmd_line.as_str()]) 7120 .default_disks() 7121 .args(["--net", guest.default_net_string().as_str()]) 7122 .args(["--pvpanic"]) 7123 .args(["--api-socket", &api_socket]) 7124 .args(["--event-monitor", format!("path={event_path}").as_str()]) 7125 .capture_output(); 7126 7127 let mut child = cmd.spawn().unwrap(); 7128 7129 let r = std::panic::catch_unwind(|| { 7130 guest.wait_vm_boot(None).unwrap(); 7131 7132 assert!(remote_command(&api_socket, "nmi", None)); 7133 7134 // Wait a while for guest 7135 thread::sleep(std::time::Duration::new(3, 0)); 7136 7137 let expected_sequential_events = [&MetaEvent { 7138 event: "panic".to_string(), 7139 device_id: None, 7140 }]; 7141 assert!(check_latest_events_exact( 7142 &expected_sequential_events, 7143 &event_path 7144 )); 7145 }); 7146 7147 kill_child(&mut child); 7148 let output = child.wait_with_output().unwrap(); 7149 7150 handle_child_output(r, &output); 7151 } 7152 } 7153 7154 mod dbus_api { 7155 use crate::*; 7156 7157 // Start cloud-hypervisor with no VM parameters, running both the HTTP 7158 // and DBus APIs. Alternate calls to the external APIs (HTTP and DBus) 7159 // to create a VM, boot it, and verify that it can be shut down and then 7160 // booted again. 7161 #[test] 7162 fn test_api_dbus_and_http_interleaved() { 7163 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7164 let guest = Guest::new(Box::new(focal)); 7165 let dbus_api = TargetApi::new_dbus_api(&guest.tmp_dir); 7166 let http_api = TargetApi::new_http_api(&guest.tmp_dir); 7167 7168 let mut child = GuestCommand::new(&guest) 7169 .args(dbus_api.guest_args()) 7170 .args(http_api.guest_args()) 7171 .capture_output() 7172 .spawn() 7173 .unwrap(); 7174 7175 thread::sleep(std::time::Duration::new(1, 0)); 7176 7177 // Verify API servers are running 7178 assert!(dbus_api.remote_command("ping", None)); 7179 assert!(http_api.remote_command("ping", None)); 7180 7181 // Create the VM first 7182 let cpu_count: u8 = 4; 7183 let request_body = guest.api_create_body( 7184 cpu_count, 7185 direct_kernel_boot_path().to_str().unwrap(), 7186 DIRECT_KERNEL_BOOT_CMDLINE, 7187 ); 7188 7189 let temp_config_path = guest.tmp_dir.as_path().join("config"); 7190 std::fs::write(&temp_config_path, request_body).unwrap(); 7191 let create_config = temp_config_path.as_os_str().to_str().unwrap(); 7192 7193 let r = std::panic::catch_unwind(|| { 7194 // Create the VM 7195 assert!(dbus_api.remote_command("create", Some(create_config),)); 7196 7197 // Then boot it 7198 assert!(http_api.remote_command("boot", None)); 7199 guest.wait_vm_boot(None).unwrap(); 7200 7201 // Check that the VM booted as expected 7202 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 7203 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 7204 7205 // Sync and shutdown without powering off to prevent filesystem 7206 // corruption. 7207 guest.ssh_command("sync").unwrap(); 7208 guest.ssh_command("sudo shutdown -H now").unwrap(); 7209 7210 // Wait for the guest to be fully shutdown 7211 thread::sleep(std::time::Duration::new(20, 0)); 7212 7213 // Then shutdown the VM 7214 assert!(dbus_api.remote_command("shutdown", None)); 7215 7216 // Then boot it again 7217 assert!(http_api.remote_command("boot", None)); 7218 guest.wait_vm_boot(None).unwrap(); 7219 7220 // Check that the VM booted as expected 7221 assert_eq!(guest.get_cpu_count().unwrap_or_default() as u8, cpu_count); 7222 assert!(guest.get_total_memory().unwrap_or_default() > 480_000); 7223 }); 7224 7225 kill_child(&mut child); 7226 let output = child.wait_with_output().unwrap(); 7227 7228 handle_child_output(r, &output); 7229 } 7230 7231 #[test] 7232 fn test_api_dbus_create_boot() { 7233 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7234 let guest = Guest::new(Box::new(focal)); 7235 7236 _test_api_create_boot(TargetApi::new_dbus_api(&guest.tmp_dir), guest) 7237 } 7238 7239 #[test] 7240 fn test_api_dbus_shutdown() { 7241 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7242 let guest = Guest::new(Box::new(focal)); 7243 7244 _test_api_shutdown(TargetApi::new_dbus_api(&guest.tmp_dir), guest) 7245 } 7246 7247 #[test] 7248 fn test_api_dbus_delete() { 7249 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7250 let guest = Guest::new(Box::new(focal)); 7251 7252 _test_api_delete(TargetApi::new_dbus_api(&guest.tmp_dir), guest); 7253 } 7254 7255 #[test] 7256 fn test_api_dbus_pause_resume() { 7257 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7258 let guest = Guest::new(Box::new(focal)); 7259 7260 _test_api_pause_resume(TargetApi::new_dbus_api(&guest.tmp_dir), guest) 7261 } 7262 } 7263 7264 mod common_sequential { 7265 use std::fs::remove_dir_all; 7266 7267 use crate::*; 7268 7269 #[test] 7270 #[cfg(not(feature = "mshv"))] 7271 fn test_memory_mergeable_on() { 7272 test_memory_mergeable(true) 7273 } 7274 7275 fn snapshot_and_check_events(api_socket: &str, snapshot_dir: &str, event_path: &str) { 7276 // Pause the VM 7277 assert!(remote_command(api_socket, "pause", None)); 7278 let latest_events: [&MetaEvent; 2] = [ 7279 &MetaEvent { 7280 event: "pausing".to_string(), 7281 device_id: None, 7282 }, 7283 &MetaEvent { 7284 event: "paused".to_string(), 7285 device_id: None, 7286 }, 7287 ]; 7288 // See: #5938 7289 thread::sleep(std::time::Duration::new(1, 0)); 7290 assert!(check_latest_events_exact(&latest_events, event_path)); 7291 7292 // Take a snapshot from the VM 7293 assert!(remote_command( 7294 api_socket, 7295 "snapshot", 7296 Some(format!("file://{snapshot_dir}").as_str()), 7297 )); 7298 7299 // Wait to make sure the snapshot is completed 7300 thread::sleep(std::time::Duration::new(10, 0)); 7301 7302 let latest_events = [ 7303 &MetaEvent { 7304 event: "snapshotting".to_string(), 7305 device_id: None, 7306 }, 7307 &MetaEvent { 7308 event: "snapshotted".to_string(), 7309 device_id: None, 7310 }, 7311 ]; 7312 // See: #5938 7313 thread::sleep(std::time::Duration::new(1, 0)); 7314 assert!(check_latest_events_exact(&latest_events, event_path)); 7315 } 7316 7317 // One thing to note about this test. The virtio-net device is heavily used 7318 // through each ssh command. There's no need to perform a dedicated test to 7319 // verify the migration went well for virtio-net. 7320 #[test] 7321 #[cfg(not(feature = "mshv"))] 7322 fn test_snapshot_restore_hotplug_virtiomem() { 7323 _test_snapshot_restore(true); 7324 } 7325 7326 #[test] 7327 fn test_snapshot_restore_basic() { 7328 _test_snapshot_restore(false); 7329 } 7330 7331 fn _test_snapshot_restore(use_hotplug: bool) { 7332 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7333 let guest = Guest::new(Box::new(focal)); 7334 let kernel_path = direct_kernel_boot_path(); 7335 7336 let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); 7337 7338 let net_id = "net123"; 7339 let net_params = format!( 7340 "id={},tap=,mac={},ip={},mask=255.255.255.0", 7341 net_id, guest.network.guest_mac, guest.network.host_ip 7342 ); 7343 let mut mem_params = "size=2G"; 7344 7345 if use_hotplug { 7346 mem_params = "size=2G,hotplug_method=virtio-mem,hotplug_size=32G" 7347 } 7348 7349 let cloudinit_params = format!( 7350 "path={},iommu=on", 7351 guest.disk_config.disk(DiskType::CloudInit).unwrap() 7352 ); 7353 7354 let socket = temp_vsock_path(&guest.tmp_dir); 7355 let event_path = temp_event_monitor_path(&guest.tmp_dir); 7356 7357 let mut child = GuestCommand::new(&guest) 7358 .args(["--api-socket", &api_socket_source]) 7359 .args(["--event-monitor", format!("path={event_path}").as_str()]) 7360 .args(["--cpus", "boot=4"]) 7361 .args(["--memory", mem_params]) 7362 .args(["--balloon", "size=0"]) 7363 .args(["--kernel", kernel_path.to_str().unwrap()]) 7364 .args([ 7365 "--disk", 7366 format!( 7367 "path={}", 7368 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 7369 ) 7370 .as_str(), 7371 cloudinit_params.as_str(), 7372 ]) 7373 .args(["--net", net_params.as_str()]) 7374 .args(["--vsock", format!("cid=3,socket={socket}").as_str()]) 7375 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 7376 .capture_output() 7377 .spawn() 7378 .unwrap(); 7379 7380 let console_text = String::from("On a branch floating down river a cricket, singing."); 7381 // Create the snapshot directory 7382 let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); 7383 7384 let r = std::panic::catch_unwind(|| { 7385 guest.wait_vm_boot(None).unwrap(); 7386 7387 // Check the number of vCPUs 7388 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); 7389 // Check the guest RAM 7390 assert!(guest.get_total_memory().unwrap_or_default() > 1_920_000); 7391 if use_hotplug { 7392 // Increase guest RAM with virtio-mem 7393 resize_command( 7394 &api_socket_source, 7395 None, 7396 Some(6 << 30), 7397 None, 7398 Some(&event_path), 7399 ); 7400 thread::sleep(std::time::Duration::new(5, 0)); 7401 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 7402 // Use balloon to remove RAM from the VM 7403 resize_command( 7404 &api_socket_source, 7405 None, 7406 None, 7407 Some(1 << 30), 7408 Some(&event_path), 7409 ); 7410 thread::sleep(std::time::Duration::new(5, 0)); 7411 let total_memory = guest.get_total_memory().unwrap_or_default(); 7412 assert!(total_memory > 4_800_000); 7413 assert!(total_memory < 5_760_000); 7414 } 7415 // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net 7416 guest.check_devices_common(Some(&socket), Some(&console_text), None); 7417 7418 // x86_64: We check that removing and adding back the virtio-net device 7419 // does not break the snapshot/restore support for virtio-pci. 7420 // This is an important thing to test as the hotplug will 7421 // trigger a PCI BAR reprogramming, which is a good way of 7422 // checking if the stored resources are correctly restored. 7423 // Unplug the virtio-net device 7424 // AArch64: Device hotplug is currently not supported, skipping here. 7425 #[cfg(target_arch = "x86_64")] 7426 { 7427 assert!(remote_command( 7428 &api_socket_source, 7429 "remove-device", 7430 Some(net_id), 7431 )); 7432 thread::sleep(std::time::Duration::new(10, 0)); 7433 let latest_events = [&MetaEvent { 7434 event: "device-removed".to_string(), 7435 device_id: Some(net_id.to_string()), 7436 }]; 7437 // See: #5938 7438 thread::sleep(std::time::Duration::new(1, 0)); 7439 assert!(check_latest_events_exact(&latest_events, &event_path)); 7440 7441 // Plug the virtio-net device again 7442 assert!(remote_command( 7443 &api_socket_source, 7444 "add-net", 7445 Some(net_params.as_str()), 7446 )); 7447 thread::sleep(std::time::Duration::new(10, 0)); 7448 } 7449 7450 snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); 7451 }); 7452 7453 // Shutdown the source VM and check console output 7454 kill_child(&mut child); 7455 let output = child.wait_with_output().unwrap(); 7456 handle_child_output(r, &output); 7457 7458 let r = std::panic::catch_unwind(|| { 7459 assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); 7460 }); 7461 7462 handle_child_output(r, &output); 7463 7464 // Remove the vsock socket file. 7465 Command::new("rm") 7466 .arg("-f") 7467 .arg(socket.as_str()) 7468 .output() 7469 .unwrap(); 7470 7471 let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); 7472 let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); 7473 7474 // Restore the VM from the snapshot 7475 let mut child = GuestCommand::new(&guest) 7476 .args(["--api-socket", &api_socket_restored]) 7477 .args([ 7478 "--event-monitor", 7479 format!("path={event_path_restored}").as_str(), 7480 ]) 7481 .args([ 7482 "--restore", 7483 format!("source_url=file://{snapshot_dir}").as_str(), 7484 ]) 7485 .capture_output() 7486 .spawn() 7487 .unwrap(); 7488 7489 // Wait for the VM to be restored 7490 thread::sleep(std::time::Duration::new(20, 0)); 7491 let expected_events = [ 7492 &MetaEvent { 7493 event: "starting".to_string(), 7494 device_id: None, 7495 }, 7496 &MetaEvent { 7497 event: "activated".to_string(), 7498 device_id: Some("__console".to_string()), 7499 }, 7500 &MetaEvent { 7501 event: "activated".to_string(), 7502 device_id: Some("__rng".to_string()), 7503 }, 7504 &MetaEvent { 7505 event: "restoring".to_string(), 7506 device_id: None, 7507 }, 7508 ]; 7509 assert!(check_sequential_events( 7510 &expected_events, 7511 &event_path_restored 7512 )); 7513 let latest_events = [&MetaEvent { 7514 event: "restored".to_string(), 7515 device_id: None, 7516 }]; 7517 assert!(check_latest_events_exact( 7518 &latest_events, 7519 &event_path_restored 7520 )); 7521 7522 // Remove the snapshot dir 7523 let _ = remove_dir_all(snapshot_dir.as_str()); 7524 7525 let r = std::panic::catch_unwind(|| { 7526 // Resume the VM 7527 assert!(remote_command(&api_socket_restored, "resume", None)); 7528 // There is no way that we can ensure the 'write()' to the 7529 // event file is completed when the 'resume' request is 7530 // returned successfully, because the 'write()' was done 7531 // asynchronously from a different thread of Cloud 7532 // Hypervisor (e.g. the event-monitor thread). 7533 thread::sleep(std::time::Duration::new(1, 0)); 7534 let latest_events = [ 7535 &MetaEvent { 7536 event: "resuming".to_string(), 7537 device_id: None, 7538 }, 7539 &MetaEvent { 7540 event: "resumed".to_string(), 7541 device_id: None, 7542 }, 7543 ]; 7544 assert!(check_latest_events_exact( 7545 &latest_events, 7546 &event_path_restored 7547 )); 7548 7549 // Perform same checks to validate VM has been properly restored 7550 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 4); 7551 let total_memory = guest.get_total_memory().unwrap_or_default(); 7552 if !use_hotplug { 7553 assert!(total_memory > 1_920_000); 7554 } else { 7555 assert!(total_memory > 4_800_000); 7556 assert!(total_memory < 5_760_000); 7557 // Deflate balloon to restore entire RAM to the VM 7558 resize_command(&api_socket_restored, None, None, Some(0), None); 7559 thread::sleep(std::time::Duration::new(5, 0)); 7560 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 7561 // Decrease guest RAM with virtio-mem 7562 resize_command(&api_socket_restored, None, Some(5 << 30), None, None); 7563 thread::sleep(std::time::Duration::new(5, 0)); 7564 let total_memory = guest.get_total_memory().unwrap_or_default(); 7565 assert!(total_memory > 4_800_000); 7566 assert!(total_memory < 5_760_000); 7567 } 7568 7569 guest.check_devices_common(Some(&socket), Some(&console_text), None); 7570 }); 7571 // Shutdown the target VM and check console output 7572 kill_child(&mut child); 7573 let output = child.wait_with_output().unwrap(); 7574 handle_child_output(r, &output); 7575 7576 let r = std::panic::catch_unwind(|| { 7577 assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); 7578 }); 7579 7580 handle_child_output(r, &output); 7581 } 7582 7583 #[test] 7584 fn test_snapshot_restore_with_fd() { 7585 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7586 let guest = Guest::new(Box::new(focal)); 7587 let kernel_path = direct_kernel_boot_path(); 7588 7589 let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); 7590 7591 let net_id = "net123"; 7592 let num_queue_pairs: usize = 2; 7593 // use a name that does not conflict with tap dev created from other tests 7594 let tap_name = "chtap999"; 7595 use std::str::FromStr; 7596 let taps = net_util::open_tap( 7597 Some(tap_name), 7598 Some(std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap()), 7599 None, 7600 &mut None, 7601 None, 7602 num_queue_pairs, 7603 Some(libc::O_RDWR | libc::O_NONBLOCK), 7604 ) 7605 .unwrap(); 7606 let net_params = format!( 7607 "id={},fd=[{},{}],mac={},ip={},mask=255.255.255.0,num_queues={}", 7608 net_id, 7609 taps[0].as_raw_fd(), 7610 taps[1].as_raw_fd(), 7611 guest.network.guest_mac, 7612 guest.network.host_ip, 7613 num_queue_pairs * 2 7614 ); 7615 7616 let cloudinit_params = format!( 7617 "path={},iommu=on", 7618 guest.disk_config.disk(DiskType::CloudInit).unwrap() 7619 ); 7620 7621 let n_cpu = 2; 7622 let event_path = temp_event_monitor_path(&guest.tmp_dir); 7623 7624 let mut child = GuestCommand::new(&guest) 7625 .args(["--api-socket", &api_socket_source]) 7626 .args(["--event-monitor", format!("path={event_path}").as_str()]) 7627 .args(["--cpus", format!("boot={}", n_cpu).as_str()]) 7628 .args(["--memory", "size=1G"]) 7629 .args(["--kernel", kernel_path.to_str().unwrap()]) 7630 .args([ 7631 "--disk", 7632 format!( 7633 "path={}", 7634 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 7635 ) 7636 .as_str(), 7637 cloudinit_params.as_str(), 7638 ]) 7639 .args(["--net", net_params.as_str()]) 7640 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 7641 .capture_output() 7642 .spawn() 7643 .unwrap(); 7644 7645 let console_text = String::from("On a branch floating down river a cricket, singing."); 7646 // Create the snapshot directory 7647 let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); 7648 7649 let r = std::panic::catch_unwind(|| { 7650 guest.wait_vm_boot(None).unwrap(); 7651 7652 // close the fds after VM boots, as CH duplicates them before using 7653 for tap in taps.iter() { 7654 unsafe { libc::close(tap.as_raw_fd()) }; 7655 } 7656 7657 // Check the number of vCPUs 7658 assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); 7659 // Check the guest RAM 7660 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 7661 7662 // Check the guest virtio-devices, e.g. block, rng, vsock, console, and net 7663 guest.check_devices_common(None, Some(&console_text), None); 7664 7665 snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); 7666 }); 7667 7668 // Shutdown the source VM and check console output 7669 kill_child(&mut child); 7670 let output = child.wait_with_output().unwrap(); 7671 handle_child_output(r, &output); 7672 7673 let r = std::panic::catch_unwind(|| { 7674 assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); 7675 }); 7676 7677 handle_child_output(r, &output); 7678 7679 let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); 7680 let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); 7681 7682 // Restore the VM from the snapshot 7683 let mut child = GuestCommand::new(&guest) 7684 .args(["--api-socket", &api_socket_restored]) 7685 .args([ 7686 "--event-monitor", 7687 format!("path={event_path_restored}").as_str(), 7688 ]) 7689 .capture_output() 7690 .spawn() 7691 .unwrap(); 7692 thread::sleep(std::time::Duration::new(2, 0)); 7693 7694 let taps = net_util::open_tap( 7695 Some(tap_name), 7696 Some(std::net::Ipv4Addr::from_str(&guest.network.host_ip).unwrap()), 7697 None, 7698 &mut None, 7699 None, 7700 num_queue_pairs, 7701 Some(libc::O_RDWR | libc::O_NONBLOCK), 7702 ) 7703 .unwrap(); 7704 let restore_params = format!( 7705 "source_url=file://{},net_fds=[{}@[{},{}]]", 7706 snapshot_dir, 7707 net_id, 7708 taps[0].as_raw_fd(), 7709 taps[1].as_raw_fd() 7710 ); 7711 assert!(remote_command( 7712 &api_socket_restored, 7713 "restore", 7714 Some(restore_params.as_str()) 7715 )); 7716 7717 // Wait for the VM to be restored 7718 thread::sleep(std::time::Duration::new(20, 0)); 7719 7720 // close the fds as CH duplicates them before using 7721 for tap in taps.iter() { 7722 unsafe { libc::close(tap.as_raw_fd()) }; 7723 } 7724 7725 let expected_events = [ 7726 &MetaEvent { 7727 event: "starting".to_string(), 7728 device_id: None, 7729 }, 7730 &MetaEvent { 7731 event: "activated".to_string(), 7732 device_id: Some("__console".to_string()), 7733 }, 7734 &MetaEvent { 7735 event: "activated".to_string(), 7736 device_id: Some("__rng".to_string()), 7737 }, 7738 &MetaEvent { 7739 event: "restoring".to_string(), 7740 device_id: None, 7741 }, 7742 ]; 7743 assert!(check_sequential_events( 7744 &expected_events, 7745 &event_path_restored 7746 )); 7747 let latest_events = [&MetaEvent { 7748 event: "restored".to_string(), 7749 device_id: None, 7750 }]; 7751 assert!(check_latest_events_exact( 7752 &latest_events, 7753 &event_path_restored 7754 )); 7755 7756 // Remove the snapshot dir 7757 let _ = remove_dir_all(snapshot_dir.as_str()); 7758 7759 let r = std::panic::catch_unwind(|| { 7760 // Resume the VM 7761 assert!(remote_command(&api_socket_restored, "resume", None)); 7762 // There is no way that we can ensure the 'write()' to the 7763 // event file is completed when the 'resume' request is 7764 // returned successfully, because the 'write()' was done 7765 // asynchronously from a different thread of Cloud 7766 // Hypervisor (e.g. the event-monitor thread). 7767 thread::sleep(std::time::Duration::new(1, 0)); 7768 let latest_events = [ 7769 &MetaEvent { 7770 event: "resuming".to_string(), 7771 device_id: None, 7772 }, 7773 &MetaEvent { 7774 event: "resumed".to_string(), 7775 device_id: None, 7776 }, 7777 ]; 7778 assert!(check_latest_events_exact( 7779 &latest_events, 7780 &event_path_restored 7781 )); 7782 7783 // Perform same checks to validate VM has been properly restored 7784 assert_eq!(guest.get_cpu_count().unwrap_or_default(), n_cpu); 7785 assert!(guest.get_total_memory().unwrap_or_default() > 960_000); 7786 7787 guest.check_devices_common(None, Some(&console_text), None); 7788 }); 7789 // Shutdown the target VM and check console output 7790 kill_child(&mut child); 7791 let output = child.wait_with_output().unwrap(); 7792 handle_child_output(r, &output); 7793 7794 let r = std::panic::catch_unwind(|| { 7795 assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); 7796 }); 7797 7798 handle_child_output(r, &output); 7799 } 7800 7801 #[test] 7802 #[cfg(target_arch = "x86_64")] 7803 fn test_snapshot_restore_pvpanic() { 7804 _test_snapshot_restore_devices(true); 7805 } 7806 7807 fn _test_snapshot_restore_devices(pvpanic: bool) { 7808 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 7809 let guest = Guest::new(Box::new(focal)); 7810 let kernel_path = direct_kernel_boot_path(); 7811 7812 let api_socket_source = format!("{}.1", temp_api_path(&guest.tmp_dir)); 7813 7814 let device_params = { 7815 let mut data = vec![]; 7816 if pvpanic { 7817 data.push("--pvpanic"); 7818 } 7819 data 7820 }; 7821 7822 let socket = temp_vsock_path(&guest.tmp_dir); 7823 let event_path = temp_event_monitor_path(&guest.tmp_dir); 7824 7825 let mut child = GuestCommand::new(&guest) 7826 .args(["--api-socket", &api_socket_source]) 7827 .args(["--event-monitor", format!("path={}", event_path).as_str()]) 7828 .args(["--cpus", "boot=2"]) 7829 .args(["--memory", "size=1G"]) 7830 .args(["--kernel", kernel_path.to_str().unwrap()]) 7831 .default_disks() 7832 .default_net() 7833 .args(["--vsock", format!("cid=3,socket={}", socket).as_str()]) 7834 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 7835 .args(device_params) 7836 .capture_output() 7837 .spawn() 7838 .unwrap(); 7839 7840 let console_text = String::from("On a branch floating down river a cricket, singing."); 7841 // Create the snapshot directory 7842 let snapshot_dir = temp_snapshot_dir_path(&guest.tmp_dir); 7843 7844 let r = std::panic::catch_unwind(|| { 7845 guest.wait_vm_boot(None).unwrap(); 7846 7847 // Check the number of vCPUs 7848 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 7849 7850 snapshot_and_check_events(&api_socket_source, &snapshot_dir, &event_path); 7851 }); 7852 7853 // Shutdown the source VM and check console output 7854 kill_child(&mut child); 7855 let output = child.wait_with_output().unwrap(); 7856 handle_child_output(r, &output); 7857 7858 // Remove the vsock socket file. 7859 Command::new("rm") 7860 .arg("-f") 7861 .arg(socket.as_str()) 7862 .output() 7863 .unwrap(); 7864 7865 let api_socket_restored = format!("{}.2", temp_api_path(&guest.tmp_dir)); 7866 let event_path_restored = format!("{}.2", temp_event_monitor_path(&guest.tmp_dir)); 7867 7868 // Restore the VM from the snapshot 7869 let mut child = GuestCommand::new(&guest) 7870 .args(["--api-socket", &api_socket_restored]) 7871 .args([ 7872 "--event-monitor", 7873 format!("path={event_path_restored}").as_str(), 7874 ]) 7875 .args([ 7876 "--restore", 7877 format!("source_url=file://{snapshot_dir}").as_str(), 7878 ]) 7879 .capture_output() 7880 .spawn() 7881 .unwrap(); 7882 7883 // Wait for the VM to be restored 7884 thread::sleep(std::time::Duration::new(20, 0)); 7885 7886 let latest_events = [&MetaEvent { 7887 event: "restored".to_string(), 7888 device_id: None, 7889 }]; 7890 assert!(check_latest_events_exact( 7891 &latest_events, 7892 &event_path_restored 7893 )); 7894 7895 // Remove the snapshot dir 7896 let _ = remove_dir_all(snapshot_dir.as_str()); 7897 7898 let r = std::panic::catch_unwind(|| { 7899 // Resume the VM 7900 assert!(remote_command(&api_socket_restored, "resume", None)); 7901 // There is no way that we can ensure the 'write()' to the 7902 // event file is completed when the 'resume' request is 7903 // returned successfully, because the 'write()' was done 7904 // asynchronously from a different thread of Cloud 7905 // Hypervisor (e.g. the event-monitor thread). 7906 thread::sleep(std::time::Duration::new(1, 0)); 7907 let latest_events = [ 7908 &MetaEvent { 7909 event: "resuming".to_string(), 7910 device_id: None, 7911 }, 7912 &MetaEvent { 7913 event: "resumed".to_string(), 7914 device_id: None, 7915 }, 7916 ]; 7917 assert!(check_latest_events_exact( 7918 &latest_events, 7919 &event_path_restored 7920 )); 7921 7922 // Check the number of vCPUs 7923 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 2); 7924 guest.check_devices_common(Some(&socket), Some(&console_text), None); 7925 7926 if pvpanic { 7927 // Trigger guest a panic 7928 make_guest_panic(&guest); 7929 // Wait a while for guest 7930 thread::sleep(std::time::Duration::new(10, 0)); 7931 7932 let expected_sequential_events = [&MetaEvent { 7933 event: "panic".to_string(), 7934 device_id: None, 7935 }]; 7936 assert!(check_latest_events_exact( 7937 &expected_sequential_events, 7938 &event_path_restored 7939 )); 7940 } 7941 }); 7942 // Shutdown the target VM and check console output 7943 kill_child(&mut child); 7944 let output = child.wait_with_output().unwrap(); 7945 handle_child_output(r, &output); 7946 7947 let r = std::panic::catch_unwind(|| { 7948 assert!(String::from_utf8_lossy(&output.stdout).contains(&console_text)); 7949 }); 7950 7951 handle_child_output(r, &output); 7952 } 7953 } 7954 7955 mod windows { 7956 use once_cell::sync::Lazy; 7957 7958 use crate::*; 7959 7960 static NEXT_DISK_ID: Lazy<Mutex<u8>> = Lazy::new(|| Mutex::new(1)); 7961 7962 struct WindowsGuest { 7963 guest: Guest, 7964 auth: PasswordAuth, 7965 } 7966 7967 trait FsType { 7968 const FS_FAT: u8; 7969 const FS_NTFS: u8; 7970 } 7971 impl FsType for WindowsGuest { 7972 const FS_FAT: u8 = 0; 7973 const FS_NTFS: u8 = 1; 7974 } 7975 7976 impl WindowsGuest { 7977 fn new() -> Self { 7978 let disk = WindowsDiskConfig::new(WINDOWS_IMAGE_NAME.to_string()); 7979 let guest = Guest::new(Box::new(disk)); 7980 let auth = PasswordAuth { 7981 username: String::from("administrator"), 7982 password: String::from("Admin123"), 7983 }; 7984 7985 WindowsGuest { guest, auth } 7986 } 7987 7988 fn guest(&self) -> &Guest { 7989 &self.guest 7990 } 7991 7992 fn ssh_cmd(&self, cmd: &str) -> String { 7993 ssh_command_ip_with_auth( 7994 cmd, 7995 &self.auth, 7996 &self.guest.network.guest_ip, 7997 DEFAULT_SSH_RETRIES, 7998 DEFAULT_SSH_TIMEOUT, 7999 ) 8000 .unwrap() 8001 } 8002 8003 fn cpu_count(&self) -> u8 { 8004 self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).NumberOfLogicalProcessors\"") 8005 .trim() 8006 .parse::<u8>() 8007 .unwrap_or(0) 8008 } 8009 8010 fn ram_size(&self) -> usize { 8011 self.ssh_cmd("powershell -Command \"(Get-CimInstance win32_computersystem).TotalPhysicalMemory\"") 8012 .trim() 8013 .parse::<usize>() 8014 .unwrap_or(0) 8015 } 8016 8017 fn netdev_count(&self) -> u8 { 8018 self.ssh_cmd("powershell -Command \"netsh int ipv4 show interfaces | Select-String ethernet | Measure-Object -Line | Format-Table -HideTableHeaders\"") 8019 .trim() 8020 .parse::<u8>() 8021 .unwrap_or(0) 8022 } 8023 8024 fn disk_count(&self) -> u8 { 8025 self.ssh_cmd("powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"") 8026 .trim() 8027 .parse::<u8>() 8028 .unwrap_or(0) 8029 } 8030 8031 fn reboot(&self) { 8032 let _ = self.ssh_cmd("shutdown /r /t 0"); 8033 } 8034 8035 fn shutdown(&self) { 8036 let _ = self.ssh_cmd("shutdown /s /t 0"); 8037 } 8038 8039 fn run_dnsmasq(&self) -> std::process::Child { 8040 let listen_address = format!("--listen-address={}", self.guest.network.host_ip); 8041 let dhcp_host = format!( 8042 "--dhcp-host={},{}", 8043 self.guest.network.guest_mac, self.guest.network.guest_ip 8044 ); 8045 let dhcp_range = format!( 8046 "--dhcp-range=eth,{},{}", 8047 self.guest.network.guest_ip, self.guest.network.guest_ip 8048 ); 8049 8050 Command::new("dnsmasq") 8051 .arg("--no-daemon") 8052 .arg("--log-queries") 8053 .arg(listen_address.as_str()) 8054 .arg("--except-interface=lo") 8055 .arg("--bind-dynamic") // Allow listening to host_ip while the interface is not ready yet. 8056 .arg("--conf-file=/dev/null") 8057 .arg(dhcp_host.as_str()) 8058 .arg(dhcp_range.as_str()) 8059 .spawn() 8060 .unwrap() 8061 } 8062 8063 // TODO Cleanup image file explicitly after test, if there's some space issues. 8064 fn disk_new(&self, fs: u8, sz: usize) -> String { 8065 let mut guard = NEXT_DISK_ID.lock().unwrap(); 8066 let id = *guard; 8067 *guard = id + 1; 8068 8069 let img = PathBuf::from(format!("/tmp/test-hotplug-{id}.raw")); 8070 let _ = fs::remove_file(&img); 8071 8072 // Create an image file 8073 let out = Command::new("qemu-img") 8074 .args([ 8075 "create", 8076 "-f", 8077 "raw", 8078 img.to_str().unwrap(), 8079 format!("{sz}m").as_str(), 8080 ]) 8081 .output() 8082 .expect("qemu-img command failed") 8083 .stdout; 8084 println!("{out:?}"); 8085 8086 // Associate image to a loop device 8087 let out = Command::new("losetup") 8088 .args(["--show", "-f", img.to_str().unwrap()]) 8089 .output() 8090 .expect("failed to create loop device") 8091 .stdout; 8092 let _tmp = String::from_utf8_lossy(&out); 8093 let loop_dev = _tmp.trim(); 8094 println!("{out:?}"); 8095 8096 // Create a partition table 8097 // echo 'type=7' | sudo sfdisk "${LOOP}" 8098 let mut child = Command::new("sfdisk") 8099 .args([loop_dev]) 8100 .stdin(Stdio::piped()) 8101 .spawn() 8102 .unwrap(); 8103 let stdin = child.stdin.as_mut().expect("failed to open stdin"); 8104 stdin 8105 .write_all("type=7".as_bytes()) 8106 .expect("failed to write stdin"); 8107 let out = child.wait_with_output().expect("sfdisk failed").stdout; 8108 println!("{out:?}"); 8109 8110 // Disengage the loop device 8111 let out = Command::new("losetup") 8112 .args(["-d", loop_dev]) 8113 .output() 8114 .expect("loop device not found") 8115 .stdout; 8116 println!("{out:?}"); 8117 8118 // Re-associate loop device pointing to the partition only 8119 let out = Command::new("losetup") 8120 .args([ 8121 "--show", 8122 "--offset", 8123 (512 * 2048).to_string().as_str(), 8124 "-f", 8125 img.to_str().unwrap(), 8126 ]) 8127 .output() 8128 .expect("failed to create loop device") 8129 .stdout; 8130 let _tmp = String::from_utf8_lossy(&out); 8131 let loop_dev = _tmp.trim(); 8132 println!("{out:?}"); 8133 8134 // Create filesystem. 8135 let fs_cmd = match fs { 8136 WindowsGuest::FS_FAT => "mkfs.msdos", 8137 WindowsGuest::FS_NTFS => "mkfs.ntfs", 8138 _ => panic!("Unknown filesystem type '{fs}'"), 8139 }; 8140 let out = Command::new(fs_cmd) 8141 .args([&loop_dev]) 8142 .output() 8143 .unwrap_or_else(|_| panic!("{fs_cmd} failed")) 8144 .stdout; 8145 println!("{out:?}"); 8146 8147 // Disengage the loop device 8148 let out = Command::new("losetup") 8149 .args(["-d", loop_dev]) 8150 .output() 8151 .unwrap_or_else(|_| panic!("loop device '{loop_dev}' not found")) 8152 .stdout; 8153 println!("{out:?}"); 8154 8155 img.to_str().unwrap().to_string() 8156 } 8157 8158 fn disks_set_rw(&self) { 8159 let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\""); 8160 } 8161 8162 fn disks_online(&self) { 8163 let _ = self.ssh_cmd("powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\""); 8164 } 8165 8166 fn disk_file_put(&self, fname: &str, data: &str) { 8167 let _ = self.ssh_cmd(&format!( 8168 "powershell -Command \"'{data}' | Set-Content -Path {fname}\"" 8169 )); 8170 } 8171 8172 fn disk_file_read(&self, fname: &str) -> String { 8173 self.ssh_cmd(&format!( 8174 "powershell -Command \"Get-Content -Path {fname}\"" 8175 )) 8176 } 8177 8178 fn wait_for_boot(&self) -> bool { 8179 let cmd = "dir /b c:\\ | find \"Windows\""; 8180 let tmo_max = 180; 8181 // The timeout increase by n*1+n*2+n*3+..., therefore the initial 8182 // interval must be small. 8183 let tmo_int = 2; 8184 let out = ssh_command_ip_with_auth( 8185 cmd, 8186 &self.auth, 8187 &self.guest.network.guest_ip, 8188 { 8189 let mut ret = 1; 8190 let mut tmo_acc = 0; 8191 loop { 8192 tmo_acc += tmo_int * ret; 8193 if tmo_acc >= tmo_max { 8194 break; 8195 } 8196 ret += 1; 8197 } 8198 ret 8199 }, 8200 tmo_int, 8201 ) 8202 .unwrap(); 8203 8204 if "Windows" == out.trim() { 8205 return true; 8206 } 8207 8208 false 8209 } 8210 } 8211 8212 fn vcpu_threads_count(pid: u32) -> u8 { 8213 // ps -T -p 12345 | grep vcpu | wc -l 8214 let out = Command::new("ps") 8215 .args(["-T", "-p", format!("{pid}").as_str()]) 8216 .output() 8217 .expect("ps command failed") 8218 .stdout; 8219 return String::from_utf8_lossy(&out).matches("vcpu").count() as u8; 8220 } 8221 8222 fn netdev_ctrl_threads_count(pid: u32) -> u8 { 8223 // ps -T -p 12345 | grep "_net[0-9]*_ctrl" | wc -l 8224 let out = Command::new("ps") 8225 .args(["-T", "-p", format!("{pid}").as_str()]) 8226 .output() 8227 .expect("ps command failed") 8228 .stdout; 8229 let mut n = 0; 8230 String::from_utf8_lossy(&out) 8231 .split_whitespace() 8232 .for_each(|s| n += (s.starts_with("_net") && s.ends_with("_ctrl")) as u8); // _net1_ctrl 8233 n 8234 } 8235 8236 fn disk_ctrl_threads_count(pid: u32) -> u8 { 8237 // ps -T -p 15782 | grep "_disk[0-9]*_q0" | wc -l 8238 let out = Command::new("ps") 8239 .args(["-T", "-p", format!("{pid}").as_str()]) 8240 .output() 8241 .expect("ps command failed") 8242 .stdout; 8243 let mut n = 0; 8244 String::from_utf8_lossy(&out) 8245 .split_whitespace() 8246 .for_each(|s| n += (s.starts_with("_disk") && s.ends_with("_q0")) as u8); // _disk0_q0, don't care about multiple queues as they're related to the same hdd 8247 n 8248 } 8249 8250 #[test] 8251 fn test_windows_guest() { 8252 let windows_guest = WindowsGuest::new(); 8253 8254 let mut child = GuestCommand::new(windows_guest.guest()) 8255 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8256 .args(["--memory", "size=4G"]) 8257 .args(["--kernel", edk2_path().to_str().unwrap()]) 8258 .args(["--serial", "tty"]) 8259 .args(["--console", "off"]) 8260 .default_disks() 8261 .default_net() 8262 .capture_output() 8263 .spawn() 8264 .unwrap(); 8265 8266 let fd = child.stdout.as_ref().unwrap().as_raw_fd(); 8267 let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8268 let fd = child.stderr.as_ref().unwrap().as_raw_fd(); 8269 let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8270 8271 assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); 8272 8273 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8274 8275 let r = std::panic::catch_unwind(|| { 8276 // Wait to make sure Windows boots up 8277 assert!(windows_guest.wait_for_boot()); 8278 8279 windows_guest.shutdown(); 8280 }); 8281 8282 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8283 let _ = child.kill(); 8284 let output = child.wait_with_output().unwrap(); 8285 8286 let _ = child_dnsmasq.kill(); 8287 let _ = child_dnsmasq.wait(); 8288 8289 handle_child_output(r, &output); 8290 } 8291 8292 #[test] 8293 fn test_windows_guest_multiple_queues() { 8294 let windows_guest = WindowsGuest::new(); 8295 8296 let mut ovmf_path = dirs::home_dir().unwrap(); 8297 ovmf_path.push("workloads"); 8298 ovmf_path.push(OVMF_NAME); 8299 8300 let mut child = GuestCommand::new(windows_guest.guest()) 8301 .args(["--cpus", "boot=4,kvm_hyperv=on"]) 8302 .args(["--memory", "size=4G"]) 8303 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8304 .args(["--serial", "tty"]) 8305 .args(["--console", "off"]) 8306 .args([ 8307 "--disk", 8308 format!( 8309 "path={},num_queues=4", 8310 windows_guest 8311 .guest() 8312 .disk_config 8313 .disk(DiskType::OperatingSystem) 8314 .unwrap() 8315 ) 8316 .as_str(), 8317 ]) 8318 .args([ 8319 "--net", 8320 format!( 8321 "tap=,mac={},ip={},mask=255.255.255.0,num_queues=8", 8322 windows_guest.guest().network.guest_mac, 8323 windows_guest.guest().network.host_ip 8324 ) 8325 .as_str(), 8326 ]) 8327 .capture_output() 8328 .spawn() 8329 .unwrap(); 8330 8331 let fd = child.stdout.as_ref().unwrap().as_raw_fd(); 8332 let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8333 let fd = child.stderr.as_ref().unwrap().as_raw_fd(); 8334 let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8335 8336 assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); 8337 8338 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8339 8340 let r = std::panic::catch_unwind(|| { 8341 // Wait to make sure Windows boots up 8342 assert!(windows_guest.wait_for_boot()); 8343 8344 windows_guest.shutdown(); 8345 }); 8346 8347 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8348 let _ = child.kill(); 8349 let output = child.wait_with_output().unwrap(); 8350 8351 let _ = child_dnsmasq.kill(); 8352 let _ = child_dnsmasq.wait(); 8353 8354 handle_child_output(r, &output); 8355 } 8356 8357 #[test] 8358 #[cfg(not(feature = "mshv"))] 8359 #[ignore = "See #4327"] 8360 fn test_windows_guest_snapshot_restore() { 8361 let windows_guest = WindowsGuest::new(); 8362 8363 let mut ovmf_path = dirs::home_dir().unwrap(); 8364 ovmf_path.push("workloads"); 8365 ovmf_path.push(OVMF_NAME); 8366 8367 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8368 let api_socket_source = format!("{}.1", temp_api_path(&tmp_dir)); 8369 8370 let mut child = GuestCommand::new(windows_guest.guest()) 8371 .args(["--api-socket", &api_socket_source]) 8372 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8373 .args(["--memory", "size=4G"]) 8374 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8375 .args(["--serial", "tty"]) 8376 .args(["--console", "off"]) 8377 .default_disks() 8378 .default_net() 8379 .capture_output() 8380 .spawn() 8381 .unwrap(); 8382 8383 let fd = child.stdout.as_ref().unwrap().as_raw_fd(); 8384 let pipesize = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8385 let fd = child.stderr.as_ref().unwrap().as_raw_fd(); 8386 let pipesize1 = unsafe { libc::fcntl(fd, libc::F_SETPIPE_SZ, PIPE_SIZE) }; 8387 8388 assert!(pipesize >= PIPE_SIZE && pipesize1 >= PIPE_SIZE); 8389 8390 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8391 8392 // Wait to make sure Windows boots up 8393 assert!(windows_guest.wait_for_boot()); 8394 8395 let snapshot_dir = temp_snapshot_dir_path(&tmp_dir); 8396 8397 // Pause the VM 8398 assert!(remote_command(&api_socket_source, "pause", None)); 8399 8400 // Take a snapshot from the VM 8401 assert!(remote_command( 8402 &api_socket_source, 8403 "snapshot", 8404 Some(format!("file://{snapshot_dir}").as_str()), 8405 )); 8406 8407 // Wait to make sure the snapshot is completed 8408 thread::sleep(std::time::Duration::new(30, 0)); 8409 8410 let _ = child.kill(); 8411 child.wait().unwrap(); 8412 8413 let api_socket_restored = format!("{}.2", temp_api_path(&tmp_dir)); 8414 8415 // Restore the VM from the snapshot 8416 let mut child = GuestCommand::new(windows_guest.guest()) 8417 .args(["--api-socket", &api_socket_restored]) 8418 .args([ 8419 "--restore", 8420 format!("source_url=file://{snapshot_dir}").as_str(), 8421 ]) 8422 .capture_output() 8423 .spawn() 8424 .unwrap(); 8425 8426 // Wait for the VM to be restored 8427 thread::sleep(std::time::Duration::new(20, 0)); 8428 8429 let r = std::panic::catch_unwind(|| { 8430 // Resume the VM 8431 assert!(remote_command(&api_socket_restored, "resume", None)); 8432 8433 windows_guest.shutdown(); 8434 }); 8435 8436 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8437 let _ = child.kill(); 8438 let output = child.wait_with_output().unwrap(); 8439 8440 let _ = child_dnsmasq.kill(); 8441 let _ = child_dnsmasq.wait(); 8442 8443 handle_child_output(r, &output); 8444 } 8445 8446 #[test] 8447 #[cfg(not(feature = "mshv"))] 8448 #[cfg(not(target_arch = "aarch64"))] 8449 fn test_windows_guest_cpu_hotplug() { 8450 let windows_guest = WindowsGuest::new(); 8451 8452 let mut ovmf_path = dirs::home_dir().unwrap(); 8453 ovmf_path.push("workloads"); 8454 ovmf_path.push(OVMF_NAME); 8455 8456 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8457 let api_socket = temp_api_path(&tmp_dir); 8458 8459 let mut child = GuestCommand::new(windows_guest.guest()) 8460 .args(["--api-socket", &api_socket]) 8461 .args(["--cpus", "boot=2,max=8,kvm_hyperv=on"]) 8462 .args(["--memory", "size=4G"]) 8463 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8464 .args(["--serial", "tty"]) 8465 .args(["--console", "off"]) 8466 .default_disks() 8467 .default_net() 8468 .capture_output() 8469 .spawn() 8470 .unwrap(); 8471 8472 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8473 8474 let r = std::panic::catch_unwind(|| { 8475 // Wait to make sure Windows boots up 8476 assert!(windows_guest.wait_for_boot()); 8477 8478 let vcpu_num = 2; 8479 // Check the initial number of CPUs the guest sees 8480 assert_eq!(windows_guest.cpu_count(), vcpu_num); 8481 // Check the initial number of vcpu threads in the CH process 8482 assert_eq!(vcpu_threads_count(child.id()), vcpu_num); 8483 8484 let vcpu_num = 6; 8485 // Hotplug some CPUs 8486 resize_command(&api_socket, Some(vcpu_num), None, None, None); 8487 // Wait to make sure CPUs are added 8488 thread::sleep(std::time::Duration::new(10, 0)); 8489 // Check the guest sees the correct number 8490 assert_eq!(windows_guest.cpu_count(), vcpu_num); 8491 // Check the CH process has the correct number of vcpu threads 8492 assert_eq!(vcpu_threads_count(child.id()), vcpu_num); 8493 8494 let vcpu_num = 4; 8495 // Remove some CPUs. Note that Windows doesn't support hot-remove. 8496 resize_command(&api_socket, Some(vcpu_num), None, None, None); 8497 // Wait to make sure CPUs are removed 8498 thread::sleep(std::time::Duration::new(10, 0)); 8499 // Reboot to let Windows catch up 8500 windows_guest.reboot(); 8501 // Wait to make sure Windows completely rebooted 8502 thread::sleep(std::time::Duration::new(60, 0)); 8503 // Check the guest sees the correct number 8504 assert_eq!(windows_guest.cpu_count(), vcpu_num); 8505 // Check the CH process has the correct number of vcpu threads 8506 assert_eq!(vcpu_threads_count(child.id()), vcpu_num); 8507 8508 windows_guest.shutdown(); 8509 }); 8510 8511 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8512 let _ = child.kill(); 8513 let output = child.wait_with_output().unwrap(); 8514 8515 let _ = child_dnsmasq.kill(); 8516 let _ = child_dnsmasq.wait(); 8517 8518 handle_child_output(r, &output); 8519 } 8520 8521 #[test] 8522 #[cfg(not(feature = "mshv"))] 8523 #[cfg(not(target_arch = "aarch64"))] 8524 fn test_windows_guest_ram_hotplug() { 8525 let windows_guest = WindowsGuest::new(); 8526 8527 let mut ovmf_path = dirs::home_dir().unwrap(); 8528 ovmf_path.push("workloads"); 8529 ovmf_path.push(OVMF_NAME); 8530 8531 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8532 let api_socket = temp_api_path(&tmp_dir); 8533 8534 let mut child = GuestCommand::new(windows_guest.guest()) 8535 .args(["--api-socket", &api_socket]) 8536 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8537 .args(["--memory", "size=2G,hotplug_size=5G"]) 8538 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8539 .args(["--serial", "tty"]) 8540 .args(["--console", "off"]) 8541 .default_disks() 8542 .default_net() 8543 .capture_output() 8544 .spawn() 8545 .unwrap(); 8546 8547 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8548 8549 let r = std::panic::catch_unwind(|| { 8550 // Wait to make sure Windows boots up 8551 assert!(windows_guest.wait_for_boot()); 8552 8553 let ram_size = 2 * 1024 * 1024 * 1024; 8554 // Check the initial number of RAM the guest sees 8555 let current_ram_size = windows_guest.ram_size(); 8556 // This size seems to be reserved by the system and thus the 8557 // reported amount differs by this constant value. 8558 let reserved_ram_size = ram_size - current_ram_size; 8559 // Verify that there's not more than 4mb constant diff wasted 8560 // by the reserved ram. 8561 assert!(reserved_ram_size < 4 * 1024 * 1024); 8562 8563 let ram_size = 4 * 1024 * 1024 * 1024; 8564 // Hotplug some RAM 8565 resize_command(&api_socket, None, Some(ram_size), None, None); 8566 // Wait to make sure RAM has been added 8567 thread::sleep(std::time::Duration::new(10, 0)); 8568 // Check the guest sees the correct number 8569 assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size); 8570 8571 let ram_size = 3 * 1024 * 1024 * 1024; 8572 // Unplug some RAM. Note that hot-remove most likely won't work. 8573 resize_command(&api_socket, None, Some(ram_size), None, None); 8574 // Wait to make sure RAM has been added 8575 thread::sleep(std::time::Duration::new(10, 0)); 8576 // Reboot to let Windows catch up 8577 windows_guest.reboot(); 8578 // Wait to make sure guest completely rebooted 8579 thread::sleep(std::time::Duration::new(60, 0)); 8580 // Check the guest sees the correct number 8581 assert_eq!(windows_guest.ram_size(), ram_size - reserved_ram_size); 8582 8583 windows_guest.shutdown(); 8584 }); 8585 8586 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8587 let _ = child.kill(); 8588 let output = child.wait_with_output().unwrap(); 8589 8590 let _ = child_dnsmasq.kill(); 8591 let _ = child_dnsmasq.wait(); 8592 8593 handle_child_output(r, &output); 8594 } 8595 8596 #[test] 8597 #[cfg(not(feature = "mshv"))] 8598 fn test_windows_guest_netdev_hotplug() { 8599 let windows_guest = WindowsGuest::new(); 8600 8601 let mut ovmf_path = dirs::home_dir().unwrap(); 8602 ovmf_path.push("workloads"); 8603 ovmf_path.push(OVMF_NAME); 8604 8605 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8606 let api_socket = temp_api_path(&tmp_dir); 8607 8608 let mut child = GuestCommand::new(windows_guest.guest()) 8609 .args(["--api-socket", &api_socket]) 8610 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8611 .args(["--memory", "size=4G"]) 8612 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8613 .args(["--serial", "tty"]) 8614 .args(["--console", "off"]) 8615 .default_disks() 8616 .default_net() 8617 .capture_output() 8618 .spawn() 8619 .unwrap(); 8620 8621 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8622 8623 let r = std::panic::catch_unwind(|| { 8624 // Wait to make sure Windows boots up 8625 assert!(windows_guest.wait_for_boot()); 8626 8627 // Initially present network device 8628 let netdev_num = 1; 8629 assert_eq!(windows_guest.netdev_count(), netdev_num); 8630 assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); 8631 8632 // Hotplug network device 8633 let (cmd_success, cmd_output) = remote_command_w_output( 8634 &api_socket, 8635 "add-net", 8636 Some(windows_guest.guest().default_net_string().as_str()), 8637 ); 8638 assert!(cmd_success); 8639 assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_net2\"")); 8640 thread::sleep(std::time::Duration::new(5, 0)); 8641 // Verify the device is on the system 8642 let netdev_num = 2; 8643 assert_eq!(windows_guest.netdev_count(), netdev_num); 8644 assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); 8645 8646 // Remove network device 8647 let cmd_success = remote_command(&api_socket, "remove-device", Some("_net2")); 8648 assert!(cmd_success); 8649 thread::sleep(std::time::Duration::new(5, 0)); 8650 // Verify the device has been removed 8651 let netdev_num = 1; 8652 assert_eq!(windows_guest.netdev_count(), netdev_num); 8653 assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); 8654 8655 windows_guest.shutdown(); 8656 }); 8657 8658 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8659 let _ = child.kill(); 8660 let output = child.wait_with_output().unwrap(); 8661 8662 let _ = child_dnsmasq.kill(); 8663 let _ = child_dnsmasq.wait(); 8664 8665 handle_child_output(r, &output); 8666 } 8667 8668 #[test] 8669 #[ignore = "See #6037"] 8670 #[cfg(not(feature = "mshv"))] 8671 #[cfg(not(target_arch = "aarch64"))] 8672 fn test_windows_guest_disk_hotplug() { 8673 let windows_guest = WindowsGuest::new(); 8674 8675 let mut ovmf_path = dirs::home_dir().unwrap(); 8676 ovmf_path.push("workloads"); 8677 ovmf_path.push(OVMF_NAME); 8678 8679 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8680 let api_socket = temp_api_path(&tmp_dir); 8681 8682 let mut child = GuestCommand::new(windows_guest.guest()) 8683 .args(["--api-socket", &api_socket]) 8684 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8685 .args(["--memory", "size=4G"]) 8686 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8687 .args(["--serial", "tty"]) 8688 .args(["--console", "off"]) 8689 .default_disks() 8690 .default_net() 8691 .capture_output() 8692 .spawn() 8693 .unwrap(); 8694 8695 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8696 8697 let disk = windows_guest.disk_new(WindowsGuest::FS_FAT, 100); 8698 8699 let r = std::panic::catch_unwind(|| { 8700 // Wait to make sure Windows boots up 8701 assert!(windows_guest.wait_for_boot()); 8702 8703 // Initially present disk device 8704 let disk_num = 1; 8705 assert_eq!(windows_guest.disk_count(), disk_num); 8706 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8707 8708 // Hotplug disk device 8709 let (cmd_success, cmd_output) = remote_command_w_output( 8710 &api_socket, 8711 "add-disk", 8712 Some(format!("path={disk},readonly=off").as_str()), 8713 ); 8714 assert!(cmd_success); 8715 assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_disk2\"")); 8716 thread::sleep(std::time::Duration::new(5, 0)); 8717 // Online disk device 8718 windows_guest.disks_set_rw(); 8719 windows_guest.disks_online(); 8720 // Verify the device is on the system 8721 let disk_num = 2; 8722 assert_eq!(windows_guest.disk_count(), disk_num); 8723 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8724 8725 let data = "hello"; 8726 let fname = "d:\\world"; 8727 windows_guest.disk_file_put(fname, data); 8728 8729 // Unmount disk device 8730 let cmd_success = remote_command(&api_socket, "remove-device", Some("_disk2")); 8731 assert!(cmd_success); 8732 thread::sleep(std::time::Duration::new(5, 0)); 8733 // Verify the device has been removed 8734 let disk_num = 1; 8735 assert_eq!(windows_guest.disk_count(), disk_num); 8736 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8737 8738 // Remount and check the file exists with the expected contents 8739 let (cmd_success, _cmd_output) = remote_command_w_output( 8740 &api_socket, 8741 "add-disk", 8742 Some(format!("path={disk},readonly=off").as_str()), 8743 ); 8744 assert!(cmd_success); 8745 thread::sleep(std::time::Duration::new(5, 0)); 8746 let out = windows_guest.disk_file_read(fname); 8747 assert_eq!(data, out.trim()); 8748 8749 // Intentionally no unmount, it'll happen at shutdown. 8750 8751 windows_guest.shutdown(); 8752 }); 8753 8754 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8755 let _ = child.kill(); 8756 let output = child.wait_with_output().unwrap(); 8757 8758 let _ = child_dnsmasq.kill(); 8759 let _ = child_dnsmasq.wait(); 8760 8761 handle_child_output(r, &output); 8762 } 8763 8764 #[test] 8765 #[ignore = "See #6037"] 8766 #[cfg(not(feature = "mshv"))] 8767 #[cfg(not(target_arch = "aarch64"))] 8768 fn test_windows_guest_disk_hotplug_multi() { 8769 let windows_guest = WindowsGuest::new(); 8770 8771 let mut ovmf_path = dirs::home_dir().unwrap(); 8772 ovmf_path.push("workloads"); 8773 ovmf_path.push(OVMF_NAME); 8774 8775 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8776 let api_socket = temp_api_path(&tmp_dir); 8777 8778 let mut child = GuestCommand::new(windows_guest.guest()) 8779 .args(["--api-socket", &api_socket]) 8780 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8781 .args(["--memory", "size=2G"]) 8782 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8783 .args(["--serial", "tty"]) 8784 .args(["--console", "off"]) 8785 .default_disks() 8786 .default_net() 8787 .capture_output() 8788 .spawn() 8789 .unwrap(); 8790 8791 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8792 8793 // Predefined data to used at various test stages 8794 let disk_test_data: [[String; 4]; 2] = [ 8795 [ 8796 "_disk2".to_string(), 8797 windows_guest.disk_new(WindowsGuest::FS_FAT, 123), 8798 "d:\\world".to_string(), 8799 "hello".to_string(), 8800 ], 8801 [ 8802 "_disk3".to_string(), 8803 windows_guest.disk_new(WindowsGuest::FS_NTFS, 333), 8804 "e:\\hello".to_string(), 8805 "world".to_string(), 8806 ], 8807 ]; 8808 8809 let r = std::panic::catch_unwind(|| { 8810 // Wait to make sure Windows boots up 8811 assert!(windows_guest.wait_for_boot()); 8812 8813 // Initially present disk device 8814 let disk_num = 1; 8815 assert_eq!(windows_guest.disk_count(), disk_num); 8816 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8817 8818 for it in &disk_test_data { 8819 let disk_id = it[0].as_str(); 8820 let disk = it[1].as_str(); 8821 // Hotplug disk device 8822 let (cmd_success, cmd_output) = remote_command_w_output( 8823 &api_socket, 8824 "add-disk", 8825 Some(format!("path={disk},readonly=off").as_str()), 8826 ); 8827 assert!(cmd_success); 8828 assert!(String::from_utf8_lossy(&cmd_output) 8829 .contains(format!("\"id\":\"{disk_id}\"").as_str())); 8830 thread::sleep(std::time::Duration::new(5, 0)); 8831 // Online disk devices 8832 windows_guest.disks_set_rw(); 8833 windows_guest.disks_online(); 8834 } 8835 // Verify the devices are on the system 8836 let disk_num = (disk_test_data.len() + 1) as u8; 8837 assert_eq!(windows_guest.disk_count(), disk_num); 8838 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8839 8840 // Put test data 8841 for it in &disk_test_data { 8842 let fname = it[2].as_str(); 8843 let data = it[3].as_str(); 8844 windows_guest.disk_file_put(fname, data); 8845 } 8846 8847 // Unmount disk devices 8848 for it in &disk_test_data { 8849 let disk_id = it[0].as_str(); 8850 let cmd_success = remote_command(&api_socket, "remove-device", Some(disk_id)); 8851 assert!(cmd_success); 8852 thread::sleep(std::time::Duration::new(5, 0)); 8853 } 8854 8855 // Verify the devices have been removed 8856 let disk_num = 1; 8857 assert_eq!(windows_guest.disk_count(), disk_num); 8858 assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); 8859 8860 // Remount 8861 for it in &disk_test_data { 8862 let disk = it[1].as_str(); 8863 let (cmd_success, _cmd_output) = remote_command_w_output( 8864 &api_socket, 8865 "add-disk", 8866 Some(format!("path={disk},readonly=off").as_str()), 8867 ); 8868 assert!(cmd_success); 8869 thread::sleep(std::time::Duration::new(5, 0)); 8870 } 8871 8872 // Check the files exists with the expected contents 8873 for it in &disk_test_data { 8874 let fname = it[2].as_str(); 8875 let data = it[3].as_str(); 8876 let out = windows_guest.disk_file_read(fname); 8877 assert_eq!(data, out.trim()); 8878 } 8879 8880 // Intentionally no unmount, it'll happen at shutdown. 8881 8882 windows_guest.shutdown(); 8883 }); 8884 8885 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8886 let _ = child.kill(); 8887 let output = child.wait_with_output().unwrap(); 8888 8889 let _ = child_dnsmasq.kill(); 8890 let _ = child_dnsmasq.wait(); 8891 8892 handle_child_output(r, &output); 8893 } 8894 8895 #[test] 8896 #[cfg(not(feature = "mshv"))] 8897 #[cfg(not(target_arch = "aarch64"))] 8898 fn test_windows_guest_netdev_multi() { 8899 let windows_guest = WindowsGuest::new(); 8900 8901 let mut ovmf_path = dirs::home_dir().unwrap(); 8902 ovmf_path.push("workloads"); 8903 ovmf_path.push(OVMF_NAME); 8904 8905 let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); 8906 let api_socket = temp_api_path(&tmp_dir); 8907 8908 let mut child = GuestCommand::new(windows_guest.guest()) 8909 .args(["--api-socket", &api_socket]) 8910 .args(["--cpus", "boot=2,kvm_hyperv=on"]) 8911 .args(["--memory", "size=4G"]) 8912 .args(["--kernel", ovmf_path.to_str().unwrap()]) 8913 .args(["--serial", "tty"]) 8914 .args(["--console", "off"]) 8915 .default_disks() 8916 // The multi net dev config is borrowed from test_multiple_network_interfaces 8917 .args([ 8918 "--net", 8919 windows_guest.guest().default_net_string().as_str(), 8920 "tap=,mac=8a:6b:6f:5a:de:ac,ip=192.168.3.1,mask=255.255.255.0", 8921 "tap=mytap42,mac=fe:1f:9e:e1:60:f2,ip=192.168.4.1,mask=255.255.255.0", 8922 ]) 8923 .capture_output() 8924 .spawn() 8925 .unwrap(); 8926 8927 let mut child_dnsmasq = windows_guest.run_dnsmasq(); 8928 8929 let r = std::panic::catch_unwind(|| { 8930 // Wait to make sure Windows boots up 8931 assert!(windows_guest.wait_for_boot()); 8932 8933 let netdev_num = 3; 8934 assert_eq!(windows_guest.netdev_count(), netdev_num); 8935 assert_eq!(netdev_ctrl_threads_count(child.id()), netdev_num); 8936 8937 let tap_count = exec_host_command_output("ip link | grep -c mytap42"); 8938 assert_eq!(String::from_utf8_lossy(&tap_count.stdout).trim(), "1"); 8939 8940 windows_guest.shutdown(); 8941 }); 8942 8943 let _ = child.wait_timeout(std::time::Duration::from_secs(60)); 8944 let _ = child.kill(); 8945 let output = child.wait_with_output().unwrap(); 8946 8947 let _ = child_dnsmasq.kill(); 8948 let _ = child_dnsmasq.wait(); 8949 8950 handle_child_output(r, &output); 8951 } 8952 } 8953 8954 #[cfg(target_arch = "x86_64")] 8955 mod sgx { 8956 use crate::*; 8957 8958 #[test] 8959 fn test_sgx() { 8960 let jammy_image = JAMMY_IMAGE_NAME.to_string(); 8961 let jammy = UbuntuDiskConfig::new(jammy_image); 8962 let guest = Guest::new(Box::new(jammy)); 8963 8964 let mut child = GuestCommand::new(&guest) 8965 .args(["--cpus", "boot=1"]) 8966 .args(["--memory", "size=512M"]) 8967 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 8968 .default_disks() 8969 .default_net() 8970 .args(["--sgx-epc", "id=epc0,size=64M"]) 8971 .capture_output() 8972 .spawn() 8973 .unwrap(); 8974 8975 let r = std::panic::catch_unwind(|| { 8976 guest.wait_vm_boot(None).unwrap(); 8977 8978 // Check if SGX is correctly detected in the guest. 8979 guest.check_sgx_support().unwrap(); 8980 8981 // Validate the SGX EPC section is 64MiB. 8982 assert_eq!( 8983 guest 8984 .ssh_command("cpuid -l 0x12 -s 2 | grep 'section size' | cut -d '=' -f 2") 8985 .unwrap() 8986 .trim(), 8987 "0x0000000004000000" 8988 ); 8989 }); 8990 8991 let _ = child.kill(); 8992 let output = child.wait_with_output().unwrap(); 8993 8994 handle_child_output(r, &output); 8995 } 8996 } 8997 8998 #[cfg(target_arch = "x86_64")] 8999 mod vfio { 9000 use crate::*; 9001 const NVIDIA_VFIO_DEVICE: &str = "/sys/bus/pci/devices/0002:00:01.0"; 9002 9003 fn test_nvidia_card_memory_hotplug(hotplug_method: &str) { 9004 let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); 9005 let guest = Guest::new(Box::new(jammy)); 9006 let api_socket = temp_api_path(&guest.tmp_dir); 9007 9008 let mut child = GuestCommand::new(&guest) 9009 .args(["--cpus", "boot=4"]) 9010 .args([ 9011 "--memory", 9012 format!("size=4G,hotplug_size=4G,hotplug_method={hotplug_method}").as_str(), 9013 ]) 9014 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 9015 .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) 9016 .args(["--api-socket", &api_socket]) 9017 .default_disks() 9018 .default_net() 9019 .capture_output() 9020 .spawn() 9021 .unwrap(); 9022 9023 let r = std::panic::catch_unwind(|| { 9024 guest.wait_vm_boot(None).unwrap(); 9025 9026 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9027 9028 guest.enable_memory_hotplug(); 9029 9030 // Add RAM to the VM 9031 let desired_ram = 6 << 30; 9032 resize_command(&api_socket, None, Some(desired_ram), None, None); 9033 thread::sleep(std::time::Duration::new(30, 0)); 9034 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 9035 9036 // Check the VFIO device works when RAM is increased to 6GiB 9037 guest.check_nvidia_gpu(); 9038 }); 9039 9040 let _ = child.kill(); 9041 let output = child.wait_with_output().unwrap(); 9042 9043 handle_child_output(r, &output); 9044 } 9045 9046 #[test] 9047 fn test_nvidia_card_memory_hotplug_acpi() { 9048 test_nvidia_card_memory_hotplug("acpi") 9049 } 9050 9051 #[test] 9052 fn test_nvidia_card_memory_hotplug_virtio_mem() { 9053 test_nvidia_card_memory_hotplug("virtio-mem") 9054 } 9055 9056 #[test] 9057 fn test_nvidia_card_pci_hotplug() { 9058 let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); 9059 let guest = Guest::new(Box::new(jammy)); 9060 let api_socket = temp_api_path(&guest.tmp_dir); 9061 9062 let mut child = GuestCommand::new(&guest) 9063 .args(["--cpus", "boot=4"]) 9064 .args(["--memory", "size=4G"]) 9065 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 9066 .args(["--api-socket", &api_socket]) 9067 .default_disks() 9068 .default_net() 9069 .capture_output() 9070 .spawn() 9071 .unwrap(); 9072 9073 let r = std::panic::catch_unwind(|| { 9074 guest.wait_vm_boot(None).unwrap(); 9075 9076 // Hotplug the card to the VM 9077 let (cmd_success, cmd_output) = remote_command_w_output( 9078 &api_socket, 9079 "add-device", 9080 Some(format!("id=vfio0,path={NVIDIA_VFIO_DEVICE}").as_str()), 9081 ); 9082 assert!(cmd_success); 9083 assert!(String::from_utf8_lossy(&cmd_output) 9084 .contains("{\"id\":\"vfio0\",\"bdf\":\"0000:00:06.0\"}")); 9085 9086 thread::sleep(std::time::Duration::new(10, 0)); 9087 9088 // Check the VFIO device works after hotplug 9089 guest.check_nvidia_gpu(); 9090 }); 9091 9092 let _ = child.kill(); 9093 let output = child.wait_with_output().unwrap(); 9094 9095 handle_child_output(r, &output); 9096 } 9097 9098 #[test] 9099 fn test_nvidia_card_reboot() { 9100 let jammy = UbuntuDiskConfig::new(JAMMY_VFIO_IMAGE_NAME.to_string()); 9101 let guest = Guest::new(Box::new(jammy)); 9102 let api_socket = temp_api_path(&guest.tmp_dir); 9103 9104 let mut child = GuestCommand::new(&guest) 9105 .args(["--cpus", "boot=4"]) 9106 .args(["--memory", "size=4G"]) 9107 .args(["--kernel", fw_path(FwType::RustHypervisorFirmware).as_str()]) 9108 .args(["--device", format!("path={NVIDIA_VFIO_DEVICE}").as_str()]) 9109 .args(["--api-socket", &api_socket]) 9110 .default_disks() 9111 .default_net() 9112 .capture_output() 9113 .spawn() 9114 .unwrap(); 9115 9116 let r = std::panic::catch_unwind(|| { 9117 guest.wait_vm_boot(None).unwrap(); 9118 9119 // Check the VFIO device works after boot 9120 guest.check_nvidia_gpu(); 9121 9122 guest.reboot_linux(0, None); 9123 9124 // Check the VFIO device works after reboot 9125 guest.check_nvidia_gpu(); 9126 }); 9127 9128 let _ = child.kill(); 9129 let output = child.wait_with_output().unwrap(); 9130 9131 handle_child_output(r, &output); 9132 } 9133 } 9134 9135 mod live_migration { 9136 use crate::*; 9137 9138 fn start_live_migration( 9139 migration_socket: &str, 9140 src_api_socket: &str, 9141 dest_api_socket: &str, 9142 local: bool, 9143 ) -> bool { 9144 // Start to receive migration from the destination VM 9145 let mut receive_migration = Command::new(clh_command("ch-remote")) 9146 .args([ 9147 &format!("--api-socket={dest_api_socket}"), 9148 "receive-migration", 9149 &format! {"unix:{migration_socket}"}, 9150 ]) 9151 .stderr(Stdio::piped()) 9152 .stdout(Stdio::piped()) 9153 .spawn() 9154 .unwrap(); 9155 // Give it '1s' to make sure the 'migration_socket' file is properly created 9156 thread::sleep(std::time::Duration::new(1, 0)); 9157 // Start to send migration from the source VM 9158 9159 let mut args = [ 9160 format!("--api-socket={}", &src_api_socket), 9161 "send-migration".to_string(), 9162 format! {"unix:{migration_socket}"}, 9163 ] 9164 .to_vec(); 9165 9166 if local { 9167 args.insert(2, "--local".to_string()); 9168 } 9169 9170 let mut send_migration = Command::new(clh_command("ch-remote")) 9171 .args(&args) 9172 .stderr(Stdio::piped()) 9173 .stdout(Stdio::piped()) 9174 .spawn() 9175 .unwrap(); 9176 9177 // The 'send-migration' command should be executed successfully within the given timeout 9178 let send_success = if let Some(status) = send_migration 9179 .wait_timeout(std::time::Duration::from_secs(30)) 9180 .unwrap() 9181 { 9182 status.success() 9183 } else { 9184 false 9185 }; 9186 9187 if !send_success { 9188 let _ = send_migration.kill(); 9189 let output = send_migration.wait_with_output().unwrap(); 9190 eprintln!( 9191 "\n\n==== Start 'send_migration' output ==== \ 9192 \n\n---stdout---\n{}\n\n---stderr---\n{} \ 9193 \n\n==== End 'send_migration' output ====\n\n", 9194 String::from_utf8_lossy(&output.stdout), 9195 String::from_utf8_lossy(&output.stderr) 9196 ); 9197 } 9198 9199 // The 'receive-migration' command should be executed successfully within the given timeout 9200 let receive_success = if let Some(status) = receive_migration 9201 .wait_timeout(std::time::Duration::from_secs(30)) 9202 .unwrap() 9203 { 9204 status.success() 9205 } else { 9206 false 9207 }; 9208 9209 if !receive_success { 9210 let _ = receive_migration.kill(); 9211 let output = receive_migration.wait_with_output().unwrap(); 9212 eprintln!( 9213 "\n\n==== Start 'receive_migration' output ==== \ 9214 \n\n---stdout---\n{}\n\n---stderr---\n{} \ 9215 \n\n==== End 'receive_migration' output ====\n\n", 9216 String::from_utf8_lossy(&output.stdout), 9217 String::from_utf8_lossy(&output.stderr) 9218 ); 9219 } 9220 9221 send_success && receive_success 9222 } 9223 9224 fn print_and_panic(src_vm: Child, dest_vm: Child, ovs_vm: Option<Child>, message: &str) -> ! { 9225 let mut src_vm = src_vm; 9226 let mut dest_vm = dest_vm; 9227 9228 let _ = src_vm.kill(); 9229 let src_output = src_vm.wait_with_output().unwrap(); 9230 eprintln!( 9231 "\n\n==== Start 'source_vm' stdout ====\n\n{}\n\n==== End 'source_vm' stdout ====", 9232 String::from_utf8_lossy(&src_output.stdout) 9233 ); 9234 eprintln!( 9235 "\n\n==== Start 'source_vm' stderr ====\n\n{}\n\n==== End 'source_vm' stderr ====", 9236 String::from_utf8_lossy(&src_output.stderr) 9237 ); 9238 let _ = dest_vm.kill(); 9239 let dest_output = dest_vm.wait_with_output().unwrap(); 9240 eprintln!( 9241 "\n\n==== Start 'destination_vm' stdout ====\n\n{}\n\n==== End 'destination_vm' stdout ====", 9242 String::from_utf8_lossy(&dest_output.stdout) 9243 ); 9244 eprintln!( 9245 "\n\n==== Start 'destination_vm' stderr ====\n\n{}\n\n==== End 'destination_vm' stderr ====", 9246 String::from_utf8_lossy(&dest_output.stderr) 9247 ); 9248 9249 if let Some(ovs_vm) = ovs_vm { 9250 let mut ovs_vm = ovs_vm; 9251 let _ = ovs_vm.kill(); 9252 let ovs_output = ovs_vm.wait_with_output().unwrap(); 9253 eprintln!( 9254 "\n\n==== Start 'ovs_vm' stdout ====\n\n{}\n\n==== End 'ovs_vm' stdout ====", 9255 String::from_utf8_lossy(&ovs_output.stdout) 9256 ); 9257 eprintln!( 9258 "\n\n==== Start 'ovs_vm' stderr ====\n\n{}\n\n==== End 'ovs_vm' stderr ====", 9259 String::from_utf8_lossy(&ovs_output.stderr) 9260 ); 9261 9262 cleanup_ovs_dpdk(); 9263 } 9264 9265 panic!("Test failed: {message}") 9266 } 9267 9268 // This test exercises the local live-migration between two Cloud Hypervisor VMs on the 9269 // same host. It ensures the following behaviors: 9270 // 1. The source VM is up and functional (including various virtio-devices are working properly); 9271 // 2. The 'send-migration' and 'receive-migration' command finished successfully; 9272 // 3. The source VM terminated gracefully after live migration; 9273 // 4. The destination VM is functional (including various virtio-devices are working properly) after 9274 // live migration; 9275 // Note: This test does not use vsock as we can't create two identical vsock on the same host. 9276 fn _test_live_migration(upgrade_test: bool, local: bool) { 9277 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 9278 let guest = Guest::new(Box::new(focal)); 9279 let kernel_path = direct_kernel_boot_path(); 9280 let console_text = String::from("On a branch floating down river a cricket, singing."); 9281 let net_id = "net123"; 9282 let net_params = format!( 9283 "id={},tap=,mac={},ip={},mask=255.255.255.0", 9284 net_id, guest.network.guest_mac, guest.network.host_ip 9285 ); 9286 9287 let memory_param: &[&str] = if local { 9288 &["--memory", "size=4G,shared=on"] 9289 } else { 9290 &["--memory", "size=4G"] 9291 }; 9292 9293 let boot_vcpus = 2; 9294 let max_vcpus = 4; 9295 9296 let pmem_temp_file = TempFile::new().unwrap(); 9297 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 9298 std::process::Command::new("mkfs.ext4") 9299 .arg(pmem_temp_file.as_path()) 9300 .output() 9301 .expect("Expect creating disk image to succeed"); 9302 let pmem_path = String::from("/dev/pmem0"); 9303 9304 // Start the source VM 9305 let src_vm_path = if !upgrade_test { 9306 clh_command("cloud-hypervisor") 9307 } else { 9308 cloud_hypervisor_release_path() 9309 }; 9310 let src_api_socket = temp_api_path(&guest.tmp_dir); 9311 let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); 9312 src_vm_cmd 9313 .args([ 9314 "--cpus", 9315 format!("boot={boot_vcpus},max={max_vcpus}").as_str(), 9316 ]) 9317 .args(memory_param) 9318 .args(["--kernel", kernel_path.to_str().unwrap()]) 9319 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 9320 .default_disks() 9321 .args(["--net", net_params.as_str()]) 9322 .args(["--api-socket", &src_api_socket]) 9323 .args([ 9324 "--pmem", 9325 format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), 9326 ]); 9327 let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); 9328 9329 // Start the destination VM 9330 let mut dest_api_socket = temp_api_path(&guest.tmp_dir); 9331 dest_api_socket.push_str(".dest"); 9332 let mut dest_child = GuestCommand::new(&guest) 9333 .args(["--api-socket", &dest_api_socket]) 9334 .capture_output() 9335 .spawn() 9336 .unwrap(); 9337 9338 let r = std::panic::catch_unwind(|| { 9339 guest.wait_vm_boot(None).unwrap(); 9340 9341 // Make sure the source VM is functional 9342 // Check the number of vCPUs 9343 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9344 9345 // Check the guest RAM 9346 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9347 9348 // Check the guest virtio-devices, e.g. block, rng, console, and net 9349 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9350 9351 // x86_64: Following what's done in the `test_snapshot_restore`, we need 9352 // to make sure that removing and adding back the virtio-net device does 9353 // not break the live-migration support for virtio-pci. 9354 #[cfg(target_arch = "x86_64")] 9355 { 9356 assert!(remote_command( 9357 &src_api_socket, 9358 "remove-device", 9359 Some(net_id), 9360 )); 9361 thread::sleep(std::time::Duration::new(10, 0)); 9362 9363 // Plug the virtio-net device again 9364 assert!(remote_command( 9365 &src_api_socket, 9366 "add-net", 9367 Some(net_params.as_str()), 9368 )); 9369 thread::sleep(std::time::Duration::new(10, 0)); 9370 } 9371 9372 // Start the live-migration 9373 let migration_socket = String::from( 9374 guest 9375 .tmp_dir 9376 .as_path() 9377 .join("live-migration.sock") 9378 .to_str() 9379 .unwrap(), 9380 ); 9381 9382 assert!( 9383 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), 9384 "Unsuccessful command: 'send-migration' or 'receive-migration'." 9385 ); 9386 }); 9387 9388 // Check and report any errors occurred during the live-migration 9389 if r.is_err() { 9390 print_and_panic( 9391 src_child, 9392 dest_child, 9393 None, 9394 "Error occurred during live-migration", 9395 ); 9396 } 9397 9398 // Check the source vm has been terminated successful (give it '3s' to settle) 9399 thread::sleep(std::time::Duration::new(3, 0)); 9400 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 9401 print_and_panic( 9402 src_child, 9403 dest_child, 9404 None, 9405 "source VM was not terminated successfully.", 9406 ); 9407 }; 9408 9409 // Post live-migration check to make sure the destination VM is functional 9410 let r = std::panic::catch_unwind(|| { 9411 // Perform same checks to validate VM has been properly migrated 9412 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9413 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9414 9415 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9416 }); 9417 9418 // Clean-up the destination VM and make sure it terminated correctly 9419 let _ = dest_child.kill(); 9420 let dest_output = dest_child.wait_with_output().unwrap(); 9421 handle_child_output(r, &dest_output); 9422 9423 // Check the destination VM has the expected 'console_text' from its output 9424 let r = std::panic::catch_unwind(|| { 9425 assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); 9426 }); 9427 handle_child_output(r, &dest_output); 9428 } 9429 9430 fn _test_live_migration_balloon(upgrade_test: bool, local: bool) { 9431 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 9432 let guest = Guest::new(Box::new(focal)); 9433 let kernel_path = direct_kernel_boot_path(); 9434 let console_text = String::from("On a branch floating down river a cricket, singing."); 9435 let net_id = "net123"; 9436 let net_params = format!( 9437 "id={},tap=,mac={},ip={},mask=255.255.255.0", 9438 net_id, guest.network.guest_mac, guest.network.host_ip 9439 ); 9440 9441 let memory_param: &[&str] = if local { 9442 &[ 9443 "--memory", 9444 "size=4G,hotplug_method=virtio-mem,hotplug_size=8G,shared=on", 9445 "--balloon", 9446 "size=0", 9447 ] 9448 } else { 9449 &[ 9450 "--memory", 9451 "size=4G,hotplug_method=virtio-mem,hotplug_size=8G", 9452 "--balloon", 9453 "size=0", 9454 ] 9455 }; 9456 9457 let boot_vcpus = 2; 9458 let max_vcpus = 4; 9459 9460 let pmem_temp_file = TempFile::new().unwrap(); 9461 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 9462 std::process::Command::new("mkfs.ext4") 9463 .arg(pmem_temp_file.as_path()) 9464 .output() 9465 .expect("Expect creating disk image to succeed"); 9466 let pmem_path = String::from("/dev/pmem0"); 9467 9468 // Start the source VM 9469 let src_vm_path = if !upgrade_test { 9470 clh_command("cloud-hypervisor") 9471 } else { 9472 cloud_hypervisor_release_path() 9473 }; 9474 let src_api_socket = temp_api_path(&guest.tmp_dir); 9475 let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); 9476 src_vm_cmd 9477 .args([ 9478 "--cpus", 9479 format!("boot={boot_vcpus},max={max_vcpus}").as_str(), 9480 ]) 9481 .args(memory_param) 9482 .args(["--kernel", kernel_path.to_str().unwrap()]) 9483 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 9484 .default_disks() 9485 .args(["--net", net_params.as_str()]) 9486 .args(["--api-socket", &src_api_socket]) 9487 .args([ 9488 "--pmem", 9489 format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), 9490 ]); 9491 let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); 9492 9493 // Start the destination VM 9494 let mut dest_api_socket = temp_api_path(&guest.tmp_dir); 9495 dest_api_socket.push_str(".dest"); 9496 let mut dest_child = GuestCommand::new(&guest) 9497 .args(["--api-socket", &dest_api_socket]) 9498 .capture_output() 9499 .spawn() 9500 .unwrap(); 9501 9502 let r = std::panic::catch_unwind(|| { 9503 guest.wait_vm_boot(None).unwrap(); 9504 9505 // Make sure the source VM is functional 9506 // Check the number of vCPUs 9507 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9508 9509 // Check the guest RAM 9510 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9511 // Increase the guest RAM 9512 resize_command(&src_api_socket, None, Some(6 << 30), None, None); 9513 thread::sleep(std::time::Duration::new(5, 0)); 9514 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 9515 // Use balloon to remove RAM from the VM 9516 resize_command(&src_api_socket, None, None, Some(1 << 30), None); 9517 thread::sleep(std::time::Duration::new(5, 0)); 9518 let total_memory = guest.get_total_memory().unwrap_or_default(); 9519 assert!(total_memory > 4_800_000); 9520 assert!(total_memory < 5_760_000); 9521 9522 // Check the guest virtio-devices, e.g. block, rng, console, and net 9523 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9524 9525 // x86_64: Following what's done in the `test_snapshot_restore`, we need 9526 // to make sure that removing and adding back the virtio-net device does 9527 // not break the live-migration support for virtio-pci. 9528 #[cfg(target_arch = "x86_64")] 9529 { 9530 assert!(remote_command( 9531 &src_api_socket, 9532 "remove-device", 9533 Some(net_id), 9534 )); 9535 thread::sleep(std::time::Duration::new(10, 0)); 9536 9537 // Plug the virtio-net device again 9538 assert!(remote_command( 9539 &src_api_socket, 9540 "add-net", 9541 Some(net_params.as_str()), 9542 )); 9543 thread::sleep(std::time::Duration::new(10, 0)); 9544 } 9545 9546 // Start the live-migration 9547 let migration_socket = String::from( 9548 guest 9549 .tmp_dir 9550 .as_path() 9551 .join("live-migration.sock") 9552 .to_str() 9553 .unwrap(), 9554 ); 9555 9556 assert!( 9557 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), 9558 "Unsuccessful command: 'send-migration' or 'receive-migration'." 9559 ); 9560 }); 9561 9562 // Check and report any errors occurred during the live-migration 9563 if r.is_err() { 9564 print_and_panic( 9565 src_child, 9566 dest_child, 9567 None, 9568 "Error occurred during live-migration", 9569 ); 9570 } 9571 9572 // Check the source vm has been terminated successful (give it '3s' to settle) 9573 thread::sleep(std::time::Duration::new(3, 0)); 9574 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 9575 print_and_panic( 9576 src_child, 9577 dest_child, 9578 None, 9579 "source VM was not terminated successfully.", 9580 ); 9581 }; 9582 9583 // Post live-migration check to make sure the destination VM is functional 9584 let r = std::panic::catch_unwind(|| { 9585 // Perform same checks to validate VM has been properly migrated 9586 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9587 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9588 9589 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9590 9591 // Perform checks on guest RAM using balloon 9592 let total_memory = guest.get_total_memory().unwrap_or_default(); 9593 assert!(total_memory > 4_800_000); 9594 assert!(total_memory < 5_760_000); 9595 // Deflate balloon to restore entire RAM to the VM 9596 resize_command(&dest_api_socket, None, None, Some(0), None); 9597 thread::sleep(std::time::Duration::new(5, 0)); 9598 assert!(guest.get_total_memory().unwrap_or_default() > 5_760_000); 9599 // Decrease guest RAM with virtio-mem 9600 resize_command(&dest_api_socket, None, Some(5 << 30), None, None); 9601 thread::sleep(std::time::Duration::new(5, 0)); 9602 let total_memory = guest.get_total_memory().unwrap_or_default(); 9603 assert!(total_memory > 4_800_000); 9604 assert!(total_memory < 5_760_000); 9605 }); 9606 9607 // Clean-up the destination VM and make sure it terminated correctly 9608 let _ = dest_child.kill(); 9609 let dest_output = dest_child.wait_with_output().unwrap(); 9610 handle_child_output(r, &dest_output); 9611 9612 // Check the destination VM has the expected 'console_text' from its output 9613 let r = std::panic::catch_unwind(|| { 9614 assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); 9615 }); 9616 handle_child_output(r, &dest_output); 9617 } 9618 9619 fn _test_live_migration_numa(upgrade_test: bool, local: bool) { 9620 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 9621 let guest = Guest::new(Box::new(focal)); 9622 let kernel_path = direct_kernel_boot_path(); 9623 let console_text = String::from("On a branch floating down river a cricket, singing."); 9624 let net_id = "net123"; 9625 let net_params = format!( 9626 "id={},tap=,mac={},ip={},mask=255.255.255.0", 9627 net_id, guest.network.guest_mac, guest.network.host_ip 9628 ); 9629 9630 let memory_param: &[&str] = if local { 9631 &[ 9632 "--memory", 9633 "size=0,hotplug_method=virtio-mem,shared=on", 9634 "--memory-zone", 9635 "id=mem0,size=1G,hotplug_size=4G,shared=on", 9636 "id=mem1,size=1G,hotplug_size=4G,shared=on", 9637 "id=mem2,size=2G,hotplug_size=4G,shared=on", 9638 "--numa", 9639 "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", 9640 "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", 9641 "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", 9642 ] 9643 } else { 9644 &[ 9645 "--memory", 9646 "size=0,hotplug_method=virtio-mem", 9647 "--memory-zone", 9648 "id=mem0,size=1G,hotplug_size=4G", 9649 "id=mem1,size=1G,hotplug_size=4G", 9650 "id=mem2,size=2G,hotplug_size=4G", 9651 "--numa", 9652 "guest_numa_id=0,cpus=[0-2,9],distances=[1@15,2@20],memory_zones=mem0", 9653 "guest_numa_id=1,cpus=[3-4,6-8],distances=[0@20,2@25],memory_zones=mem1", 9654 "guest_numa_id=2,cpus=[5,10-11],distances=[0@25,1@30],memory_zones=mem2", 9655 ] 9656 }; 9657 9658 let boot_vcpus = 6; 9659 let max_vcpus = 12; 9660 9661 let pmem_temp_file = TempFile::new().unwrap(); 9662 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 9663 std::process::Command::new("mkfs.ext4") 9664 .arg(pmem_temp_file.as_path()) 9665 .output() 9666 .expect("Expect creating disk image to succeed"); 9667 let pmem_path = String::from("/dev/pmem0"); 9668 9669 // Start the source VM 9670 let src_vm_path = if !upgrade_test { 9671 clh_command("cloud-hypervisor") 9672 } else { 9673 cloud_hypervisor_release_path() 9674 }; 9675 let src_api_socket = temp_api_path(&guest.tmp_dir); 9676 let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); 9677 src_vm_cmd 9678 .args([ 9679 "--cpus", 9680 format!("boot={boot_vcpus},max={max_vcpus}").as_str(), 9681 ]) 9682 .args(memory_param) 9683 .args(["--kernel", kernel_path.to_str().unwrap()]) 9684 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 9685 .default_disks() 9686 .args(["--net", net_params.as_str()]) 9687 .args(["--api-socket", &src_api_socket]) 9688 .args([ 9689 "--pmem", 9690 format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), 9691 ]); 9692 let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); 9693 9694 // Start the destination VM 9695 let mut dest_api_socket = temp_api_path(&guest.tmp_dir); 9696 dest_api_socket.push_str(".dest"); 9697 let mut dest_child = GuestCommand::new(&guest) 9698 .args(["--api-socket", &dest_api_socket]) 9699 .capture_output() 9700 .spawn() 9701 .unwrap(); 9702 9703 let r = std::panic::catch_unwind(|| { 9704 guest.wait_vm_boot(None).unwrap(); 9705 9706 // Make sure the source VM is functional 9707 // Check the number of vCPUs 9708 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9709 9710 // Check the guest RAM 9711 assert!(guest.get_total_memory().unwrap_or_default() > 2_880_000); 9712 9713 // Check the guest virtio-devices, e.g. block, rng, console, and net 9714 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9715 9716 // Check the NUMA parameters are applied correctly and resize 9717 // each zone to test the case where we migrate a VM with the 9718 // virtio-mem regions being used. 9719 { 9720 guest.check_numa_common( 9721 Some(&[960_000, 960_000, 1_920_000]), 9722 Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), 9723 Some(&["10 15 20", "20 10 25", "25 30 10"]), 9724 ); 9725 9726 // AArch64 currently does not support hotplug, and therefore we only 9727 // test hotplug-related function on x86_64 here. 9728 #[cfg(target_arch = "x86_64")] 9729 { 9730 guest.enable_memory_hotplug(); 9731 9732 // Resize every memory zone and check each associated NUMA node 9733 // has been assigned the right amount of memory. 9734 resize_zone_command(&src_api_socket, "mem0", "2G"); 9735 resize_zone_command(&src_api_socket, "mem1", "2G"); 9736 resize_zone_command(&src_api_socket, "mem2", "3G"); 9737 thread::sleep(std::time::Duration::new(5, 0)); 9738 9739 guest.check_numa_common(Some(&[1_920_000, 1_920_000, 1_920_000]), None, None); 9740 } 9741 } 9742 9743 // x86_64: Following what's done in the `test_snapshot_restore`, we need 9744 // to make sure that removing and adding back the virtio-net device does 9745 // not break the live-migration support for virtio-pci. 9746 #[cfg(target_arch = "x86_64")] 9747 { 9748 assert!(remote_command( 9749 &src_api_socket, 9750 "remove-device", 9751 Some(net_id), 9752 )); 9753 thread::sleep(std::time::Duration::new(10, 0)); 9754 9755 // Plug the virtio-net device again 9756 assert!(remote_command( 9757 &src_api_socket, 9758 "add-net", 9759 Some(net_params.as_str()), 9760 )); 9761 thread::sleep(std::time::Duration::new(10, 0)); 9762 } 9763 9764 // Start the live-migration 9765 let migration_socket = String::from( 9766 guest 9767 .tmp_dir 9768 .as_path() 9769 .join("live-migration.sock") 9770 .to_str() 9771 .unwrap(), 9772 ); 9773 9774 assert!( 9775 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), 9776 "Unsuccessful command: 'send-migration' or 'receive-migration'." 9777 ); 9778 }); 9779 9780 // Check and report any errors occurred during the live-migration 9781 if r.is_err() { 9782 print_and_panic( 9783 src_child, 9784 dest_child, 9785 None, 9786 "Error occurred during live-migration", 9787 ); 9788 } 9789 9790 // Check the source vm has been terminated successful (give it '3s' to settle) 9791 thread::sleep(std::time::Duration::new(3, 0)); 9792 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 9793 print_and_panic( 9794 src_child, 9795 dest_child, 9796 None, 9797 "source VM was not terminated successfully.", 9798 ); 9799 }; 9800 9801 // Post live-migration check to make sure the destination VM is functional 9802 let r = std::panic::catch_unwind(|| { 9803 // Perform same checks to validate VM has been properly migrated 9804 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9805 #[cfg(target_arch = "x86_64")] 9806 assert!(guest.get_total_memory().unwrap_or_default() > 6_720_000); 9807 #[cfg(target_arch = "aarch64")] 9808 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9809 9810 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9811 9812 // Perform NUMA related checks 9813 { 9814 #[cfg(target_arch = "aarch64")] 9815 { 9816 guest.check_numa_common( 9817 Some(&[960_000, 960_000, 1_920_000]), 9818 Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), 9819 Some(&["10 15 20", "20 10 25", "25 30 10"]), 9820 ); 9821 } 9822 9823 // AArch64 currently does not support hotplug, and therefore we only 9824 // test hotplug-related function on x86_64 here. 9825 #[cfg(target_arch = "x86_64")] 9826 { 9827 guest.check_numa_common( 9828 Some(&[1_920_000, 1_920_000, 2_880_000]), 9829 Some(&[vec![0, 1, 2], vec![3, 4], vec![5]]), 9830 Some(&["10 15 20", "20 10 25", "25 30 10"]), 9831 ); 9832 9833 guest.enable_memory_hotplug(); 9834 9835 // Resize every memory zone and check each associated NUMA node 9836 // has been assigned the right amount of memory. 9837 resize_zone_command(&dest_api_socket, "mem0", "4G"); 9838 resize_zone_command(&dest_api_socket, "mem1", "4G"); 9839 resize_zone_command(&dest_api_socket, "mem2", "4G"); 9840 // Resize to the maximum amount of CPUs and check each NUMA 9841 // node has been assigned the right CPUs set. 9842 resize_command(&dest_api_socket, Some(max_vcpus), None, None, None); 9843 thread::sleep(std::time::Duration::new(5, 0)); 9844 9845 guest.check_numa_common( 9846 Some(&[3_840_000, 3_840_000, 3_840_000]), 9847 Some(&[vec![0, 1, 2, 9], vec![3, 4, 6, 7, 8], vec![5, 10, 11]]), 9848 None, 9849 ); 9850 } 9851 } 9852 }); 9853 9854 // Clean-up the destination VM and make sure it terminated correctly 9855 let _ = dest_child.kill(); 9856 let dest_output = dest_child.wait_with_output().unwrap(); 9857 handle_child_output(r, &dest_output); 9858 9859 // Check the destination VM has the expected 'console_text' from its output 9860 let r = std::panic::catch_unwind(|| { 9861 assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); 9862 }); 9863 handle_child_output(r, &dest_output); 9864 } 9865 9866 fn _test_live_migration_watchdog(upgrade_test: bool, local: bool) { 9867 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 9868 let guest = Guest::new(Box::new(focal)); 9869 let kernel_path = direct_kernel_boot_path(); 9870 let console_text = String::from("On a branch floating down river a cricket, singing."); 9871 let net_id = "net123"; 9872 let net_params = format!( 9873 "id={},tap=,mac={},ip={},mask=255.255.255.0", 9874 net_id, guest.network.guest_mac, guest.network.host_ip 9875 ); 9876 9877 let memory_param: &[&str] = if local { 9878 &["--memory", "size=4G,shared=on"] 9879 } else { 9880 &["--memory", "size=4G"] 9881 }; 9882 9883 let boot_vcpus = 2; 9884 let max_vcpus = 4; 9885 9886 let pmem_temp_file = TempFile::new().unwrap(); 9887 pmem_temp_file.as_file().set_len(128 << 20).unwrap(); 9888 std::process::Command::new("mkfs.ext4") 9889 .arg(pmem_temp_file.as_path()) 9890 .output() 9891 .expect("Expect creating disk image to succeed"); 9892 let pmem_path = String::from("/dev/pmem0"); 9893 9894 // Start the source VM 9895 let src_vm_path = if !upgrade_test { 9896 clh_command("cloud-hypervisor") 9897 } else { 9898 cloud_hypervisor_release_path() 9899 }; 9900 let src_api_socket = temp_api_path(&guest.tmp_dir); 9901 let mut src_vm_cmd = GuestCommand::new_with_binary_path(&guest, &src_vm_path); 9902 src_vm_cmd 9903 .args([ 9904 "--cpus", 9905 format!("boot={boot_vcpus},max={max_vcpus}").as_str(), 9906 ]) 9907 .args(memory_param) 9908 .args(["--kernel", kernel_path.to_str().unwrap()]) 9909 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 9910 .default_disks() 9911 .args(["--net", net_params.as_str()]) 9912 .args(["--api-socket", &src_api_socket]) 9913 .args([ 9914 "--pmem", 9915 format!("file={}", pmem_temp_file.as_path().to_str().unwrap(),).as_str(), 9916 ]) 9917 .args(["--watchdog"]); 9918 let mut src_child = src_vm_cmd.capture_output().spawn().unwrap(); 9919 9920 // Start the destination VM 9921 let mut dest_api_socket = temp_api_path(&guest.tmp_dir); 9922 dest_api_socket.push_str(".dest"); 9923 let mut dest_child = GuestCommand::new(&guest) 9924 .args(["--api-socket", &dest_api_socket]) 9925 .capture_output() 9926 .spawn() 9927 .unwrap(); 9928 9929 let r = std::panic::catch_unwind(|| { 9930 guest.wait_vm_boot(None).unwrap(); 9931 9932 // Make sure the source VM is functional 9933 // Check the number of vCPUs 9934 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 9935 // Check the guest RAM 9936 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 9937 // Check the guest virtio-devices, e.g. block, rng, console, and net 9938 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 9939 // x86_64: Following what's done in the `test_snapshot_restore`, we need 9940 // to make sure that removing and adding back the virtio-net device does 9941 // not break the live-migration support for virtio-pci. 9942 #[cfg(target_arch = "x86_64")] 9943 { 9944 assert!(remote_command( 9945 &src_api_socket, 9946 "remove-device", 9947 Some(net_id), 9948 )); 9949 thread::sleep(std::time::Duration::new(10, 0)); 9950 9951 // Plug the virtio-net device again 9952 assert!(remote_command( 9953 &src_api_socket, 9954 "add-net", 9955 Some(net_params.as_str()), 9956 )); 9957 thread::sleep(std::time::Duration::new(10, 0)); 9958 } 9959 9960 // Enable watchdog and ensure its functional 9961 let expected_reboot_count = 1; 9962 // Enable the watchdog with a 15s timeout 9963 enable_guest_watchdog(&guest, 15); 9964 9965 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 9966 assert_eq!( 9967 guest 9968 .ssh_command("sudo journalctl | grep -c -- \"Watchdog started\"") 9969 .unwrap() 9970 .trim() 9971 .parse::<u32>() 9972 .unwrap_or_default(), 9973 1 9974 ); 9975 // Allow some normal time to elapse to check we don't get spurious reboots 9976 thread::sleep(std::time::Duration::new(40, 0)); 9977 // Check no reboot 9978 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 9979 9980 // Start the live-migration 9981 let migration_socket = String::from( 9982 guest 9983 .tmp_dir 9984 .as_path() 9985 .join("live-migration.sock") 9986 .to_str() 9987 .unwrap(), 9988 ); 9989 9990 assert!( 9991 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), 9992 "Unsuccessful command: 'send-migration' or 'receive-migration'." 9993 ); 9994 }); 9995 9996 // Check and report any errors occurred during the live-migration 9997 if r.is_err() { 9998 print_and_panic( 9999 src_child, 10000 dest_child, 10001 None, 10002 "Error occurred during live-migration", 10003 ); 10004 } 10005 10006 // Check the source vm has been terminated successful (give it '3s' to settle) 10007 thread::sleep(std::time::Duration::new(3, 0)); 10008 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 10009 print_and_panic( 10010 src_child, 10011 dest_child, 10012 None, 10013 "source VM was not terminated successfully.", 10014 ); 10015 }; 10016 10017 // Post live-migration check to make sure the destination VM is functional 10018 let r = std::panic::catch_unwind(|| { 10019 // Perform same checks to validate VM has been properly migrated 10020 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 10021 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 10022 10023 guest.check_devices_common(None, Some(&console_text), Some(&pmem_path)); 10024 10025 // Perform checks on watchdog 10026 let mut expected_reboot_count = 1; 10027 10028 // Allow some normal time to elapse to check we don't get spurious reboots 10029 thread::sleep(std::time::Duration::new(40, 0)); 10030 // Check no reboot 10031 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 10032 10033 // Trigger a panic (sync first). We need to do this inside a screen with a delay so the SSH command returns. 10034 guest.ssh_command("screen -dmS reboot sh -c \"sleep 5; echo s | tee /proc/sysrq-trigger; echo c | sudo tee /proc/sysrq-trigger\"").unwrap(); 10035 // Allow some time for the watchdog to trigger (max 30s) and reboot to happen 10036 guest.wait_vm_boot(Some(50)).unwrap(); 10037 // Check a reboot is triggered by the watchdog 10038 expected_reboot_count += 1; 10039 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 10040 10041 #[cfg(target_arch = "x86_64")] 10042 { 10043 // Now pause the VM and remain offline for 30s 10044 assert!(remote_command(&dest_api_socket, "pause", None)); 10045 thread::sleep(std::time::Duration::new(30, 0)); 10046 assert!(remote_command(&dest_api_socket, "resume", None)); 10047 10048 // Check no reboot 10049 assert_eq!(get_reboot_count(&guest), expected_reboot_count); 10050 } 10051 }); 10052 10053 // Clean-up the destination VM and make sure it terminated correctly 10054 let _ = dest_child.kill(); 10055 let dest_output = dest_child.wait_with_output().unwrap(); 10056 handle_child_output(r, &dest_output); 10057 10058 // Check the destination VM has the expected 'console_text' from its output 10059 let r = std::panic::catch_unwind(|| { 10060 assert!(String::from_utf8_lossy(&dest_output.stdout).contains(&console_text)); 10061 }); 10062 handle_child_output(r, &dest_output); 10063 } 10064 10065 fn _test_live_migration_ovs_dpdk(upgrade_test: bool, local: bool) { 10066 let ovs_focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10067 let ovs_guest = Guest::new(Box::new(ovs_focal)); 10068 10069 let migration_focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10070 let migration_guest = Guest::new(Box::new(migration_focal)); 10071 let src_api_socket = temp_api_path(&migration_guest.tmp_dir); 10072 10073 // Start two VMs that are connected through ovs-dpdk and one of the VMs is the source VM for live-migration 10074 let (mut ovs_child, mut src_child) = 10075 setup_ovs_dpdk_guests(&ovs_guest, &migration_guest, &src_api_socket, upgrade_test); 10076 10077 // Start the destination VM 10078 let mut dest_api_socket = temp_api_path(&migration_guest.tmp_dir); 10079 dest_api_socket.push_str(".dest"); 10080 let mut dest_child = GuestCommand::new(&migration_guest) 10081 .args(["--api-socket", &dest_api_socket]) 10082 .capture_output() 10083 .spawn() 10084 .unwrap(); 10085 10086 let r = std::panic::catch_unwind(|| { 10087 // Give it '1s' to make sure the 'dest_api_socket' file is properly created 10088 thread::sleep(std::time::Duration::new(1, 0)); 10089 10090 // Start the live-migration 10091 let migration_socket = String::from( 10092 migration_guest 10093 .tmp_dir 10094 .as_path() 10095 .join("live-migration.sock") 10096 .to_str() 10097 .unwrap(), 10098 ); 10099 10100 assert!( 10101 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, local), 10102 "Unsuccessful command: 'send-migration' or 'receive-migration'." 10103 ); 10104 }); 10105 10106 // Check and report any errors occurred during the live-migration 10107 if r.is_err() { 10108 print_and_panic( 10109 src_child, 10110 dest_child, 10111 Some(ovs_child), 10112 "Error occurred during live-migration", 10113 ); 10114 } 10115 10116 // Check the source vm has been terminated successful (give it '3s' to settle) 10117 thread::sleep(std::time::Duration::new(3, 0)); 10118 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 10119 print_and_panic( 10120 src_child, 10121 dest_child, 10122 Some(ovs_child), 10123 "source VM was not terminated successfully.", 10124 ); 10125 }; 10126 10127 // Post live-migration check to make sure the destination VM is functional 10128 let r = std::panic::catch_unwind(|| { 10129 // Perform same checks to validate VM has been properly migrated 10130 // Spawn a new netcat listener in the OVS VM 10131 let guest_ip = ovs_guest.network.guest_ip.clone(); 10132 thread::spawn(move || { 10133 ssh_command_ip( 10134 "nc -l 12345", 10135 &guest_ip, 10136 DEFAULT_SSH_RETRIES, 10137 DEFAULT_SSH_TIMEOUT, 10138 ) 10139 .unwrap(); 10140 }); 10141 10142 // Wait for the server to be listening 10143 thread::sleep(std::time::Duration::new(5, 0)); 10144 10145 // And check the connection is still functional after live-migration 10146 migration_guest 10147 .ssh_command("nc -vz 172.100.0.1 12345") 10148 .unwrap(); 10149 }); 10150 10151 // Clean-up the destination VM and OVS VM, and make sure they terminated correctly 10152 let _ = dest_child.kill(); 10153 let _ = ovs_child.kill(); 10154 let dest_output = dest_child.wait_with_output().unwrap(); 10155 let ovs_output = ovs_child.wait_with_output().unwrap(); 10156 10157 cleanup_ovs_dpdk(); 10158 10159 handle_child_output(r, &dest_output); 10160 handle_child_output(Ok(()), &ovs_output); 10161 } 10162 10163 // This test exercises the local live-migration between two Cloud Hypervisor VMs on the 10164 // same host with Landlock enabled on both VMs. The test validates the following: 10165 // 1. The source VM is up and functional 10166 // 2. Ensure Landlock is enabled on source VM by hotplugging a disk. As the path for this 10167 // disk is not known to the source VM this step will fail. 10168 // 3. The 'send-migration' and 'receive-migration' command finished successfully; 10169 // 4. The source VM terminated gracefully after live migration; 10170 // 5. The destination VM is functional after live migration; 10171 // 6. Ensure Landlock is enabled on destination VM by hotplugging a disk. As the path for 10172 // this disk is not known to the destination VM this step will fail. 10173 fn _test_live_migration_with_landlock() { 10174 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10175 let guest = Guest::new(Box::new(focal)); 10176 let kernel_path = direct_kernel_boot_path(); 10177 let net_id = "net123"; 10178 let net_params = format!( 10179 "id={},tap=,mac={},ip={},mask=255.255.255.0", 10180 net_id, guest.network.guest_mac, guest.network.host_ip 10181 ); 10182 10183 let boot_vcpus = 2; 10184 let max_vcpus = 4; 10185 10186 let mut blk_file_path = dirs::home_dir().unwrap(); 10187 blk_file_path.push("workloads"); 10188 blk_file_path.push("blk.img"); 10189 10190 let src_api_socket = temp_api_path(&guest.tmp_dir); 10191 let mut src_child = GuestCommand::new(&guest) 10192 .args([ 10193 "--cpus", 10194 format!("boot={boot_vcpus},max={max_vcpus}").as_str(), 10195 ]) 10196 .args(["--memory", "size=4G,shared=on"]) 10197 .args(["--kernel", kernel_path.to_str().unwrap()]) 10198 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 10199 .default_disks() 10200 .args(["--api-socket", &src_api_socket]) 10201 .args(["--landlock"]) 10202 .args(["--net", net_params.as_str()]) 10203 .args([ 10204 "--landlock-rules", 10205 format!("path={:?},access=rw", guest.tmp_dir.as_path()).as_str(), 10206 ]) 10207 .capture_output() 10208 .spawn() 10209 .unwrap(); 10210 10211 // Start the destination VM 10212 let mut dest_api_socket = temp_api_path(&guest.tmp_dir); 10213 dest_api_socket.push_str(".dest"); 10214 let mut dest_child = GuestCommand::new(&guest) 10215 .args(["--api-socket", &dest_api_socket]) 10216 .capture_output() 10217 .spawn() 10218 .unwrap(); 10219 10220 let r = std::panic::catch_unwind(|| { 10221 guest.wait_vm_boot(None).unwrap(); 10222 10223 // Make sure the source VM is functaionl 10224 // Check the number of vCPUs 10225 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 10226 10227 // Check the guest RAM 10228 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 10229 10230 // Check Landlock is enabled by hot-plugging a disk. 10231 assert!(!remote_command( 10232 &src_api_socket, 10233 "add-disk", 10234 Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), 10235 )); 10236 10237 // Start the live-migration 10238 let migration_socket = String::from( 10239 guest 10240 .tmp_dir 10241 .as_path() 10242 .join("live-migration.sock") 10243 .to_str() 10244 .unwrap(), 10245 ); 10246 10247 assert!( 10248 start_live_migration(&migration_socket, &src_api_socket, &dest_api_socket, true), 10249 "Unsuccessful command: 'send-migration' or 'receive-migration'." 10250 ); 10251 }); 10252 10253 // Check and report any errors occurred during the live-migration 10254 if r.is_err() { 10255 print_and_panic( 10256 src_child, 10257 dest_child, 10258 None, 10259 "Error occurred during live-migration", 10260 ); 10261 } 10262 10263 // Check the source vm has been terminated successful (give it '3s' to settle) 10264 thread::sleep(std::time::Duration::new(3, 0)); 10265 if !src_child.try_wait().unwrap().map_or(false, |s| s.success()) { 10266 print_and_panic( 10267 src_child, 10268 dest_child, 10269 None, 10270 "source VM was not terminated successfully.", 10271 ); 10272 }; 10273 10274 // Post live-migration check to make sure the destination VM is funcational 10275 let r = std::panic::catch_unwind(|| { 10276 // Perform same checks to validate VM has been properly migrated 10277 assert_eq!(guest.get_cpu_count().unwrap_or_default(), boot_vcpus); 10278 assert!(guest.get_total_memory().unwrap_or_default() > 3_840_000); 10279 }); 10280 10281 // Check Landlock is enabled on destination VM by hot-plugging a disk. 10282 assert!(!remote_command( 10283 &dest_api_socket, 10284 "add-disk", 10285 Some(format!("path={},id=test0", blk_file_path.to_str().unwrap()).as_str()), 10286 )); 10287 10288 // Clean-up the destination VM and make sure it terminated correctly 10289 let _ = dest_child.kill(); 10290 let dest_output = dest_child.wait_with_output().unwrap(); 10291 handle_child_output(r, &dest_output); 10292 } 10293 10294 mod live_migration_parallel { 10295 use super::*; 10296 #[test] 10297 fn test_live_migration_basic() { 10298 _test_live_migration(false, false) 10299 } 10300 10301 #[test] 10302 fn test_live_migration_local() { 10303 _test_live_migration(false, true) 10304 } 10305 10306 #[test] 10307 fn test_live_migration_watchdog() { 10308 _test_live_migration_watchdog(false, false) 10309 } 10310 10311 #[test] 10312 fn test_live_migration_watchdog_local() { 10313 _test_live_migration_watchdog(false, true) 10314 } 10315 10316 #[test] 10317 fn test_live_upgrade_basic() { 10318 _test_live_migration(true, false) 10319 } 10320 10321 #[test] 10322 fn test_live_upgrade_local() { 10323 _test_live_migration(true, true) 10324 } 10325 10326 #[test] 10327 fn test_live_upgrade_watchdog() { 10328 _test_live_migration_watchdog(true, false) 10329 } 10330 10331 #[test] 10332 fn test_live_upgrade_watchdog_local() { 10333 _test_live_migration_watchdog(true, true) 10334 } 10335 #[test] 10336 #[cfg(target_arch = "x86_64")] 10337 fn test_live_migration_with_landlock() { 10338 _test_live_migration_with_landlock() 10339 } 10340 } 10341 10342 mod live_migration_sequential { 10343 use super::*; 10344 10345 // NUMA & balloon live migration tests are large so run sequentially 10346 10347 #[test] 10348 fn test_live_migration_balloon() { 10349 _test_live_migration_balloon(false, false) 10350 } 10351 10352 #[test] 10353 fn test_live_migration_balloon_local() { 10354 _test_live_migration_balloon(false, true) 10355 } 10356 10357 #[test] 10358 fn test_live_upgrade_balloon() { 10359 _test_live_migration_balloon(true, false) 10360 } 10361 10362 #[test] 10363 fn test_live_upgrade_balloon_local() { 10364 _test_live_migration_balloon(true, true) 10365 } 10366 10367 #[test] 10368 #[cfg(not(feature = "mshv"))] 10369 fn test_live_migration_numa() { 10370 _test_live_migration_numa(false, false) 10371 } 10372 10373 #[test] 10374 #[cfg(not(feature = "mshv"))] 10375 fn test_live_migration_numa_local() { 10376 _test_live_migration_numa(false, true) 10377 } 10378 10379 #[test] 10380 #[cfg(not(feature = "mshv"))] 10381 fn test_live_upgrade_numa() { 10382 _test_live_migration_numa(true, false) 10383 } 10384 10385 #[test] 10386 #[cfg(not(feature = "mshv"))] 10387 fn test_live_upgrade_numa_local() { 10388 _test_live_migration_numa(true, true) 10389 } 10390 10391 // Require to run ovs-dpdk tests sequentially because they rely on the same ovs-dpdk setup 10392 #[test] 10393 #[ignore = "See #5532"] 10394 #[cfg(target_arch = "x86_64")] 10395 #[cfg(not(feature = "mshv"))] 10396 fn test_live_migration_ovs_dpdk() { 10397 _test_live_migration_ovs_dpdk(false, false); 10398 } 10399 10400 #[test] 10401 #[cfg(target_arch = "x86_64")] 10402 #[cfg(not(feature = "mshv"))] 10403 fn test_live_migration_ovs_dpdk_local() { 10404 _test_live_migration_ovs_dpdk(false, true); 10405 } 10406 10407 #[test] 10408 #[ignore = "See #5532"] 10409 #[cfg(target_arch = "x86_64")] 10410 #[cfg(not(feature = "mshv"))] 10411 fn test_live_upgrade_ovs_dpdk() { 10412 _test_live_migration_ovs_dpdk(true, false); 10413 } 10414 10415 #[test] 10416 #[ignore = "See #5532"] 10417 #[cfg(target_arch = "x86_64")] 10418 #[cfg(not(feature = "mshv"))] 10419 fn test_live_upgrade_ovs_dpdk_local() { 10420 _test_live_migration_ovs_dpdk(true, true); 10421 } 10422 } 10423 } 10424 10425 #[cfg(target_arch = "aarch64")] 10426 mod aarch64_acpi { 10427 use crate::*; 10428 10429 #[test] 10430 fn test_simple_launch_acpi() { 10431 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10432 10433 vec![Box::new(focal)].drain(..).for_each(|disk_config| { 10434 let guest = Guest::new(disk_config); 10435 10436 let mut child = GuestCommand::new(&guest) 10437 .args(["--cpus", "boot=1"]) 10438 .args(["--memory", "size=512M"]) 10439 .args(["--kernel", edk2_path().to_str().unwrap()]) 10440 .default_disks() 10441 .default_net() 10442 .args(["--serial", "tty", "--console", "off"]) 10443 .capture_output() 10444 .spawn() 10445 .unwrap(); 10446 10447 let r = std::panic::catch_unwind(|| { 10448 guest.wait_vm_boot(Some(120)).unwrap(); 10449 10450 assert_eq!(guest.get_cpu_count().unwrap_or_default(), 1); 10451 assert!(guest.get_total_memory().unwrap_or_default() > 400_000); 10452 assert_eq!(guest.get_pci_bridge_class().unwrap_or_default(), "0x060000"); 10453 }); 10454 10455 let _ = child.kill(); 10456 let output = child.wait_with_output().unwrap(); 10457 10458 handle_child_output(r, &output); 10459 }); 10460 } 10461 10462 #[test] 10463 fn test_guest_numa_nodes_acpi() { 10464 _test_guest_numa_nodes(true); 10465 } 10466 10467 #[test] 10468 fn test_cpu_topology_421_acpi() { 10469 test_cpu_topology(4, 2, 1, true); 10470 } 10471 10472 #[test] 10473 fn test_cpu_topology_142_acpi() { 10474 test_cpu_topology(1, 4, 2, true); 10475 } 10476 10477 #[test] 10478 fn test_cpu_topology_262_acpi() { 10479 test_cpu_topology(2, 6, 2, true); 10480 } 10481 10482 #[test] 10483 fn test_power_button_acpi() { 10484 _test_power_button(true); 10485 } 10486 10487 #[test] 10488 fn test_virtio_iommu() { 10489 _test_virtio_iommu(true) 10490 } 10491 } 10492 10493 mod rate_limiter { 10494 use super::*; 10495 10496 // Check if the 'measured' rate is within the expected 'difference' (in percentage) 10497 // compared to given 'limit' rate. 10498 fn check_rate_limit(measured: f64, limit: f64, difference: f64) -> bool { 10499 let upper_limit = limit * (1_f64 + difference); 10500 let lower_limit = limit * (1_f64 - difference); 10501 10502 if measured > lower_limit && measured < upper_limit { 10503 return true; 10504 } 10505 10506 eprintln!( 10507 "\n\n==== Start 'check_rate_limit' failed ==== \ 10508 \n\nmeasured={measured}, , lower_limit={lower_limit}, upper_limit={upper_limit} \ 10509 \n\n==== End 'check_rate_limit' failed ====\n\n" 10510 ); 10511 10512 false 10513 } 10514 10515 fn _test_rate_limiter_net(rx: bool) { 10516 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10517 let guest = Guest::new(Box::new(focal)); 10518 10519 let test_timeout = 10; 10520 let num_queues = 2; 10521 let queue_size = 256; 10522 let bw_size = 10485760_u64; // bytes 10523 let bw_refill_time = 100; // ms 10524 let limit_bps = (bw_size * 8 * 1000) as f64 / bw_refill_time as f64; 10525 10526 let net_params = format!( 10527 "tap=,mac={},ip={},mask=255.255.255.0,num_queues={},queue_size={},bw_size={},bw_refill_time={}", 10528 guest.network.guest_mac, 10529 guest.network.host_ip, 10530 num_queues, 10531 queue_size, 10532 bw_size, 10533 bw_refill_time, 10534 ); 10535 10536 let mut child = GuestCommand::new(&guest) 10537 .args(["--cpus", &format!("boot={}", num_queues / 2)]) 10538 .args(["--memory", "size=4G"]) 10539 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 10540 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 10541 .default_disks() 10542 .args(["--net", net_params.as_str()]) 10543 .capture_output() 10544 .spawn() 10545 .unwrap(); 10546 10547 let r = std::panic::catch_unwind(|| { 10548 guest.wait_vm_boot(None).unwrap(); 10549 let measured_bps = 10550 measure_virtio_net_throughput(test_timeout, num_queues / 2, &guest, rx, true) 10551 .unwrap(); 10552 assert!(check_rate_limit(measured_bps, limit_bps, 0.1)); 10553 }); 10554 10555 let _ = child.kill(); 10556 let output = child.wait_with_output().unwrap(); 10557 handle_child_output(r, &output); 10558 } 10559 10560 #[test] 10561 fn test_rate_limiter_net_rx() { 10562 _test_rate_limiter_net(true); 10563 } 10564 10565 #[test] 10566 fn test_rate_limiter_net_tx() { 10567 _test_rate_limiter_net(false); 10568 } 10569 10570 fn _test_rate_limiter_block(bandwidth: bool, num_queues: u32) { 10571 let test_timeout = 10; 10572 let fio_ops = FioOps::RandRW; 10573 10574 let bw_size = if bandwidth { 10575 10485760_u64 // bytes 10576 } else { 10577 100_u64 // I/O 10578 }; 10579 let bw_refill_time = 100; // ms 10580 let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; 10581 10582 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10583 let guest = Guest::new(Box::new(focal)); 10584 let api_socket = temp_api_path(&guest.tmp_dir); 10585 let test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap(); 10586 let blk_rate_limiter_test_img = 10587 String::from(test_img_dir.as_path().join("blk.img").to_str().unwrap()); 10588 10589 // Create the test block image 10590 assert!(exec_host_command_output(&format!( 10591 "dd if=/dev/zero of={blk_rate_limiter_test_img} bs=1M count=1024" 10592 )) 10593 .status 10594 .success()); 10595 10596 let test_blk_params = if bandwidth { 10597 format!( 10598 "path={blk_rate_limiter_test_img},num_queues={num_queues},bw_size={bw_size},bw_refill_time={bw_refill_time}" 10599 ) 10600 } else { 10601 format!( 10602 "path={blk_rate_limiter_test_img},num_queues={num_queues},ops_size={bw_size},ops_refill_time={bw_refill_time}" 10603 ) 10604 }; 10605 10606 let mut child = GuestCommand::new(&guest) 10607 .args(["--cpus", &format!("boot={num_queues}")]) 10608 .args(["--memory", "size=4G"]) 10609 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 10610 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 10611 .args([ 10612 "--disk", 10613 format!( 10614 "path={}", 10615 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 10616 ) 10617 .as_str(), 10618 format!( 10619 "path={}", 10620 guest.disk_config.disk(DiskType::CloudInit).unwrap() 10621 ) 10622 .as_str(), 10623 test_blk_params.as_str(), 10624 ]) 10625 .default_net() 10626 .args(["--api-socket", &api_socket]) 10627 .capture_output() 10628 .spawn() 10629 .unwrap(); 10630 10631 let r = std::panic::catch_unwind(|| { 10632 guest.wait_vm_boot(None).unwrap(); 10633 10634 let fio_command = format!( 10635 "sudo fio --filename=/dev/vdc --name=test --output-format=json \ 10636 --direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \ 10637 --rw={fio_ops} --runtime={test_timeout} --numjobs={num_queues}" 10638 ); 10639 let output = guest.ssh_command(&fio_command).unwrap(); 10640 10641 // Parse fio output 10642 let measured_rate = if bandwidth { 10643 parse_fio_output(&output, &fio_ops, num_queues).unwrap() 10644 } else { 10645 parse_fio_output_iops(&output, &fio_ops, num_queues).unwrap() 10646 }; 10647 assert!(check_rate_limit(measured_rate, limit_rate, 0.1)); 10648 }); 10649 10650 let _ = child.kill(); 10651 let output = child.wait_with_output().unwrap(); 10652 handle_child_output(r, &output); 10653 } 10654 10655 fn _test_rate_limiter_group_block(bandwidth: bool, num_queues: u32, num_disks: u32) { 10656 let test_timeout = 10; 10657 let fio_ops = FioOps::RandRW; 10658 10659 let bw_size = if bandwidth { 10660 10485760_u64 // bytes 10661 } else { 10662 100_u64 // I/O 10663 }; 10664 let bw_refill_time = 100; // ms 10665 let limit_rate = (bw_size * 1000) as f64 / bw_refill_time as f64; 10666 10667 let focal = UbuntuDiskConfig::new(FOCAL_IMAGE_NAME.to_string()); 10668 let guest = Guest::new(Box::new(focal)); 10669 let api_socket = temp_api_path(&guest.tmp_dir); 10670 let test_img_dir = TempDir::new_with_prefix("/var/tmp/ch").unwrap(); 10671 10672 let rate_limit_group_arg = if bandwidth { 10673 format!("id=group0,bw_size={bw_size},bw_refill_time={bw_refill_time}") 10674 } else { 10675 format!("id=group0,ops_size={bw_size},ops_refill_time={bw_refill_time}") 10676 }; 10677 10678 let mut disk_args = vec![ 10679 "--disk".to_string(), 10680 format!( 10681 "path={}", 10682 guest.disk_config.disk(DiskType::OperatingSystem).unwrap() 10683 ), 10684 format!( 10685 "path={}", 10686 guest.disk_config.disk(DiskType::CloudInit).unwrap() 10687 ), 10688 ]; 10689 10690 for i in 0..num_disks { 10691 let test_img_path = String::from( 10692 test_img_dir 10693 .as_path() 10694 .join(format!("blk{}.img", i)) 10695 .to_str() 10696 .unwrap(), 10697 ); 10698 10699 assert!(exec_host_command_output(&format!( 10700 "dd if=/dev/zero of={test_img_path} bs=1M count=1024" 10701 )) 10702 .status 10703 .success()); 10704 10705 disk_args.push(format!( 10706 "path={test_img_path},num_queues={num_queues},rate_limit_group=group0" 10707 )); 10708 } 10709 10710 let mut child = GuestCommand::new(&guest) 10711 .args(["--cpus", &format!("boot={}", num_queues * num_disks)]) 10712 .args(["--memory", "size=4G"]) 10713 .args(["--kernel", direct_kernel_boot_path().to_str().unwrap()]) 10714 .args(["--cmdline", DIRECT_KERNEL_BOOT_CMDLINE]) 10715 .args(["--rate-limit-group", &rate_limit_group_arg]) 10716 .args(disk_args) 10717 .default_net() 10718 .args(["--api-socket", &api_socket]) 10719 .capture_output() 10720 .spawn() 10721 .unwrap(); 10722 10723 let r = std::panic::catch_unwind(|| { 10724 guest.wait_vm_boot(None).unwrap(); 10725 10726 let mut fio_command = format!( 10727 "sudo fio --name=global --output-format=json \ 10728 --direct=1 --bs=4k --ioengine=io_uring --iodepth=64 \ 10729 --rw={fio_ops} --runtime={test_timeout} --numjobs={num_queues}" 10730 ); 10731 10732 // Generate additional argument for each disk: 10733 // --name=job0 --filename=/dev/vdc \ 10734 // --name=job1 --filename=/dev/vdd \ 10735 // --name=job2 --filename=/dev/vde \ 10736 // ... 10737 for i in 0..num_disks { 10738 let c: char = 'c'; 10739 let arg = format!( 10740 " --name=job{i} --filename=/dev/vd{}", 10741 char::from_u32((c as u32) + i).unwrap() 10742 ); 10743 fio_command += &arg; 10744 } 10745 let output = guest.ssh_command(&fio_command).unwrap(); 10746 10747 // Parse fio output 10748 let measured_rate = if bandwidth { 10749 parse_fio_output(&output, &fio_ops, num_queues * num_disks).unwrap() 10750 } else { 10751 parse_fio_output_iops(&output, &fio_ops, num_queues * num_disks).unwrap() 10752 }; 10753 assert!(check_rate_limit(measured_rate, limit_rate, 0.2)); 10754 }); 10755 10756 let _ = child.kill(); 10757 let output = child.wait_with_output().unwrap(); 10758 handle_child_output(r, &output); 10759 } 10760 10761 #[test] 10762 fn test_rate_limiter_block_bandwidth() { 10763 _test_rate_limiter_block(true, 1); 10764 _test_rate_limiter_block(true, 2) 10765 } 10766 10767 #[test] 10768 fn test_rate_limiter_group_block_bandwidth() { 10769 _test_rate_limiter_group_block(true, 1, 1); 10770 _test_rate_limiter_group_block(true, 2, 1); 10771 _test_rate_limiter_group_block(true, 1, 2); 10772 _test_rate_limiter_group_block(true, 2, 2); 10773 } 10774 10775 #[test] 10776 fn test_rate_limiter_block_iops() { 10777 _test_rate_limiter_block(false, 1); 10778 _test_rate_limiter_block(false, 2); 10779 } 10780 10781 #[test] 10782 fn test_rate_limiter_group_block_iops() { 10783 _test_rate_limiter_group_block(false, 1, 1); 10784 _test_rate_limiter_group_block(false, 2, 1); 10785 _test_rate_limiter_group_block(false, 1, 2); 10786 _test_rate_limiter_group_block(false, 2, 2); 10787 } 10788 } 10789