diff --git a/Cargo.lock b/Cargo.lock index 9758628b571..a7540e5a5d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,22 @@ dependencies = [ "vm_resource", ] +[[package]] +name = "gdma_test" +version = "0.0.0" +dependencies = [ + "async-trait", + "futures", + "gdma", + "gdma_defs", + "gdma_resources", + "pal_async", + "pci_resources", + "test_with_tracing", + "vm_resource", + "zerocopy", +] + [[package]] name = "generation_id" version = "0.0.0" @@ -6037,6 +6053,7 @@ dependencies = [ "disklayer_vhdx", "firmware_uefi", "gdma", + "gdma_test", "guest_crash_device", "guest_emulation_device", "guest_emulation_log", diff --git a/Cargo.toml b/Cargo.toml index e0e1029e300..798841fd2ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,6 +280,7 @@ missing_dev_resources = { path = "vm/devices/missing_dev_resources" } gdma = { path = "vm/devices/net/gdma" } gdma_defs = { path = "vm/devices/net/gdma_defs" } gdma_resources = { path = "vm/devices/net/gdma_resources" } +gdma_test = { path = "vm/devices/net/gdma_test" } linux_net_bindings = { path = "vm/devices/net/linux_net_bindings" } mana_driver = { path = "vm/devices/net/mana_driver" } vfio_sys = { path = "vm/devices/user_driver/vfio_sys" } diff --git a/openvmm/openvmm_resources/Cargo.toml b/openvmm/openvmm_resources/Cargo.toml index ee1e92e762f..27804fcd687 100644 --- a/openvmm/openvmm_resources/Cargo.toml +++ b/openvmm/openvmm_resources/Cargo.toml @@ -62,6 +62,7 @@ vmgs_broker.workspace = true # PCI devices cxl_spec.workspace = true gdma.workspace = true +gdma_test.workspace = true nvme.workspace = true nvme_test.workspace = true diff --git a/openvmm/openvmm_resources/src/lib.rs b/openvmm/openvmm_resources/src/lib.rs index 91054d9fcf9..aa413203ece 100644 --- a/openvmm/openvmm_resources/src/lib.rs +++ b/openvmm/openvmm_resources/src/lib.rs @@ -88,6 +88,7 @@ vm_resource::register_static_resolvers! { // PCI devices cxl_spec::test::resolver::CxlTestDeviceResolver, gdma::resolver::GdmaDeviceResolver, + gdma_test::resolver::GdmaTestDeviceResolver, nvme::resolver::NvmeControllerResolver, nvme_test::resolver::NvmeFaultControllerResolver, virtio::resolver::VirtioPciResolver, diff --git a/petri/src/vm/openvmm/mod.rs b/petri/src/vm/openvmm/mod.rs index 2b00d38147b..fc6a060d9ad 100644 --- a/petri/src/vm/openvmm/mod.rs +++ b/petri/src/vm/openvmm/mod.rs @@ -19,6 +19,7 @@ mod start; pub use hugetlb::HUGETLB_2MB_PAGE_SIZE; #[cfg(target_os = "linux")] pub use hugetlb::ensure_2mb_hugetlb_pages; +pub use modify::ManaTestControl; pub use runtime::OpenVmmFramebufferAccess; pub use runtime::OpenVmmInspector; pub use runtime::PetriVmOpenVmm; diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index 1994c456086..d680fd9249d 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -15,9 +15,12 @@ use chipset_resources::battery::HostBatteryUpdate; use disk_backend_resources::LayeredDiskHandle; use disk_backend_resources::layer::RamDiskLayerHandle; use gdma_resources::GdmaDeviceHandle; +use gdma_resources::GdmaTestDeviceHandle; +use gdma_resources::GdmaTestRequest; use gdma_resources::VportDefinition; use get_resources::ged::IgvmAttestTestConfig; use guid::Guid; +use mesh::rpc::RpcSend; use net_backend_resources::mac_address::MacAddress; use nvme_resources::NamespaceDefinition; use nvme_resources::NvmeControllerHandle; @@ -35,6 +38,8 @@ use openvmm_defs::config::Vtl2BaseAddressType; use vm_resource::IntoResource; use vmotherboard::ChipsetDeviceHandle; +const MANA_VTL0_INSTANCE: Guid = guid::guid!("f8615163-d915-4743-a7d8-efa75db7b85a"); + impl PetriVmConfigOpenVmm { /// Enable the VTL0 alias map. // TODO: Remove once #912 is fixed. @@ -154,6 +159,64 @@ impl PetriVmConfigOpenVmm { self } + /// Add a test-controllable VTL2 PCIe NIC and its paired VTL0 VF. + pub fn with_nic_test_control(mut self, test_config: ManaTestConfig) -> Self { + let vtl2_endpoint = net_backend_resources::consomme::ConsommeHandle { + cidr: None, + ports: Vec::new(), + recv: None, + } + .into_resource(); + let vtl0_endpoint = net_backend_resources::consomme::ConsommeHandle { + cidr: None, + ports: Vec::new(), + recv: None, + } + .into_resource(); + + if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() { + self.config.vpci_devices.extend([ + VpciDeviceConfig { + vtl: DeviceVtl::Vtl2, + instance_id: MANA_INSTANCE, + resource: GdmaTestDeviceHandle { + vports: vec![VportDefinition { + mac_address: NIC_MAC_ADDRESS, + endpoint: vtl2_endpoint, + }], + request_recv: test_config.request_recv, + } + .into_resource(), + vnode: None, + }, + VpciDeviceConfig { + vtl: DeviceVtl::Vtl0, + instance_id: MANA_VTL0_INSTANCE, + resource: GdmaDeviceHandle { + vports: vec![VportDefinition { + mac_address: NIC_MAC_ADDRESS, + endpoint: vtl0_endpoint, + }], + } + .into_resource(), + vnode: None, + }, + ]); + + vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push( + vtl2_settings_proto::NicDeviceLegacy { + instance_id: MANA_INSTANCE.to_string(), + subordinate_instance_id: Some(MANA_VTL0_INSTANCE.to_string()), + max_sub_channels: None, + }, + ); + } else { + panic!("with_nic_test_control requires VTL2 settings"); + } + + self + } + /// Add a PCIe NVMe device to the VM using the NVMe emulator. pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self { self.config.pcie_devices.push(PcieDeviceConfig { @@ -589,3 +652,45 @@ impl PetriVmConfigOpenVmm { self } } + +/// Control interface for issuing test requests to the MANA emulator. +/// +/// Created with [`ManaTestControl::new`] and connected with +/// [`PetriVmConfigOpenVmm::with_nic_test_control`]. +pub struct ManaTestControl { + request_send: mesh::Sender>, +} + +/// Configuration token for a test-controllable MANA emulator. +pub struct ManaTestConfig { + request_recv: mesh::Receiver>, +} + +impl ManaTestControl { + /// Creates a test control and its corresponding device configuration. + pub fn new() -> (Self, ManaTestConfig) { + let (request_send, request_recv) = mesh::channel(); + (Self { request_send }, ManaTestConfig { request_recv }) + } + + /// Requests VF reconfiguration through the emulated hardware channel. + /// + /// Completion means the reset EQE has been posted to the HWC EQ. + /// It may take time for the EQE to be processed. + /// Then, VF Reconfiguration will be completed asynchronously. + /// + /// `revoke_vtl0_vf`: when `true` the guest VTL0 VF is revoked as part of + /// the reconfiguration. + pub async fn inject_vf_reset(&self, revoke_vtl0_vf: bool) -> Result<(), mesh::rpc::RpcError> { + self.request_send + .call(|rpc| rpc, GdmaTestRequest::VfReset { revoke_vtl0_vf }) + .await + } + + /// Shuts down test control after all preceding requests have completed. + pub async fn shutdown(self) -> Result<(), mesh::rpc::RpcError> { + self.request_send + .call(|rpc| rpc, GdmaTestRequest::Shutdown) + .await + } +} diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 339e4d27c7d..715d0997f9f 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -645,6 +645,7 @@ impl GedChannel { HostRequests::CREATE_RAM_GPA_RANGE => self.handle_create_ram_gpa_range(message_buf)?, HostRequests::RESET_RAM_GPA_RANGE => self.handle_reset_ram_gpa_range(message_buf)?, HostRequests::LOAD_FIRMWARE => self.handle_load_firmware(message_buf)?, + HostRequests::VPCI_DEVICE_CONTROL => self.handle_vpci_device_control(message_buf)?, _ => { tracing::error!(message_id = ?header.message_id(), "unexpected message"); return Err(Error::InvalidSequence); @@ -1084,6 +1085,24 @@ impl GedChannel { Ok(()) } + fn handle_vpci_device_control(&mut self, message_buf: &[u8]) -> Result<(), Error> { + let request = get_protocol::VpciDeviceControlRequest::read_from_prefix(message_buf) + .map_err(|_| Error::MessageTooSmall)? + .0; + let status = match request.code { + get_protocol::VpciDeviceControlCode::OFFER + | get_protocol::VpciDeviceControlCode::REVOKE + | get_protocol::VpciDeviceControlCode::RESET => { + get_protocol::VpciDeviceControlStatus::SUCCESS + } + _ => get_protocol::VpciDeviceControlStatus::INVALID_REQUEST, + }; + let response = get_protocol::VpciDeviceControlResponse::new(status); + self.channel + .try_send(response.as_bytes()) + .map_err(Error::Vmbus) + } + fn handle_host_notification( &mut self, header: get_protocol::HeaderHostNotification, diff --git a/vm/devices/net/gdma/Cargo.toml b/vm/devices/net/gdma/Cargo.toml index a2ca8dc3bc0..f9786892a80 100644 --- a/vm/devices/net/gdma/Cargo.toml +++ b/vm/devices/net/gdma/Cargo.toml @@ -6,6 +6,9 @@ name = "gdma" edition.workspace = true rust-version.workspace = true +[features] +test_helpers = [] + [dependencies] gdma_defs.workspace = true gdma_resources.workspace = true diff --git a/vm/devices/net/gdma/src/bnic.rs b/vm/devices/net/gdma/src/bnic.rs index 9d6c66fd110..6224cdbfee8 100644 --- a/vm/devices/net/gdma/src/bnic.rs +++ b/vm/devices/net/gdma/src/bnic.rs @@ -35,6 +35,7 @@ use gdma_defs::Wqe; use gdma_defs::access::WqeAccess; use gdma_defs::bnic as bnic_defs; use gdma_defs::bnic::ManaDestroyWqobjReq; +use gdma_defs::bnic::ManaMoveFilterVTL2PrivilegedReq; use gdma_defs::bnic::ManaTxShortOob; use gdma_defs::bnic::Tristate; use guestmem::GuestMemory; @@ -190,6 +191,7 @@ struct Vport { task: TaskControl, queue_cfg: QueueCfg, serial_no: u32, + direction_to_vtl0: bool, } impl InspectMut for Vport { @@ -201,6 +203,7 @@ impl InspectMut for Vport { .field("tx_cq", self.queue_cfg.tx.map(|(_wq, cq)| cq)) .field("rx_wq", self.queue_cfg.rx.map(|(wq, _cq)| wq)) .field("rx_cq", self.queue_cfg.rx.map(|(_wq, cq)| cq)) + .field("direction_to_vtl0", self.direction_to_vtl0) .merge(&mut self.task); } } @@ -228,6 +231,7 @@ impl BasicNic { task: TaskControl::new(TxRxState), queue_cfg: QueueCfg { tx: None, rx: None }, serial_no: 0, + direction_to_vtl0: false, } }, ) @@ -424,19 +428,30 @@ impl BasicNic { } } ManaCommandCode::MANA_VTL2_MOVE_FILTER => { - anyhow::bail!("unsupported command MANA_VTL2_MOVE_FILTER"); + let req: ManaMoveFilterVTL2PrivilegedReq = read + .read_plain() + .context("reading move vport filter request")?; + let vport = self + .vports + .get_mut(req.vport as usize) + .context("invalid vport")?; + vport.direction_to_vtl0 = match req.direction_to_vtl0 { + 0 => false, + 1 => true, + value => anyhow::bail!("invalid filter direction {value}"), + }; } ManaCommandCode::MANA_VTL2_QUERY_FILTER_STATE => { let req: gdma_defs::bnic::ManaQueryFilterStateReq = read .read_plain() .context("reading query vport filter state request")?; - let _ = self + let vport = self .vports - .get_mut(req.vport as usize) + .get(req.vport as usize) .context("invalid vport")?; let resp = gdma_defs::bnic::ManaQueryFilterStateResponse { - direction_to_vtl0: 0, + direction_to_vtl0: vport.direction_to_vtl0.into(), reserved: [0; 7], }; diff --git a/vm/devices/net/gdma/src/lib.rs b/vm/devices/net/gdma/src/lib.rs index 00f01dd5b19..7b3611ad3d6 100644 --- a/vm/devices/net/gdma/src/lib.rs +++ b/vm/devices/net/gdma/src/lib.rs @@ -121,6 +121,35 @@ enum SmcError { pub use bnic::BnicConfig; +/// Helpers for cross-crate testing. +#[cfg(feature = "test_helpers")] +pub mod test_helpers { + use super::GdmaDevice; + use super::VportConfig; + use super::queues; + use gdma_resources::VportDefinition; + use std::sync::Arc; + use vm_resource::ResourceResolver; + + /// Resolves vport definitions for a test GDMA device. + pub async fn resolve_vports( + resolver: &ResourceResolver, + vports: Vec, + ) -> Result, super::resolver::Error> { + super::resolver::resolve_vports(resolver, vports).await + } + + /// Returns a function that injects EQEs into the hardware channel EQ. + pub fn hwc_eq_injector(device: &GdmaDevice) -> impl Fn(u8, &[u8]) + Send + Sync + 'static { + let queues = Arc::downgrade(&device.queues); + move |ty, data| { + if let Some(queues) = queues.upgrade() { + queues.post_eq(queues::ID_OFFSET as u32, ty, data) + } + } + } +} + pub struct VportConfig { pub mac_address: MacAddress, pub endpoint: Box, diff --git a/vm/devices/net/gdma/src/queues.rs b/vm/devices/net/gdma/src/queues.rs index 8aa186dfb4c..3746ddffa80 100644 --- a/vm/devices/net/gdma/src/queues.rs +++ b/vm/devices/net/gdma/src/queues.rs @@ -37,7 +37,7 @@ use zerocopy::IntoBytes; use zerocopy::KnownLayout; // Offset the queue IDs seen by the guest. -const ID_OFFSET: usize = 24; +pub(crate) const ID_OFFSET: usize = 24; struct CqEq { region: DmaRegion, diff --git a/vm/devices/net/gdma/src/resolver.rs b/vm/devices/net/gdma/src/resolver.rs index ec2e9c4cdb2..113b508cc7f 100644 --- a/vm/devices/net/gdma/src/resolver.rs +++ b/vm/devices/net/gdma/src/resolver.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Resource resolver for the nvme controller. +//! Resource resolver for the GDMA device. use crate::GdmaDevice; use crate::VportConfig; @@ -26,7 +26,7 @@ declare_static_async_resolver! { (PciDeviceHandleKind, GdmaDeviceHandle), } -/// Error returned by [`GdmaDeviceResolver`]. +/// Error returned by [`GdmaDeviceResolver`] and shared resolvers. #[derive(Debug, Error)] #[expect(missing_docs)] pub enum Error { @@ -34,6 +34,29 @@ pub enum Error { VportResolve(#[source] ResolveError), } +pub(crate) async fn resolve_vports( + resolver: &ResourceResolver, + vports: Vec, +) -> Result, Error> { + try_join_all(vports.into_iter().map(async |vport| { + let endpoint = resolver + .resolve( + vport.endpoint, + ResolveEndpointParams { + mac_address: vport.mac_address, + }, + ) + .await + .map_err(Error::VportResolve)?; + + Ok(VportConfig { + mac_address: vport.mac_address, + endpoint: endpoint.0, + }) + })) + .await +} + #[async_trait] impl AsyncResolveResource for GdmaDeviceResolver { type Output = ResolvedPciDevice; @@ -45,23 +68,7 @@ impl AsyncResolveResource for GdmaDeviceR resource: GdmaDeviceHandle, input: ResolvePciDeviceHandleParams<'_>, ) -> Result { - let vports = try_join_all(resource.vports.into_iter().map(async |vport| { - let endpoint = resolver - .resolve( - vport.endpoint, - ResolveEndpointParams { - mac_address: vport.mac_address, - }, - ) - .await - .map_err(Error::VportResolve)?; - - Ok(VportConfig { - mac_address: vport.mac_address, - endpoint: endpoint.0, - }) - })) - .await?; + let vports = resolve_vports(resolver, resource.vports).await?; let device = GdmaDevice::new( input.driver_source, diff --git a/vm/devices/net/gdma_resources/src/lib.rs b/vm/devices/net/gdma_resources/src/lib.rs index 9bbc7d0e3f0..d5096f42f13 100644 --- a/vm/devices/net/gdma_resources/src/lib.rs +++ b/vm/devices/net/gdma_resources/src/lib.rs @@ -23,6 +23,33 @@ impl ResourceId for GdmaDeviceHandle { const ID: &'static str = "gdma"; } +/// A resource handle to a test-controllable GDMA device. +/// +/// Used in VMM tests to issue typed hardware requests. +#[derive(MeshPayload)] +pub struct GdmaTestDeviceHandle { + /// The vports to instantiate on the NIC. + pub vports: Vec, + /// Channel for delivering requests from the test harness. + pub request_recv: mesh::Receiver>, +} + +impl ResourceId for GdmaTestDeviceHandle { + const ID: &'static str = "gdma-test"; +} + +/// A test request for an emulated GDMA device. +#[derive(MeshPayload)] +pub enum GdmaTestRequest { + /// Shut down the test-control channel. + Shutdown, + /// Request that the VF be reconfigured. + VfReset { + /// Whether OpenHCL should revoke the VTL0 VF during reset. + revoke_vtl0_vf: bool, + }, +} + /// A basic NIC vport definition. #[derive(MeshPayload)] pub struct VportDefinition { diff --git a/vm/devices/net/gdma_test/Cargo.toml b/vm/devices/net/gdma_test/Cargo.toml new file mode 100644 index 00000000000..2e6839eda50 --- /dev/null +++ b/vm/devices/net/gdma_test/Cargo.toml @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "gdma_test" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +gdma = { workspace = true, features = ["test_helpers"] } +gdma_defs.workspace = true +gdma_resources.workspace = true +pci_resources.workspace = true +vm_resource.workspace = true +pal_async.workspace = true +async-trait.workspace = true +futures.workspace = true +zerocopy.workspace = true + +[dev-dependencies] +test_with_tracing.workspace = true + +[lints] +workspace = true diff --git a/vm/devices/net/gdma_test/src/lib.rs b/vm/devices/net/gdma_test/src/lib.rs new file mode 100644 index 00000000000..e4c542c624c --- /dev/null +++ b/vm/devices/net/gdma_test/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! A GDMA device emulator with out-of-band EQE injection support. +//! +//! The production [`gdma`] crate has no test-specific state; +//! this crate provides the test-controllable variant via +//! [`gdma_resources::GdmaTestDeviceHandle`] and +//! [`resolver::GdmaTestDeviceResolver`]. + +#![forbid(unsafe_code)] + +pub mod resolver; diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs new file mode 100644 index 00000000000..0e0634f8022 --- /dev/null +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Resource resolver for [`GdmaTestDeviceHandle`]. + +use async_trait::async_trait; +use futures::StreamExt; +use gdma::GdmaDevice; +use gdma::resolver::Error; +use gdma::test_helpers::hwc_eq_injector; +use gdma::test_helpers::resolve_vports; +use gdma_defs::EqeVfReset; +use gdma_defs::GDMA_EQE_HWC_RESET_REQUEST; +use gdma_resources::GdmaTestDeviceHandle; +use gdma_resources::GdmaTestRequest; +use pal_async::task::Spawn; +use pci_resources::ResolvePciDeviceHandleParams; +use pci_resources::ResolvedPciDevice; +use vm_resource::AsyncResolveResource; +use vm_resource::ResourceResolver; +use vm_resource::declare_static_async_resolver; +use vm_resource::kind::PciDeviceHandleKind; +use zerocopy::IntoBytes; + +/// Resource resolver for [`GdmaTestDeviceHandle`]. +/// +/// Creates a standard GDMA device and spawns a background task that translates +/// test requests into EQEs injected directly into the HWC EQ via +/// [`gdma::test_helpers::hwc_eq_injector`]. The task exits when test control is +/// shut down. +pub struct GdmaTestDeviceResolver; + +declare_static_async_resolver! { + GdmaTestDeviceResolver, + (PciDeviceHandleKind, GdmaTestDeviceHandle), +} + +enum EncodedTestRequest { + VfReset(EqeVfReset), +} + +impl EncodedTestRequest { + fn eqe_type(&self) -> u8 { + match self { + Self::VfReset(_) => GDMA_EQE_HWC_RESET_REQUEST, + } + } + + fn data(&self) -> &[u8] { + match self { + Self::VfReset(data) => data.as_bytes(), + } + } +} + +fn encode_request(request: GdmaTestRequest) -> EncodedTestRequest { + match request { + GdmaTestRequest::Shutdown => unreachable!("shutdown requests are handled by the loop"), + GdmaTestRequest::VfReset { revoke_vtl0_vf } => { + EncodedTestRequest::VfReset(EqeVfReset::new().with_revoke_vtl0_vf(revoke_vtl0_vf)) + } + } +} + +#[async_trait] +impl AsyncResolveResource for GdmaTestDeviceResolver { + type Output = ResolvedPciDevice; + type Error = Error; + + async fn resolve( + &self, + resolver: &ResourceResolver, + resource: GdmaTestDeviceHandle, + input: ResolvePciDeviceHandleParams<'_>, + ) -> Result { + let vports = resolve_vports(resolver, resource.vports).await?; + + let device = GdmaDevice::new( + input.driver_source, + input.dma_target.guest_memory().clone(), + input.dma_target.msi_target(), + vports, + input.register_mmio, + ); + + let inject_eqe = hwc_eq_injector(&device); + let mut request_recv = resource.request_recv; + + input + .driver_source + .simple() + .spawn("gdma-test-control", async move { + while let Some(rpc) = request_recv.next().await { + let mut shutdown = false; + rpc.handle(async |request| { + if matches!(request, GdmaTestRequest::Shutdown) { + shutdown = true; + } else { + let request = encode_request(request); + inject_eqe(request.eqe_type(), request.data()) + } + }) + .await; + if shutdown { + break; + } + } + }) + .detach(); + + Ok(device.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_with_tracing::test; + + #[test] + fn encode_vf_reset() { + for revoke_vtl0_vf in [false, true] { + let request = encode_request(GdmaTestRequest::VfReset { revoke_vtl0_vf }); + assert_eq!(request.eqe_type(), GDMA_EQE_HWC_RESET_REQUEST); + let EncodedTestRequest::VfReset(data) = request; + assert_eq!(data.revoke_vtl0_vf(), revoke_vtl0_vf); + } + } +} diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs index c5593ee4284..608f3511c8d 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs @@ -3,6 +3,8 @@ //! Integration tests for x86_64 Linux direct boot with OpenHCL. +mod mana_nic; + use crate::x86_64::storage::new_test_vtl2_nvme_device; use guid::Guid; use memory_range::MemoryRange; @@ -23,62 +25,6 @@ use petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_ use vmm_test_macros::openvmm_test; use zerocopy::FromBytes; -/// Today this only tests that the nic can get an IP address via consomme's DHCP -/// implementation. -/// -/// FUTURE: Test traffic on the nic. -async fn validate_mana_nic(agent: &PipetteClient) -> Result<(), anyhow::Error> { - let sh = agent.unix_shell(); - cmd!(sh, "ifconfig eth0 up").run().await?; - cmd!(sh, "udhcpc eth0").run().await?; - let output = cmd!(sh, "ifconfig eth0").read().await?; - // Validate that we see a mana nic with the expected MAC address and IPs. - assert!(output.contains("HWaddr 00:15:5D:12:12:12")); - assert!(output.contains("inet addr:10.0.0.2")); - assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); - - Ok(()) -} - -/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by -/// the MANA emulator), and vmbus relay. -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic(config: PetriVmBuilder) -> Result<(), anyhow::Error> { - let (vm, agent) = config - .with_vmbus_redirect(true) - .modify_backend(|b| b.with_nic()) - .run() - .await?; - - validate_mana_nic(&agent).await?; - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - -/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by -/// the MANA emulator), and vmbus relay. Use the shared pool override to test -/// the shared pool dma path. -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic_shared_pool( - config: PetriVmBuilder, -) -> Result<(), anyhow::Error> { - let (vm, agent) = config - .with_vmbus_redirect(true) - .modify_backend(|b| b.with_nic()) - .run() - .await?; - - validate_mana_nic(&agent).await?; - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - /// Test an OpenHCL Linux direct VM with many NVMe devices assigned to VTL2 and vmbus relay. #[openvmm_test(openhcl_linux_direct_x64 [LATEST_LINUX_DIRECT_TEST_X64])] async fn many_nvme_devices_servicing_very_heavy( diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs new file mode 100644 index 00000000000..f54ecedff2d --- /dev/null +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! MANA integration tests for x86_64 Linux direct boot with OpenHCL. + +use petri::PetriVmBuilder; +use petri::openvmm::ManaTestControl; +use petri::openvmm::OpenVmmPetriBackend; +use petri::pipette::PipetteClient; +use petri::pipette::cmd; +use vmm_test_macros::openvmm_test; + +/// Get an IP address via consomme's DHCP implementation. +async fn configure_mana_nic(agent: &PipetteClient, interface: &str) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + cmd!(sh, "ifconfig {interface} up").run().await?; + cmd!(sh, "udhcpc -i {interface}").run().await?; + + Ok(()) +} + +/// Validates ICMP by testing that the nic can ping consomme's IP address. +/// +/// FUTURE: TCP / UDP traffic? +async fn validate_mana_nic(agent: &PipetteClient, interface: &str) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + let output = cmd!(sh, "ifconfig {interface}").read().await?; + // Validate that we see a mana nic with the expected MAC address and IPs. + assert!(output.contains("HWaddr 00:15:5D:12:12:12")); + assert!(output.contains("inet addr:10.0.0.2")); + cmd!(sh, "ping -c 1 -W 5 -I {interface} 10.0.0.1") + .run() + .await?; + + Ok(()) +} + +async fn validate_vtl0_mana_vf(agent: &PipetteClient) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + let vf_output = cmd!(sh, "ifconfig eth0").read().await?; + assert!(vf_output.contains("HWaddr 00:15:5D:12:12:12")); + let vf_master = cmd!(sh, "readlink /sys/class/net/eth0/master") + .read() + .await?; + assert_eq!(vf_master.rsplit('/').next(), Some("eth1")); + + Ok(()) +} + +/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by +/// the MANA emulator), and vmbus relay. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic(config: PetriVmBuilder) -> Result<(), anyhow::Error> { + let (vm, agent) = config + .with_vmbus_redirect(true) + .modify_backend(|b| b.with_nic()) + .run() + .await?; + + configure_mana_nic(&agent, "eth0").await?; + validate_mana_nic(&agent, "eth0").await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by +/// the MANA emulator), and vmbus relay. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_shared_pool( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (vm, agent) = config + .with_vmbus_redirect(true) + .modify_backend(|b| b.with_nic()) + .run() + .await?; + + configure_mana_nic(&agent, "eth0").await?; + validate_mana_nic(&agent, "eth0").await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_with_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; + + mana.shutdown().await?; + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +async fn mana_nic_vf_reconfig( + config: PetriVmBuilder, + revoke_vtl0_vf: bool, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; + + // Injection only waits for the EQE to be posted. + mana.inject_vf_reset(revoke_vtl0_vf).await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; + + mana.shutdown().await?; + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Test VF reconfiguration while retaining the VTL0 VF. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_vf_reconfig_keep_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + mana_nic_vf_reconfig(config, false).await +} + +/// Test VF reconfiguration while revoking the VTL0 VF. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_vf_reconfig_revoke_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + mana_nic_vf_reconfig(config, true).await +}