From 01e2826f26d100795d94a17cd6ac3e04be445a60 Mon Sep 17 00:00:00 2001 From: Anatol Belski Date: Thu, 13 May 2021 17:25:05 +0200 Subject: [PATCH] tests: Implement disk hotplug test Signed-off-by: Anatol Belski --- docs/windows.md | 2 + tests/integration.rs | 262 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 263 insertions(+), 1 deletion(-) diff --git a/docs/windows.md b/docs/windows.md index bf9f4b42c..c9b0861c9 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -192,6 +192,8 @@ RAM hotplug is supported. Note, that while the `pnpmem.sys` driver in use suppor Network device hotplug and hot-remove are supported. +Disk hotplug and hot-remove are supported. After the device has been hotplugged, it will need to be onlined from within the guest. Among other tools, powershell applets `Get-Disk` and `Set-Disk` can be used for the disk configuration and activation. + ## Debugging The Windows guest debugging process relies heavily on QEMU and [socat](http://www.dest-unreach.org/socat/). The procedure requires two Windows VMs: diff --git a/tests/integration.rs b/tests/integration.rs index 74abef3fe..9a90463d6 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -5351,6 +5351,20 @@ mod tests { .unwrap_or(0); } + fn disk_count(&self) -> u8 { + return ssh_command_ip_with_auth( + "powershell -Command \"Get-Disk | Measure-Object -Line | Format-Table -HideTableHeaders\"", + &self.auth, + &self.guest.network.guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap() + .trim() + .parse::() + .unwrap_or(0); + } + fn reboot(&self) { ssh_command_ip_with_auth( "shutdown /r /t 0", @@ -5396,6 +5410,141 @@ mod tests { .spawn() .unwrap() } + + // XXX Follow up test involving multiple disks will require: + // - Make image size variable + // - Make image filename random + // - Cleanup image file after test + // - NTFS should be added for use along with FAT for better coverage, needs mkfs.ntfs in the container. + fn disk_new(&self) -> String { + let img = PathBuf::from( + String::from_utf8_lossy(b"/tmp/test-fat-hotplug-0.raw").to_string(), + ); + let _ = fs::remove_file(&img); + + // Create an image file + let out = Command::new("qemu-img") + .args(&["create", "-f", "raw", &img.to_str().unwrap(), "100m"]) + .output() + .expect("qemu-img command failed") + .stdout; + println!("{:?}", out); + + // Associate image to a loop device + let out = Command::new("losetup") + .args(&["--show", "-f", &img.to_str().unwrap()]) + .output() + .expect("failed to create loop device") + .stdout; + let _tmp = String::from_utf8_lossy(&out); + let loop_dev = _tmp.trim(); + println!("{:?}", out); + + // Create a partition table + // echo 'type=7' | sudo sfdisk "${LOOP}" + let mut child = Command::new("sfdisk") + .args(&[loop_dev]) + .stdin(Stdio::piped()) + .spawn() + .unwrap(); + let stdin = child.stdin.as_mut().expect("failed to open stdin"); + let _ = stdin + .write_all("type=7".as_bytes()) + .expect("failed to write stdin"); + let out = child.wait_with_output().expect("sfdisk failed").stdout; + println!("{:?}", out); + + // Disengage the loop device + let out = Command::new("losetup") + .args(&["-d", &loop_dev]) + .output() + .expect("loop device not found") + .stdout; + println!("{:?}", out); + + // Re-associate loop device pointing to the partition only + let out = Command::new("losetup") + .args(&[ + "--show", + "--offset", + (512 * 2048).to_string().as_str(), + "-f", + &img.to_str().unwrap(), + ]) + .output() + .expect("failed to create loop device") + .stdout; + let _tmp = String::from_utf8_lossy(&out); + let loop_dev = _tmp.trim(); + println!("{:?}", out); + + // Create msdos filesystem. + // XXX mkfs.ntfs is missing in the docker image and should be added + // For mkfs.ntfs also add -f. + let out = Command::new("mkfs.msdos") + .args(&[&loop_dev]) + .output() + .expect("mkfs.msdos failed") + .stdout; + println!("{:?}", out); + + // Disengage the loop device + let out = Command::new("losetup") + .args(&["-d", &loop_dev]) + .output() + .expect("loop device not found") + .stdout; + println!("{:?}", out); + + img.to_str().unwrap().to_string() + } + + fn disks_set_rw(&self) { + ssh_command_ip_with_auth( + "powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsReadOnly $False\"", + &self.auth, + &self.guest.network.guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + } + + fn disks_online(&self) { + ssh_command_ip_with_auth( + "powershell -Command \"Get-Disk | Where-Object IsOffline -eq $True | Set-Disk -IsOffline $False\"", + &self.auth, + &self.guest.network.guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + } + + fn disk_file_put(&self, fname: &str, data: &str) { + ssh_command_ip_with_auth( + &format!( + "powershell -Command \"'{}' | Set-Content -Path {}\"", + data, fname + ), + &self.auth, + &self.guest.network.guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap(); + } + + fn disk_file_read(&self, fname: &str) -> String { + ssh_command_ip_with_auth( + &format!("powershell -Command \"Get-Content -Path {}\"", fname), + &self.auth, + &self.guest.network.guest_ip, + DEFAULT_SSH_RETRIES, + DEFAULT_SSH_TIMEOUT, + ) + .unwrap() + } } fn vcpu_threads_count(pid: u32) -> u8 { @@ -5422,6 +5571,20 @@ mod tests { n } + fn disk_ctrl_threads_count(pid: u32) -> u8 { + // ps -T -p 15782 | grep "_disk[0-9]*_q0" | wc -l + let out = Command::new("ps") + .args(&["-T", "-p", format!("{}", pid).as_str()]) + .output() + .expect("ps command failed") + .stdout; + let mut n = 0; + String::from_utf8_lossy(&out) + .split_whitespace() + .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 + n + } + #[test] fn test_windows_guest() { let windows_guest = WindowsGuest::new(); @@ -5795,7 +5958,7 @@ mod tests { let mut child = GuestCommand::new(windows_guest.guest()) .args(&["--api-socket", &api_socket]) .args(&["--cpus", "boot=2,kvm_hyperv=on"]) - .args(&["--memory", "size=2G,hotplug_size=5G"]) + .args(&["--memory", "size=4G"]) .args(&["--kernel", ovmf_path.to_str().unwrap()]) .args(&["--serial", "tty"]) .args(&["--console", "off"]) @@ -5854,6 +6017,103 @@ mod tests { handle_child_output(r, &output); } + + #[test] + #[cfg(not(feature = "mshv"))] + fn test_windows_guest_disk_hotplug() { + let windows_guest = WindowsGuest::new(); + + let mut ovmf_path = dirs::home_dir().unwrap(); + ovmf_path.push("workloads"); + ovmf_path.push(OVMF_NAME); + + let tmp_dir = TempDir::new_with_prefix("/tmp/ch").unwrap(); + let api_socket = temp_api_path(&tmp_dir); + + let mut child = GuestCommand::new(windows_guest.guest()) + .args(&["--api-socket", &api_socket]) + .args(&["--cpus", "boot=2,kvm_hyperv=on"]) + .args(&["--memory", "size=4G"]) + .args(&["--kernel", ovmf_path.to_str().unwrap()]) + .args(&["--serial", "tty"]) + .args(&["--console", "off"]) + .default_disks() + .default_net() + .capture_output() + .spawn() + .unwrap(); + + // Wait to make sure Windows boots up + thread::sleep(std::time::Duration::new(60, 0)); + + let mut child_dnsmasq = windows_guest.run_dnsmasq(); + // Give some time for the guest to reach dnsmasq and get + // assigned the right IP address. + thread::sleep(std::time::Duration::new(30, 0)); + + let disk = windows_guest.disk_new(); + + let r = std::panic::catch_unwind(|| { + // Initially present disk device + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + + // Hotplug disk device + let (cmd_success, cmd_output) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={},readonly=off", disk).as_str()), + ); + assert!(cmd_success); + assert!(String::from_utf8_lossy(&cmd_output).contains("\"id\":\"_disk2\"")); + thread::sleep(std::time::Duration::new(5, 0)); + // Online disk device + windows_guest.disks_set_rw(); + windows_guest.disks_online(); + // Verify the device is on the system + let disk_num = 2; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + + let data = "hello"; + let fname = "d:\\world"; + windows_guest.disk_file_put(fname, data); + + // Unmount disk device + let cmd_success = remote_command(&api_socket, "remove-device", Some("_disk2")); + assert!(cmd_success); + thread::sleep(std::time::Duration::new(5, 0)); + // Verify the device has been removed + let disk_num = 1; + assert_eq!(windows_guest.disk_count(), disk_num); + assert_eq!(disk_ctrl_threads_count(child.id()), disk_num); + + // Remount and check the file exists with the expected contents + let (cmd_success, _cmd_output) = remote_command_w_output( + &api_socket, + "add-disk", + Some(format!("path={},readonly=off", disk).as_str()), + ); + assert!(cmd_success); + thread::sleep(std::time::Duration::new(5, 0)); + let out = windows_guest.disk_file_read(fname); + assert_eq!(data, out.trim()); + + // Intentionally no unmount, it'll happen at shutdown. + + windows_guest.shutdown(); + }); + + let _ = child.wait_timeout(std::time::Duration::from_secs(60)); + let _ = child.kill(); + let output = child.wait_with_output().unwrap(); + + let _ = child_dnsmasq.kill(); + let _ = child_dnsmasq.wait(); + + handle_child_output(r, &output); + } } #[cfg(target_arch = "x86_64")]