microsoft/openvmm
Publicmirrored from https://github.com/microsoft/openvmmAvailable
petri/src/vm/construct.rs
993lines · modecode
| 1 | // Copyright (c) Microsoft Corporation. |
| 2 | // Licensed under the MIT License. |
| 3 | |
| 4 | //! Contains [`PetriVmConfig::new`], which builds a [`PetriVmConfig`] with all |
| 5 | //! default settings for a given [`Firmware`] and [`MachineArch`]. |
| 6 | |
| 7 | use crate::linux_direct_serial_agent::LinuxDirectSerialAgent; |
| 8 | use crate::openhcl_diag::OpenHclDiagHandler; |
| 9 | use crate::tracing::trace_attachment; |
| 10 | use crate::vm::PetriVmResources; |
| 11 | use crate::Firmware; |
| 12 | use crate::PcatGuest; |
| 13 | use crate::PetriVmConfig; |
| 14 | use crate::UefiGuest; |
| 15 | use crate::BOOT_NVME_INSTANCE; |
| 16 | use crate::BOOT_NVME_LUN; |
| 17 | use crate::BOOT_NVME_NSID; |
| 18 | use crate::SCSI_INSTANCE; |
| 19 | use crate::SIZE_1_GB; |
| 20 | use anyhow::Context; |
| 21 | use disk_backend_resources::RamDiffDiskHandle; |
| 22 | use framebuffer::Framebuffer; |
| 23 | use framebuffer::FramebufferAccess; |
| 24 | use framebuffer::FRAMEBUFFER_SIZE; |
| 25 | use fs_err::File; |
| 26 | use futures::io::BufReader; |
| 27 | use futures::AsyncBufReadExt; |
| 28 | use futures::AsyncRead; |
| 29 | use futures::AsyncReadExt; |
| 30 | use get_resources::ged::FirmwareEvent; |
| 31 | use guid::Guid; |
| 32 | use hvlite_defs::config::Config; |
| 33 | use hvlite_defs::config::DeviceVtl; |
| 34 | use hvlite_defs::config::HypervisorConfig; |
| 35 | use hvlite_defs::config::LateMapVtl0MemoryPolicy; |
| 36 | use hvlite_defs::config::LoadMode; |
| 37 | use hvlite_defs::config::MemoryConfig; |
| 38 | use hvlite_defs::config::ProcessorTopologyConfig; |
| 39 | use hvlite_defs::config::SerialInformation; |
| 40 | use hvlite_defs::config::VmbusConfig; |
| 41 | use hvlite_defs::config::VpciDeviceConfig; |
| 42 | use hvlite_defs::config::Vtl2BaseAddressType; |
| 43 | use hvlite_defs::config::Vtl2Config; |
| 44 | use hvlite_defs::config::DEFAULT_MMIO_GAPS; |
| 45 | use hvlite_defs::config::DEFAULT_MMIO_GAPS_WITH_VTL2; |
| 46 | use hvlite_defs::config::DEFAULT_PCAT_BOOT_ORDER; |
| 47 | use hvlite_helpers::crash_dump::spawn_dump_handler; |
| 48 | use hvlite_helpers::disk::open_disk_type; |
| 49 | use hvlite_pcat_locator::RomFileLocation; |
| 50 | use hyperv_ic_resources::shutdown::ShutdownIcHandle; |
| 51 | use ide_resources::GuestMedia; |
| 52 | use ide_resources::IdeDeviceConfig; |
| 53 | use nvme_resources::NamespaceDefinition; |
| 54 | use nvme_resources::NvmeControllerHandle; |
| 55 | use pal_async::socket::PolledSocket; |
| 56 | use pal_async::task::Spawn; |
| 57 | use pal_async::task::Task; |
| 58 | use pal_async::DefaultDriver; |
| 59 | use petri_artifacts_common::artifacts as common_artifacts; |
| 60 | use petri_artifacts_common::tags::MachineArch; |
| 61 | use petri_artifacts_core::AsArtifactHandle; |
| 62 | use petri_artifacts_core::TestArtifacts; |
| 63 | use petri_artifacts_vmm_test::artifacts as hvlite_artifacts; |
| 64 | use pipette_client::PIPETTE_VSOCK_PORT; |
| 65 | use scsidisk_resources::SimpleScsiDiskHandle; |
| 66 | use scsidisk_resources::SimpleScsiDvdHandle; |
| 67 | use serial_16550_resources::ComPort; |
| 68 | use serial_core::resources::DisconnectedSerialBackendHandle; |
| 69 | use serial_socket::unix::OpenUnixStreamSerialConfig; |
| 70 | use sparse_mmap::alloc_shared_memory; |
| 71 | use std::fmt::Write as _; |
| 72 | use std::io::Write; |
| 73 | use std::path::PathBuf; |
| 74 | use std::sync::Arc; |
| 75 | use storvsp_resources::ScsiControllerHandle; |
| 76 | use storvsp_resources::ScsiDeviceAndPath; |
| 77 | use storvsp_resources::ScsiPath; |
| 78 | use uidevices_resources::SynthVideoHandle; |
| 79 | use unix_socket::UnixListener; |
| 80 | use unix_socket::UnixStream; |
| 81 | use video_core::SharedFramebufferHandle; |
| 82 | use vm_manifest_builder::VmManifestBuilder; |
| 83 | use vm_resource::kind::SerialBackendHandle; |
| 84 | use vm_resource::kind::VmbusDeviceHandleKind; |
| 85 | use vm_resource::IntoResource; |
| 86 | use vm_resource::Resource; |
| 87 | use vmbus_serial_resources::VmbusSerialDeviceHandle; |
| 88 | use vmbus_serial_resources::VmbusSerialPort; |
| 89 | use vtl2_settings_proto::Vtl2Settings; |
| 90 | |
| 91 | impl PetriVmConfig { |
| 92 | /// Create a new VM configuration. |
| 93 | pub fn new( |
| 94 | firmware: Firmware, |
| 95 | arch: MachineArch, |
| 96 | resolver: TestArtifacts, |
| 97 | driver: &DefaultDriver, |
| 98 | ) -> anyhow::Result<Self> { |
| 99 | // Use the current thread name for the test name, both cargo-test and |
| 100 | // cargo-nextest set this. |
| 101 | // FUTURE: If we ever want to use petri outside a testing context this |
| 102 | // will need to be revisited. |
| 103 | let current_thread = std::thread::current(); |
| 104 | let test_name = current_thread.name().context("no thread name configured")?; |
| 105 | if test_name.is_empty() { |
| 106 | anyhow::bail!("thread name is empty"); |
| 107 | } |
| 108 | if test_name == "main" { |
| 109 | anyhow::bail!("thread name is 'main', not running from test thread"); |
| 110 | } |
| 111 | // Windows paths can't include colons, replace them. |
| 112 | let test_name = test_name.replace("::", "__"); |
| 113 | let setup = PetriVmConfigSetupCore { |
| 114 | test_name: &test_name, |
| 115 | arch, |
| 116 | firmware: &firmware, |
| 117 | resolver: &resolver, |
| 118 | driver, |
| 119 | }; |
| 120 | |
| 121 | let TestLogFiles { |
| 122 | output_dir, |
| 123 | hvlite_file, |
| 124 | guest_file, |
| 125 | petri_file, |
| 126 | openhcl_file, |
| 127 | } = setup |
| 128 | .create_log_files() |
| 129 | .context("failed to create test log files")?; |
| 130 | |
| 131 | crate::tracing::try_init_tracing(petri_file.into())?; |
| 132 | |
| 133 | let mut chipset = VmManifestBuilder::new( |
| 134 | match firmware { |
| 135 | Firmware::LinuxDirect {} => { |
| 136 | vm_manifest_builder::BaseChipsetType::HyperVGen2LinuxDirect |
| 137 | } |
| 138 | Firmware::OpenhclLinuxDirect { .. } => { |
| 139 | vm_manifest_builder::BaseChipsetType::HclHost |
| 140 | } |
| 141 | Firmware::OpenhclUefi { .. } => vm_manifest_builder::BaseChipsetType::HclHost, |
| 142 | Firmware::Pcat { .. } => vm_manifest_builder::BaseChipsetType::HypervGen1, |
| 143 | Firmware::Uefi { .. } => vm_manifest_builder::BaseChipsetType::HypervGen2Uefi, |
| 144 | }, |
| 145 | match arch { |
| 146 | MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64, |
| 147 | MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64, |
| 148 | }, |
| 149 | ); |
| 150 | |
| 151 | let load_mode = setup.load_firmware()?; |
| 152 | |
| 153 | let SerialData { |
| 154 | mut emulated_serial_config, |
| 155 | serial_tasks, |
| 156 | linux_direct_serial_agent, |
| 157 | } = setup.configure_serial(guest_file, openhcl_file)?; |
| 158 | |
| 159 | let (video_dev, framebuffer, framebuffer_access) = match setup.config_video()? { |
| 160 | Some((v, fb, fba)) => { |
| 161 | chipset = chipset.with_framebuffer(); |
| 162 | (Some(v), Some(fb), Some(fba)) |
| 163 | } |
| 164 | None => (None, None, None), |
| 165 | }; |
| 166 | |
| 167 | let mut devices = Vec::new(); |
| 168 | |
| 169 | let (firmware_event_send, firmware_event_recv) = mesh::mpsc_channel(); |
| 170 | |
| 171 | let (with_vtl2, vtl2_vmbus, openhcl_diag_handler, ged, ged_send, mut vtl2_settings) = |
| 172 | if firmware.is_openhcl() { |
| 173 | let (ged, ged_send) = setup.config_openhcl_vmbus_devices( |
| 174 | &mut emulated_serial_config, |
| 175 | &mut devices, |
| 176 | &firmware_event_send, |
| 177 | framebuffer.is_some(), |
| 178 | )?; |
| 179 | let (vtl2_vsock_listener, vtl2_vsock_path) = |
| 180 | tempfile_helpers::with_temp_path(|path| UnixListener::bind(path))?; |
| 181 | let ged_send = Arc::new(ged_send); |
| 182 | ( |
| 183 | Some(Vtl2Config { |
| 184 | vtl0_alias_map: false, // TODO: enable when OpenVMM supports it for DMA |
| 185 | late_map_vtl0_memory: Some(LateMapVtl0MemoryPolicy::InjectException), |
| 186 | vtl2_emulates_apic: false, |
| 187 | }), |
| 188 | Some(VmbusConfig { |
| 189 | vsock_listener: Some(vtl2_vsock_listener), |
| 190 | vsock_path: Some(vtl2_vsock_path.to_string_lossy().into_owned()), |
| 191 | vmbus_max_version: None, |
| 192 | vtl2_redirect: false, |
| 193 | #[cfg(windows)] |
| 194 | vmbusproxy_handle: None, |
| 195 | }), |
| 196 | Some(OpenHclDiagHandler { |
| 197 | ged_send: ged_send.clone(), |
| 198 | vtl2_vsock_path, |
| 199 | }), |
| 200 | Some(ged), |
| 201 | Some(ged_send), |
| 202 | // Basic sane default |
| 203 | Some(Vtl2Settings { |
| 204 | version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(), |
| 205 | dynamic: Some(Default::default()), |
| 206 | fixed: Some(Default::default()), |
| 207 | namespace_settings: Default::default(), |
| 208 | }), |
| 209 | ) |
| 210 | } else { |
| 211 | (None, None, None, None, None, None) |
| 212 | }; |
| 213 | |
| 214 | setup.load_boot_disk(&mut devices, vtl2_settings.as_mut())?; |
| 215 | let expected_boot_event = setup.get_expected_boot_event(); |
| 216 | |
| 217 | // Configure the serial ports now that they have been updated by the |
| 218 | // OpenHCL configuration. |
| 219 | chipset = chipset.with_serial(emulated_serial_config); |
| 220 | // Set so that we don't pull serial data until the guest is |
| 221 | // ready. Otherwise, Linux will drop the input serial data |
| 222 | // on the floor during boot. |
| 223 | if matches!(firmware, Firmware::LinuxDirect { .. }) { |
| 224 | chipset = chipset.with_serial_wait_for_rts(); |
| 225 | } |
| 226 | |
| 227 | // Partition the devices by type. |
| 228 | let mut vmbus_devices = Vec::new(); |
| 229 | let mut ide_disks = Vec::new(); |
| 230 | let floppy_disks = Vec::new(); |
| 231 | let mut vpci_devices = Vec::new(); |
| 232 | for d in devices { |
| 233 | match d { |
| 234 | Device::Vmbus(vtl, resource) => vmbus_devices.push((vtl, resource)), |
| 235 | Device::Vpci(c) => vpci_devices.push(c), |
| 236 | Device::Ide(c) => ide_disks.push(c), |
| 237 | } |
| 238 | } |
| 239 | |
| 240 | // Extract video configuration |
| 241 | let vga_firmware = match video_dev { |
| 242 | Some(VideoDevice::Vga(firmware)) => Some(firmware), |
| 243 | Some(VideoDevice::Synth(vtl, resource)) => { |
| 244 | vmbus_devices.push((vtl, resource)); |
| 245 | None |
| 246 | } |
| 247 | None => None, |
| 248 | }; |
| 249 | |
| 250 | // Add the Hyper-V Shutdown IC |
| 251 | let (shutdown_ic_send, shutdown_ic_recv) = mesh::channel(); |
| 252 | vmbus_devices.push(( |
| 253 | DeviceVtl::Vtl0, |
| 254 | ShutdownIcHandle { |
| 255 | recv: shutdown_ic_recv, |
| 256 | } |
| 257 | .into_resource(), |
| 258 | )); |
| 259 | |
| 260 | // Make a vmbus vsock path for pipette connections |
| 261 | let (vmbus_vsock_listener, vmbus_vsock_path) = |
| 262 | tempfile_helpers::with_temp_path(|path| UnixListener::bind(path))?; |
| 263 | |
| 264 | let chipset = chipset |
| 265 | .build() |
| 266 | .context("failed to build chipset configuration")?; |
| 267 | |
| 268 | let config = Config { |
| 269 | // Firmware |
| 270 | load_mode, |
| 271 | firmware_event_send: Some(firmware_event_send), |
| 272 | |
| 273 | // CPU and RAM |
| 274 | memory: MemoryConfig { |
| 275 | mem_size: if firmware.is_openhcl() { |
| 276 | 4 * SIZE_1_GB |
| 277 | } else { |
| 278 | SIZE_1_GB |
| 279 | }, |
| 280 | mmio_gaps: if firmware.is_openhcl() { |
| 281 | DEFAULT_MMIO_GAPS_WITH_VTL2.into() |
| 282 | } else { |
| 283 | DEFAULT_MMIO_GAPS.into() |
| 284 | }, |
| 285 | prefetch_memory: false, |
| 286 | }, |
| 287 | processor_topology: ProcessorTopologyConfig { |
| 288 | proc_count: 2, |
| 289 | vps_per_socket: None, |
| 290 | enable_smt: None, |
| 291 | arch: Default::default(), |
| 292 | }, |
| 293 | |
| 294 | // Base chipset |
| 295 | chipset: chipset.chipset, |
| 296 | chipset_devices: chipset.chipset_devices, |
| 297 | |
| 298 | // Basic virtualization device support |
| 299 | hypervisor: HypervisorConfig { |
| 300 | with_hv: true, |
| 301 | user_mode_hv_enlightenments: false, |
| 302 | user_mode_apic: false, |
| 303 | with_vtl2, |
| 304 | with_isolation: firmware.isolation(), |
| 305 | }, |
| 306 | vmbus: Some(VmbusConfig { |
| 307 | vsock_listener: Some(vmbus_vsock_listener), |
| 308 | vsock_path: Some(vmbus_vsock_path.to_string_lossy().into_owned()), |
| 309 | vmbus_max_version: None, |
| 310 | vtl2_redirect: false, |
| 311 | #[cfg(windows)] |
| 312 | vmbusproxy_handle: None, |
| 313 | }), |
| 314 | vtl2_vmbus, |
| 315 | |
| 316 | // Devices |
| 317 | floppy_disks, |
| 318 | ide_disks, |
| 319 | vpci_devices, |
| 320 | vmbus_devices, |
| 321 | |
| 322 | // Video support |
| 323 | framebuffer, |
| 324 | vga_firmware, |
| 325 | |
| 326 | // Reasonable defaults |
| 327 | custom_uefi_vars: Default::default(), |
| 328 | |
| 329 | // Disabled for VMM tests by default |
| 330 | #[cfg(windows)] |
| 331 | kernel_vmnics: vec![], |
| 332 | input: mesh::MpscReceiver::new(), |
| 333 | vtl2_gfx: false, |
| 334 | virtio_console_pci: false, |
| 335 | virtio_serial: None, |
| 336 | virtio_devices: vec![], |
| 337 | #[cfg(windows)] |
| 338 | vpci_resources: vec![], |
| 339 | vmgs_file: None, |
| 340 | secure_boot_enabled: false, |
| 341 | debugger_rpc: None, |
| 342 | generation_id_recv: None, |
| 343 | }; |
| 344 | |
| 345 | // Make the pipette connection listener. |
| 346 | let path = config.vmbus.as_ref().unwrap().vsock_path.as_ref().unwrap(); |
| 347 | let path = format!("{path}_{PIPETTE_VSOCK_PORT}"); |
| 348 | let pipette_listener = PolledSocket::new( |
| 349 | driver, |
| 350 | UnixListener::bind(path).context("failed to bind to pipette listener")?, |
| 351 | )?; |
| 352 | |
| 353 | // Make the vtl2 pipette connection listener. |
| 354 | let vtl2_pipette_listener = if let Some(vtl2_vmbus) = &config.vtl2_vmbus { |
| 355 | let path = vtl2_vmbus.vsock_path.as_ref().unwrap(); |
| 356 | let path = format!("{path}_{PIPETTE_VSOCK_PORT}"); |
| 357 | Some(PolledSocket::new( |
| 358 | driver, |
| 359 | UnixListener::bind(path).context("failed to bind to vtl2 pipette listener")?, |
| 360 | )?) |
| 361 | } else { |
| 362 | None |
| 363 | }; |
| 364 | |
| 365 | Ok(Self { |
| 366 | firmware, |
| 367 | arch, |
| 368 | config, |
| 369 | |
| 370 | resources: PetriVmResources { |
| 371 | serial_tasks, |
| 372 | firmware_event_recv, |
| 373 | shutdown_ic_send, |
| 374 | expected_boot_event, |
| 375 | ged_send, |
| 376 | pipette_listener, |
| 377 | vtl2_pipette_listener, |
| 378 | openhcl_diag_handler, |
| 379 | linux_direct_serial_agent, |
| 380 | driver: driver.clone(), |
| 381 | resolver, |
| 382 | output_dir, |
| 383 | }, |
| 384 | |
| 385 | hvlite_log_file: hvlite_file, |
| 386 | |
| 387 | ged, |
| 388 | vtl2_settings, |
| 389 | framebuffer_access, |
| 390 | }) |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | struct PetriVmConfigSetupCore<'a> { |
| 395 | test_name: &'a str, |
| 396 | arch: MachineArch, |
| 397 | firmware: &'a Firmware, |
| 398 | resolver: &'a TestArtifacts, |
| 399 | driver: &'a DefaultDriver, |
| 400 | } |
| 401 | |
| 402 | struct SerialData { |
| 403 | emulated_serial_config: [Option<Resource<SerialBackendHandle>>; 4], |
| 404 | serial_tasks: Vec<Task<anyhow::Result<()>>>, |
| 405 | linux_direct_serial_agent: Option<LinuxDirectSerialAgent>, |
| 406 | } |
| 407 | |
| 408 | struct TestLogFiles { |
| 409 | output_dir: PathBuf, |
| 410 | hvlite_file: File, |
| 411 | guest_file: File, |
| 412 | petri_file: File, |
| 413 | openhcl_file: Option<File>, |
| 414 | } |
| 415 | |
| 416 | enum LogTarget { |
| 417 | Linux, |
| 418 | Uefi, |
| 419 | Pcat, |
| 420 | Openhcl, |
| 421 | } |
| 422 | |
| 423 | enum Device { |
| 424 | Vmbus(DeviceVtl, Resource<VmbusDeviceHandleKind>), |
| 425 | Vpci(VpciDeviceConfig), |
| 426 | Ide(IdeDeviceConfig), |
| 427 | } |
| 428 | |
| 429 | enum VideoDevice { |
| 430 | Vga(RomFileLocation), |
| 431 | Synth(DeviceVtl, Resource<VmbusDeviceHandleKind>), |
| 432 | } |
| 433 | |
| 434 | impl PetriVmConfigSetupCore<'_> { |
| 435 | fn create_log_files(&self) -> anyhow::Result<TestLogFiles> { |
| 436 | // DEVNOTE: This function runs before tracing is set up. |
| 437 | |
| 438 | let test_log_dir = self.resolver.resolve(common_artifacts::TEST_LOG_DIRECTORY); |
| 439 | let output_dir = test_log_dir.join(self.test_name); |
| 440 | if output_dir.exists() { |
| 441 | std::fs::remove_dir_all(&output_dir)?; |
| 442 | } |
| 443 | std::fs::create_dir_all(&output_dir)?; |
| 444 | |
| 445 | // NOTE: Due to a WSL + Windows defender bug, .txt extensions take forever to create within WSL |
| 446 | // when cross compiling. Name them .log which works around it. |
| 447 | let hvlite_file = File::create(output_dir.join("hvlite.log"))?; |
| 448 | let guest_file = File::create(output_dir.join("guest.log"))?; |
| 449 | let petri_file = File::create(output_dir.join("petri.log"))?; |
| 450 | let openhcl_file = if self.firmware.is_openhcl() { |
| 451 | Some(File::create(output_dir.join("openhcl.log"))?) |
| 452 | } else { |
| 453 | None |
| 454 | }; |
| 455 | |
| 456 | for attachment in [&hvlite_file, &guest_file, &petri_file] |
| 457 | .into_iter() |
| 458 | .chain(openhcl_file.as_ref()) |
| 459 | { |
| 460 | trace_attachment(attachment.path()); |
| 461 | } |
| 462 | |
| 463 | Ok(TestLogFiles { |
| 464 | output_dir, |
| 465 | hvlite_file, |
| 466 | guest_file, |
| 467 | petri_file, |
| 468 | openhcl_file, |
| 469 | }) |
| 470 | } |
| 471 | |
| 472 | fn configure_serial( |
| 473 | &self, |
| 474 | guest_file: File, |
| 475 | openhcl_file: Option<File>, |
| 476 | ) -> anyhow::Result<SerialData> { |
| 477 | let mut serial_tasks = Vec::new(); |
| 478 | |
| 479 | let serial0_log_target = match self.firmware { |
| 480 | Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => LogTarget::Linux, |
| 481 | Firmware::Pcat { .. } => LogTarget::Pcat, |
| 482 | Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => LogTarget::Uefi, |
| 483 | }; |
| 484 | |
| 485 | let (serial0_host, serial0) = self |
| 486 | .create_serial_stream() |
| 487 | .context("failed to create serial0 stream")?; |
| 488 | let (serial0_read, serial0_write) = serial0_host.split(); |
| 489 | let serial0_task = self |
| 490 | .spawn_serial_task( |
| 491 | "serial0-console", |
| 492 | serial0_log_target, |
| 493 | serial0_read, |
| 494 | guest_file, |
| 495 | ) |
| 496 | .context("failed to spawn serial0 task")?; |
| 497 | serial_tasks.push(serial0_task); |
| 498 | |
| 499 | let serial2 = if self.firmware.is_openhcl() { |
| 500 | let (serial2_host, serial2) = self |
| 501 | .create_serial_stream() |
| 502 | .context("failed to create serial2 stream")?; |
| 503 | let serial2_task = self |
| 504 | .spawn_serial_task( |
| 505 | "serial2-openhcl", |
| 506 | LogTarget::Openhcl, |
| 507 | serial2_host, |
| 508 | openhcl_file.unwrap(), |
| 509 | ) |
| 510 | .context("failed to spawn serial2 task")?; |
| 511 | serial_tasks.push(serial2_task); |
| 512 | serial2 |
| 513 | } else { |
| 514 | None |
| 515 | }; |
| 516 | |
| 517 | if self.firmware.is_linux_direct() { |
| 518 | let (serial1_host, serial1) = self.create_serial_stream()?; |
| 519 | let (serial1_read, _serial1_write) = serial1_host.split(); |
| 520 | let linux_direct_serial_agent = |
| 521 | LinuxDirectSerialAgent::new(serial1_read, serial0_write); |
| 522 | Ok(SerialData { |
| 523 | emulated_serial_config: [serial0, serial1, serial2, None], |
| 524 | serial_tasks, |
| 525 | linux_direct_serial_agent: Some(linux_direct_serial_agent), |
| 526 | }) |
| 527 | } else { |
| 528 | Ok(SerialData { |
| 529 | emulated_serial_config: [serial0, None, serial2, None], |
| 530 | serial_tasks, |
| 531 | linux_direct_serial_agent: None, |
| 532 | }) |
| 533 | } |
| 534 | } |
| 535 | |
| 536 | fn create_serial_stream( |
| 537 | &self, |
| 538 | ) -> anyhow::Result<( |
| 539 | PolledSocket<UnixStream>, |
| 540 | Option<Resource<SerialBackendHandle>>, |
| 541 | )> { |
| 542 | let (host_side, guest_side) = UnixStream::pair()?; |
| 543 | let host_side = PolledSocket::new(self.driver, host_side)?; |
| 544 | let serial = OpenUnixStreamSerialConfig::from(guest_side).into_resource(); |
| 545 | Ok((host_side, Some(serial))) |
| 546 | } |
| 547 | |
| 548 | fn spawn_serial_task( |
| 549 | &self, |
| 550 | task_name: &str, |
| 551 | log_target: LogTarget, |
| 552 | reader: impl AsyncRead + Unpin + Send + 'static, |
| 553 | mut file: File, |
| 554 | ) -> anyhow::Result<Task<anyhow::Result<()>>> { |
| 555 | Ok(self.driver.spawn(task_name, async move { |
| 556 | let mut buf = Vec::new(); |
| 557 | let mut reader = BufReader::new(reader); |
| 558 | loop { |
| 559 | buf.clear(); |
| 560 | let n = (&mut reader).take(256).read_until(b'\n', &mut buf).await?; |
| 561 | if n == 0 { |
| 562 | break; |
| 563 | } |
| 564 | |
| 565 | let string_buf = String::from_utf8_lossy(&buf); |
| 566 | let string_buf_trimmed = string_buf.trim_end(); |
| 567 | // tracing's target needs to be a const, can't just pass in a string |
| 568 | match log_target { |
| 569 | LogTarget::Linux => { |
| 570 | tracing::info!(target: crate::tracing::LINUX_TARGET, "{}", string_buf_trimmed) |
| 571 | } |
| 572 | LogTarget::Uefi => { |
| 573 | tracing::info!(target: crate::tracing::UEFI_TARGET, "{}", string_buf_trimmed) |
| 574 | } |
| 575 | LogTarget::Pcat => { |
| 576 | tracing::info!(target: crate::tracing::PCAT_TARGET, "{}", string_buf_trimmed) |
| 577 | } |
| 578 | LogTarget::Openhcl => { |
| 579 | tracing::info!(target: crate::tracing::OPENHCL_TARGET, "{}", string_buf_trimmed) |
| 580 | } |
| 581 | } |
| 582 | |
| 583 | file.write_all(&buf)?; |
| 584 | } |
| 585 | Ok(()) |
| 586 | })) |
| 587 | } |
| 588 | |
| 589 | fn load_firmware(&self) -> anyhow::Result<LoadMode> { |
| 590 | // Forward OPENVMM_LOG to OpenHCL if it's set. |
| 591 | let openhcl_tracing = |
| 592 | if let Ok(x) = std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG")) { |
| 593 | format!("OPENVMM_LOG={x}") |
| 594 | } else { |
| 595 | "OPENVMM_LOG=debug".to_owned() |
| 596 | }; |
| 597 | |
| 598 | Ok(match (self.arch, &self.firmware) { |
| 599 | (MachineArch::X86_64, Firmware::LinuxDirect { .. }) => { |
| 600 | let kernel = File::open( |
| 601 | self.resolver |
| 602 | .resolve(hvlite_artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_X64), |
| 603 | ) |
| 604 | .context("Failed to open kernel")? |
| 605 | .into(); |
| 606 | let initrd = File::open( |
| 607 | self.resolver |
| 608 | .resolve(hvlite_artifacts::loadable::LINUX_DIRECT_TEST_INITRD_X64), |
| 609 | ) |
| 610 | .context("Failed to open initrd")? |
| 611 | .into(); |
| 612 | LoadMode::Linux { |
| 613 | kernel, |
| 614 | initrd: Some(initrd), |
| 615 | cmdline: "console=ttyS0 debug panic=-1 rdinit=/bin/sh".into(), |
| 616 | custom_dsdt: None, |
| 617 | enable_serial: true, |
| 618 | } |
| 619 | } |
| 620 | (MachineArch::Aarch64, Firmware::LinuxDirect { .. }) => { |
| 621 | let kernel = File::open( |
| 622 | self.resolver |
| 623 | .resolve(hvlite_artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64), |
| 624 | ) |
| 625 | .context("Failed to open kernel")? |
| 626 | .into(); |
| 627 | let initrd = File::open( |
| 628 | self.resolver |
| 629 | .resolve(hvlite_artifacts::loadable::LINUX_DIRECT_TEST_INITRD_AARCH64), |
| 630 | ) |
| 631 | .context("Failed to open initrd")? |
| 632 | .into(); |
| 633 | LoadMode::Linux { |
| 634 | kernel, |
| 635 | initrd: Some(initrd), |
| 636 | cmdline: "console=ttyAMA0 earlycon debug panic=-1 rdinit=/bin/sh".into(), |
| 637 | custom_dsdt: None, |
| 638 | enable_serial: true, |
| 639 | } |
| 640 | } |
| 641 | (MachineArch::X86_64, Firmware::Pcat { .. }) => { |
| 642 | let firmware = hvlite_pcat_locator::find_pcat_bios( |
| 643 | self.resolver |
| 644 | .try_resolve(hvlite_artifacts::loadable::PCAT_FIRMWARE_X64) |
| 645 | .as_deref(), |
| 646 | ) |
| 647 | .context("Failed to load packaged PCAT binary")?; |
| 648 | LoadMode::Pcat { |
| 649 | firmware, |
| 650 | boot_order: DEFAULT_PCAT_BOOT_ORDER, |
| 651 | } |
| 652 | } |
| 653 | (_, Firmware::Uefi { .. }) => { |
| 654 | let firmware = File::open(self.resolver.resolve(match self.arch { |
| 655 | MachineArch::X86_64 => hvlite_artifacts::loadable::UEFI_FIRMWARE_X64.erase(), |
| 656 | MachineArch::Aarch64 => { |
| 657 | hvlite_artifacts::loadable::UEFI_FIRMWARE_AARCH64.erase() |
| 658 | } |
| 659 | })) |
| 660 | .context("Failed to open uefi firmware file")? |
| 661 | .into(); |
| 662 | LoadMode::Uefi { |
| 663 | firmware, |
| 664 | enable_debugging: false, |
| 665 | enable_memory_protections: false, |
| 666 | disable_frontpage: true, |
| 667 | enable_tpm: false, |
| 668 | enable_battery: false, |
| 669 | enable_serial: true, |
| 670 | enable_vpci_boot: false, |
| 671 | uefi_console_mode: Some(hvlite_defs::config::UefiConsoleMode::Com1), |
| 672 | } |
| 673 | } |
| 674 | ( |
| 675 | MachineArch::X86_64, |
| 676 | Firmware::OpenhclLinuxDirect { .. } | Firmware::OpenhclUefi { .. }, |
| 677 | ) => { |
| 678 | let mut cmdline = format!("panic=-1 reboot=triple {openhcl_tracing}"); |
| 679 | |
| 680 | let (igvm_artifact, isolated) = match self.firmware { |
| 681 | Firmware::OpenhclLinuxDirect { .. } => { |
| 682 | // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data |
| 683 | // until the guest is ready. Otherwise, Linux will drop the input serial |
| 684 | // data on the floor during boot. |
| 685 | write!(cmdline, " UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh\"").unwrap(); |
| 686 | ( |
| 687 | hvlite_artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64.erase(), |
| 688 | false, |
| 689 | ) |
| 690 | } |
| 691 | Firmware::OpenhclUefi { isolation, .. } if isolation.is_some() => { |
| 692 | (hvlite_artifacts::openhcl_igvm::LATEST_CVM_X64.erase(), true) |
| 693 | } |
| 694 | _ => ( |
| 695 | hvlite_artifacts::openhcl_igvm::LATEST_STANDARD_X64.erase(), |
| 696 | false, |
| 697 | ), |
| 698 | }; |
| 699 | let path = self.resolver.resolve(igvm_artifact); |
| 700 | let file = File::open(path) |
| 701 | .context("failed to open openhcl firmware file")? |
| 702 | .into(); |
| 703 | LoadMode::Igvm { |
| 704 | file, |
| 705 | cmdline, |
| 706 | vtl2_base_address: if isolated { |
| 707 | // Isolated VMs must load at the location specified by |
| 708 | // the file, as they do not support relocation. |
| 709 | Vtl2BaseAddressType::File |
| 710 | } else { |
| 711 | // By default, utilize IGVM relocation and tell hvlite |
| 712 | // to place VTL2 at 2GB. This tests both relocation |
| 713 | // support in hvlite, and relocation support within |
| 714 | // underhill. |
| 715 | Vtl2BaseAddressType::Absolute(2 * SIZE_1_GB) |
| 716 | }, |
| 717 | com_serial: Some(SerialInformation { |
| 718 | io_port: ComPort::Com3.io_port(), |
| 719 | irq: ComPort::Com3.irq().into(), |
| 720 | }), |
| 721 | } |
| 722 | } |
| 723 | (a, f) => anyhow::bail!("Unsupported firmware {f:?} for arch {a:?}"), |
| 724 | }) |
| 725 | } |
| 726 | |
| 727 | fn load_boot_disk( |
| 728 | &self, |
| 729 | devices: &mut impl Extend<Device>, |
| 730 | vtl2_settings: Option<&mut Vtl2Settings>, |
| 731 | ) -> anyhow::Result<()> { |
| 732 | match &self.firmware { |
| 733 | Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => { |
| 734 | // Nothing to do, everything is contained in LoadMode |
| 735 | } |
| 736 | Firmware::Uefi { |
| 737 | guest: UefiGuest::None, |
| 738 | } |
| 739 | | Firmware::OpenhclUefi { |
| 740 | guest: UefiGuest::None, |
| 741 | .. |
| 742 | } => { |
| 743 | // Nothing to do, no guest |
| 744 | } |
| 745 | Firmware::Pcat { guest } => { |
| 746 | let path = self.resolver.resolve(guest.artifact()); |
| 747 | let inner_disk = open_disk_type(&path, true)?; |
| 748 | let guest_media = match guest { |
| 749 | PcatGuest::Vhd(_) => GuestMedia::Disk { |
| 750 | read_only: false, |
| 751 | disk_parameters: None, |
| 752 | disk_type: (RamDiffDiskHandle { lower: inner_disk }).into_resource(), |
| 753 | }, |
| 754 | PcatGuest::Iso(_) => GuestMedia::Dvd( |
| 755 | SimpleScsiDvdHandle { |
| 756 | media: Some(inner_disk), |
| 757 | requests: None, |
| 758 | } |
| 759 | .into_resource(), |
| 760 | ), |
| 761 | }; |
| 762 | devices.extend([Device::Ide(IdeDeviceConfig { |
| 763 | path: ide_resources::IdePath { |
| 764 | channel: 0, |
| 765 | drive: 0, |
| 766 | }, |
| 767 | guest_media, |
| 768 | })]); |
| 769 | } |
| 770 | Firmware::Uefi { guest } |
| 771 | | Firmware::OpenhclUefi { |
| 772 | guest, |
| 773 | vtl2_nvme_boot: false, |
| 774 | .. |
| 775 | } => { |
| 776 | let path = self.resolver.resolve(guest.artifact()); |
| 777 | devices.extend([Device::Vmbus( |
| 778 | DeviceVtl::Vtl0, |
| 779 | ScsiControllerHandle { |
| 780 | instance_id: SCSI_INSTANCE, |
| 781 | max_sub_channel_count: 1, |
| 782 | io_queue_depth: None, |
| 783 | devices: vec![ScsiDeviceAndPath { |
| 784 | path: ScsiPath { |
| 785 | path: 0, |
| 786 | target: 0, |
| 787 | lun: 0, |
| 788 | }, |
| 789 | device: SimpleScsiDiskHandle { |
| 790 | read_only: false, |
| 791 | parameters: Default::default(), |
| 792 | disk: RamDiffDiskHandle { |
| 793 | lower: open_disk_type(&path, true)?, |
| 794 | } |
| 795 | .into_resource(), |
| 796 | } |
| 797 | .into_resource(), |
| 798 | }], |
| 799 | requests: None, |
| 800 | } |
| 801 | .into_resource(), |
| 802 | )]); |
| 803 | } |
| 804 | Firmware::OpenhclUefi { |
| 805 | guest, |
| 806 | vtl2_nvme_boot: true, |
| 807 | .. |
| 808 | } => { |
| 809 | let path = self.resolver.resolve(guest.artifact()); |
| 810 | devices.extend([Device::Vpci(VpciDeviceConfig { |
| 811 | vtl: DeviceVtl::Vtl2, |
| 812 | instance_id: BOOT_NVME_INSTANCE, |
| 813 | resource: NvmeControllerHandle { |
| 814 | subsystem_id: BOOT_NVME_INSTANCE, |
| 815 | max_io_queues: 64, |
| 816 | msix_count: 64, |
| 817 | namespaces: vec![NamespaceDefinition { |
| 818 | nsid: BOOT_NVME_NSID, |
| 819 | disk: RamDiffDiskHandle { |
| 820 | lower: open_disk_type(&path, true)?, |
| 821 | } |
| 822 | .into_resource(), |
| 823 | read_only: false, |
| 824 | }], |
| 825 | } |
| 826 | .into_resource(), |
| 827 | })]); |
| 828 | vtl2_settings |
| 829 | .expect("openhcl config should have vtl2settings") |
| 830 | .dynamic |
| 831 | .as_mut() |
| 832 | .unwrap() |
| 833 | .storage_controllers |
| 834 | .push(vtl2_settings_proto::StorageController { |
| 835 | instance_id: SCSI_INSTANCE.to_string(), |
| 836 | protocol: vtl2_settings_proto::storage_controller::StorageProtocol::Scsi |
| 837 | .into(), |
| 838 | luns: vec![vtl2_settings_proto::Lun { |
| 839 | location: BOOT_NVME_LUN, |
| 840 | device_id: Guid::new_random().to_string(), |
| 841 | vendor_id: "HvLite".to_string(), |
| 842 | product_id: "Disk".to_string(), |
| 843 | product_revision_level: "1.0".to_string(), |
| 844 | serial_number: "0".to_string(), |
| 845 | model_number: "1".to_string(), |
| 846 | physical_devices: Some(vtl2_settings_proto::PhysicalDevices { |
| 847 | r#type: vtl2_settings_proto::physical_devices::BackingType::Single |
| 848 | .into(), |
| 849 | device: Some(vtl2_settings_proto::PhysicalDevice { |
| 850 | device_type: |
| 851 | vtl2_settings_proto::physical_device::DeviceType::Nvme |
| 852 | .into(), |
| 853 | device_path: BOOT_NVME_INSTANCE.to_string(), |
| 854 | sub_device_path: BOOT_NVME_NSID, |
| 855 | }), |
| 856 | devices: Vec::new(), |
| 857 | }), |
| 858 | ..Default::default() |
| 859 | }], |
| 860 | io_queue_depth: None, |
| 861 | }); |
| 862 | } |
| 863 | } |
| 864 | |
| 865 | Ok(()) |
| 866 | } |
| 867 | |
| 868 | fn config_openhcl_vmbus_devices( |
| 869 | &self, |
| 870 | serial: &mut [Option<Resource<SerialBackendHandle>>], |
| 871 | devices: &mut impl Extend<Device>, |
| 872 | firmware_event_send: &mesh::MpscSender<FirmwareEvent>, |
| 873 | framebuffer: bool, |
| 874 | ) -> anyhow::Result<( |
| 875 | get_resources::ged::GuestEmulationDeviceHandle, |
| 876 | mesh::Sender<get_resources::ged::GuestEmulationRequest>, |
| 877 | )> { |
| 878 | let serial0 = serial[0].take(); |
| 879 | devices.extend([Device::Vmbus( |
| 880 | DeviceVtl::Vtl2, |
| 881 | VmbusSerialDeviceHandle { |
| 882 | port: VmbusSerialPort::Com1, |
| 883 | backend: serial0.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()), |
| 884 | } |
| 885 | .into_resource(), |
| 886 | )]); |
| 887 | let serial1 = serial[1].take(); |
| 888 | devices.extend([Device::Vmbus( |
| 889 | DeviceVtl::Vtl2, |
| 890 | VmbusSerialDeviceHandle { |
| 891 | port: VmbusSerialPort::Com2, |
| 892 | backend: serial1.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()), |
| 893 | } |
| 894 | .into_resource(), |
| 895 | )]); |
| 896 | |
| 897 | let gel = get_resources::gel::GuestEmulationLogHandle.into_resource(); |
| 898 | |
| 899 | let (crash, task) = spawn_dump_handler( |
| 900 | self.driver, |
| 901 | self.resolver |
| 902 | .resolve(hvlite_artifacts::OPENHCL_DUMP_DIRECTORY), |
| 903 | None, |
| 904 | ); |
| 905 | task.detach(); |
| 906 | |
| 907 | devices.extend([ |
| 908 | Device::Vmbus(DeviceVtl::Vtl2, crash), |
| 909 | Device::Vmbus(DeviceVtl::Vtl2, gel), |
| 910 | ]); |
| 911 | |
| 912 | let (guest_request_send, guest_request_recv) = mesh::channel(); |
| 913 | |
| 914 | // Save the GED handle to add later after configuration is complete. |
| 915 | let ged = get_resources::ged::GuestEmulationDeviceHandle { |
| 916 | firmware: get_resources::ged::GuestFirmwareConfig::Uefi { |
| 917 | firmware_debug: false, |
| 918 | disable_frontpage: true, |
| 919 | enable_vpci_boot: false, |
| 920 | console_mode: get_resources::ged::UefiConsoleMode::COM1, |
| 921 | }, |
| 922 | com1: true, |
| 923 | com2: true, |
| 924 | vmbus_redirection: false, |
| 925 | vtl2_settings: None, // Will be added at startup to allow tests to modify |
| 926 | framebuffer: framebuffer.then(|| SharedFramebufferHandle.into_resource()), |
| 927 | guest_request_recv, |
| 928 | enable_tpm: false, |
| 929 | firmware_event_send: Some(firmware_event_send.clone()), |
| 930 | secure_boot_enabled: false, |
| 931 | secure_boot_template: get_resources::ged::GuestSecureBootTemplateType::None, |
| 932 | enable_battery: false, |
| 933 | }; |
| 934 | |
| 935 | Ok((ged, guest_request_send)) |
| 936 | } |
| 937 | |
| 938 | fn config_video( |
| 939 | &self, |
| 940 | ) -> anyhow::Result<Option<(VideoDevice, Framebuffer, FramebufferAccess)>> { |
| 941 | if self.firmware.isolation().is_some() { |
| 942 | return Ok(None); |
| 943 | } |
| 944 | |
| 945 | let video_dev = match self.firmware { |
| 946 | Firmware::Pcat { .. } => Some(VideoDevice::Vga( |
| 947 | hvlite_pcat_locator::find_svga_bios( |
| 948 | self.resolver |
| 949 | .try_resolve(hvlite_artifacts::loadable::SVGA_FIRMWARE_X64) |
| 950 | .as_deref(), |
| 951 | ) |
| 952 | .context("Failed to load VGA BIOS")?, |
| 953 | )), |
| 954 | Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => Some(VideoDevice::Synth( |
| 955 | DeviceVtl::Vtl0, |
| 956 | SynthVideoHandle { |
| 957 | framebuffer: SharedFramebufferHandle.into_resource(), |
| 958 | } |
| 959 | .into_resource(), |
| 960 | )), |
| 961 | Firmware::OpenhclLinuxDirect { .. } | Firmware::LinuxDirect { .. } => None, |
| 962 | }; |
| 963 | |
| 964 | Ok(if let Some(vdev) = video_dev { |
| 965 | let vram = alloc_shared_memory(FRAMEBUFFER_SIZE).context("allocating framebuffer")?; |
| 966 | let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0) |
| 967 | .context("creating framebuffer")?; |
| 968 | Some((vdev, fb, fba)) |
| 969 | } else { |
| 970 | None |
| 971 | }) |
| 972 | } |
| 973 | |
| 974 | fn get_expected_boot_event(&self) -> Option<FirmwareEvent> { |
| 975 | match &self.firmware { |
| 976 | Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None, |
| 977 | Firmware::Pcat { .. } => { |
| 978 | // TODO: Handle older PCAT versions that don't fire the event |
| 979 | Some(FirmwareEvent::BootAttempt) |
| 980 | } |
| 981 | Firmware::Uefi { |
| 982 | guest: UefiGuest::None, |
| 983 | } |
| 984 | | Firmware::OpenhclUefi { |
| 985 | guest: UefiGuest::None, |
| 986 | .. |
| 987 | } => Some(FirmwareEvent::NoBootDevice), |
| 988 | Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => { |
| 989 | Some(FirmwareEvent::BootSuccess) |
| 990 | } |
| 991 | } |
| 992 | } |
| 993 | } |
| 994 | |