diff --git a/CHANGELOG.md b/CHANGELOG.md index d0853af69d3..30122d4765c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ and this project adheres to ### Added +- [#5687](https://github.com/firecracker-microvm/firecracker/issues/5687): Add + generic vhost-user frontend device support. This allows attaching any virtio + device type (e.g. virtio-fs, virtio-scsi) via the vhost-user protocol without + requiring a dedicated Firecracker frontend for each device type. Configured + via the `/vhost-user-devices/{id}` API endpoint. - [#5323](https://github.com/firecracker-microvm/firecracker/pull/5323): Add support for Vsock Unix domain socket path overriding on snapshot restore. More information can be found in the diff --git a/docs/vhost-user.md b/docs/vhost-user.md new file mode 100644 index 00000000000..3ec1b2463ba --- /dev/null +++ b/docs/vhost-user.md @@ -0,0 +1,102 @@ +# Using generic vhost-user devices + +## What is a vhost-user device + +The [vhost-user protocol](https://qemu-project.gitlab.io/qemu/interop/vhost-user.html) +allows virtio device emulation to be offloaded to a separate backend +process communicating over a Unix domain socket. The backend handles the +actual device logic while Firecracker acts as the frontend, managing +virtqueues and guest memory. + +A generic vhost-user frontend knows nothing about the specific virtio +device type being implemented. The backend is fully responsible for the +device configuration space. This allows using device types that +Firecracker would never support natively (e.g. virtio-fs, virtio-scsi) +without requiring a dedicated frontend for each. + +## Prerequisites + +- The vhost-user backend process must be running and listening on the + configured Unix domain socket **before** configuring the device in + Firecracker. +- The backend must support the `VHOST_USER_PROTOCOL_F_CONFIG` protocol + feature, as Firecracker relies on the backend to provide the device + configuration space. +- The guest kernel must include the driver for the virtio device type + being emulated (e.g. `CONFIG_VIRTIO_FS=y` for virtio-fs). + +## Configuration + +The following options are available: + +- `id` - unique identifier of the device. +- `device_type` - the virtio device type ID as defined in the + [virtio specification](https://docs.oasis-open.org/virtio/virtio/v1.3/csd01/virtio-v1.3-csd01.html#x1-1930005). + For example: `26` for virtio-fs, `8` for virtio-scsi. +- `socket` - path to the vhost-user backend Unix domain socket. +- `num_queues` - number of virtqueues to configure for this device. +- `queue_size` (optional) - size of each virtqueue. Defaults to 256. + +### Config file + +```json +"vhost-user-devices": [ + { + "id": "fs0", + "device_type": 26, + "socket": "/tmp/virtiofsd.sock", + "num_queues": 1, + "queue_size": 256 + } +] +``` + +### API + +```console +curl --unix-socket $socket_location -i \ + -X PUT 'http://localhost/vhost-user-devices/fs0' \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "{ + \"id\": \"fs0\", + \"device_type\": 26, + \"socket\": \"/tmp/virtiofsd.sock\", + \"num_queues\": 1, + \"queue_size\": 256 + }" +``` + +## Example: virtio-fs with virtiofsd + +Start the [virtiofsd](https://gitlab.com/virtio-fs/virtiofsd) backend: + +```console +virtiofsd \ + --socket-path=/tmp/virtiofsd.sock \ + --shared-dir=/path/to/shared \ + --tag=myfs +``` + +> [!NOTE] +> +> The `--tag` flag is required to enable the `VHOST_USER_PROTOCOL_F_CONFIG` +> protocol feature in virtiofsd. + +Then configure the device in Firecracker as shown above. Inside the +guest, mount the shared directory: + +```console +mount -t virtiofs myfs /mnt +``` + +## Limitations + +- **Snapshotting is not supported.** Creating or restoring snapshots of + a VM with generic vhost-user devices will fail. +- **Configuration space writes are not yet forwarded** to the backend + via `VHOST_USER_SET_CONFIG`. +- **The backend must be started before the device is attached.** + Firecracker connects to the socket when processing the + `PUT /vhost-user-devices/{id}` request and will return an error if the + backend is not available. diff --git a/src/firecracker/src/api_server/parsed_request.rs b/src/firecracker/src/api_server/parsed_request.rs index 959aa1e5a0a..ea6646c422f 100644 --- a/src/firecracker/src/api_server/parsed_request.rs +++ b/src/firecracker/src/api_server/parsed_request.rs @@ -27,6 +27,7 @@ use super::request::net::{parse_patch_net, parse_put_net}; use super::request::pmem::parse_put_pmem; use super::request::snapshot::{parse_patch_vm_state, parse_put_snapshot}; use super::request::version::parse_get_version; +use super::request::vhost_user_device::parse_put_vhost_user_device; use super::request::vsock::parse_put_vsock; use crate::api_server::request::hotplug::memory::{ parse_get_memory_hotplug, parse_patch_memory_hotplug, parse_put_memory_hotplug, @@ -107,6 +108,9 @@ impl TryFrom<&Request> for ParsedRequest { parse_put_net(body, path_tokens.next()) } (Method::Put, "snapshot", Some(body)) => parse_put_snapshot(body, path_tokens.next()), + (Method::Put, "vhost-user-devices", Some(body)) => { + parse_put_vhost_user_device(body, path_tokens.next()) + } (Method::Put, "vsock", Some(body)) => parse_put_vsock(body), (Method::Put, "entropy", Some(body)) => parse_put_entropy(body), (Method::Put, "hotplug", Some(body)) if path_tokens.next() == Some("memory") => { diff --git a/src/firecracker/src/api_server/request/mod.rs b/src/firecracker/src/api_server/request/mod.rs index 9be4617bd8e..fd3c6a9cdf3 100644 --- a/src/firecracker/src/api_server/request/mod.rs +++ b/src/firecracker/src/api_server/request/mod.rs @@ -18,5 +18,6 @@ pub mod pmem; pub mod serial; pub mod snapshot; pub mod version; +pub mod vhost_user_device; pub mod vsock; pub use micro_http::{Body, Method, StatusCode}; diff --git a/src/firecracker/src/api_server/request/vhost_user_device.rs b/src/firecracker/src/api_server/request/vhost_user_device.rs new file mode 100644 index 00000000000..e3a1b5fb9d7 --- /dev/null +++ b/src/firecracker/src/api_server/request/vhost_user_device.rs @@ -0,0 +1,83 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use vmm::logger::{IncMetric, METRICS}; +use vmm::rpc_interface::VmmAction; +use vmm::vmm_config::vhost_user_device::VhostUserDeviceConfig; + +use super::super::parsed_request::{ParsedRequest, RequestError, checked_id}; +use super::{Body, StatusCode}; + +pub(crate) fn parse_put_vhost_user_device( + body: &Body, + id_from_path: Option<&str>, +) -> Result { + METRICS.put_api_requests.vhost_user_count.inc(); + let id = if let Some(id) = id_from_path { + checked_id(id)? + } else { + METRICS.put_api_requests.vhost_user_fails.inc(); + return Err(RequestError::EmptyID); + }; + + let device_cfg = + serde_json::from_slice::(body.raw()).inspect_err(|_| { + METRICS.put_api_requests.vhost_user_fails.inc(); + })?; + + if id != device_cfg.id { + METRICS.put_api_requests.vhost_user_fails.inc(); + Err(RequestError::Generic( + StatusCode::BadRequest, + "The id from the path does not match the id from the body!".to_string(), + )) + } else { + Ok(ParsedRequest::new_sync(VmmAction::InsertVhostUserDevice( + device_cfg, + ))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api_server::parsed_request::tests::vmm_action_from_request; + + #[test] + fn test_parse_put_vhost_user_device_request() { + parse_put_vhost_user_device(&Body::new("invalid_payload"), None).unwrap_err(); + parse_put_vhost_user_device(&Body::new("invalid_payload"), Some("id")).unwrap_err(); + + let body = r#"{ + "id": "bar", + "device_type": 26, + "socket": "/tmp/test.sock", + "num_queues": 2 + }"#; + parse_put_vhost_user_device(&Body::new(body), Some("1")).unwrap_err(); + + let body = r#"{ + "foo": "1" + }"#; + parse_put_vhost_user_device(&Body::new(body), Some("1")).unwrap_err(); + + let body = r#"{ + "id": "fs0", + "device_type": 26, + "socket": "/tmp/virtiofsd.sock", + "num_queues": 2 + }"#; + let r = vmm_action_from_request( + parse_put_vhost_user_device(&Body::new(body), Some("fs0")).unwrap(), + ); + + let expected_config = VhostUserDeviceConfig { + id: "fs0".to_string(), + device_type: 26, + socket: "/tmp/virtiofsd.sock".to_string(), + num_queues: 2, + queue_size: None, + }; + assert_eq!(r, VmmAction::InsertVhostUserDevice(expected_config)); + } +} diff --git a/src/firecracker/swagger/firecracker.yaml b/src/firecracker/swagger/firecracker.yaml index 8d87eefc11a..0352f1e6423 100644 --- a/src/firecracker/swagger/firecracker.yaml +++ b/src/firecracker/swagger/firecracker.yaml @@ -371,6 +371,40 @@ paths: schema: $ref: "#/definitions/Error" + /vhost-user-devices/{id}: + put: + summary: Creates or updates a generic vhost-user device. Pre-boot only. + description: + Creates a new generic vhost-user device with ID specified by id parameter. + If a device with the specified ID already exists, it is replaced. + The backend process must be running and listening on the specified Unix + domain socket before making this request. The CONFIG protocol feature is + required. + operationId: putGuestVhostUserDeviceByID + parameters: + - name: id + in: path + description: The id of the guest vhost-user device + required: true + type: string + - name: body + in: body + description: Guest vhost-user device properties + required: true + schema: + $ref: "#/definitions/VhostUserDevice" + responses: + 204: + description: Vhost-user device is created/updated + 400: + description: Vhost-user device cannot be created/updated due to bad input + schema: + $ref: "#/definitions/Error" + default: + description: Internal server error. + schema: + $ref: "#/definitions/Error" + /logger: put: summary: Initializes the logger by specifying a named pipe or a file for the logs output. @@ -1256,6 +1290,41 @@ definitions: description: Flag to map backing file in read-only mode. + VhostUserDevice: + type: object + required: + - id + - device_type + - socket + - num_queues + properties: + id: + type: string + description: + Unique identifier of the device. + device_type: + type: integer + minimum: 0 + maximum: 255 + description: + The virtio device type ID as defined in the virtio specification. + For example, 26 for virtio-fs, 8 for virtio-scsi. + The backend is responsible for handling the corresponding device protocol. + socket: + type: string + description: + Path to the vhost-user backend Unix domain socket. + num_queues: + type: integer + minimum: 1 + description: + Number of virtqueues to configure for this device. + queue_size: + type: integer + minimum: 1 + description: + Queue size. Defaults to 256 if not specified. + Error: type: object properties: @@ -1298,6 +1367,11 @@ definitions: description: Configurations for all pmem devices. items: $ref: "#/definitions/Pmem" + vhost-user-devices: + type: array + description: Configurations for all generic vhost-user devices. + items: + $ref: "#/definitions/VhostUserDevice" vsock: $ref: "#/definitions/Vsock" entropy: diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index 9f9b71a30c3..8cba634d081 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -36,6 +36,7 @@ use crate::devices::virtio::mem::{VIRTIO_MEM_DEFAULT_SLOT_SIZE_MIB, VirtioMem}; use crate::devices::virtio::net::Net; use crate::devices::virtio::pmem::device::Pmem; use crate::devices::virtio::rng::Entropy; +use crate::devices::virtio::vhost_user_generic::device::VhostUserGeneric; use crate::devices::virtio::vsock::{Vsock, VsockUnixBackend}; #[cfg(feature = "gdb")] use crate::gdb; @@ -246,6 +247,13 @@ pub fn build_microvm_for_boot( vm_resources.pmem.devices.iter(), event_manager, )?; + attach_vhost_user_devices( + &mut device_manager, + &vm, + &mut boot_cmdline, + vm_resources.vhost_user.devices.iter(), + event_manager, + )?; if let Some(unix_vsock) = vm_resources.vsock.get() { attach_unixsock_vsock_device( @@ -752,6 +760,28 @@ fn attach_pmem_devices<'a, I: Iterator>> + Debug>( Ok(()) } +fn attach_vhost_user_devices<'a, I: Iterator>> + Debug>( + device_manager: &mut DeviceManager, + vm: &Arc, + cmdline: &mut LoaderKernelCmdline, + vhost_user_devices: I, + event_manager: &mut EventManager, +) -> Result<(), StartMicrovmError> { + for device in vhost_user_devices { + let id = device.lock().expect("Poisoned lock").id.clone(); + // The device mutex mustn't be locked here otherwise it will deadlock. + device_manager.attach_virtio_device( + vm, + id, + device.clone(), + cmdline, + event_manager, + true, // vhost-user devices require memfd-backed memory + )?; + } + Ok(()) +} + fn attach_unixsock_vsock_device( device_manager: &mut DeviceManager, vm: &Arc, diff --git a/src/vmm/src/device_manager/pci_mngr.rs b/src/vmm/src/device_manager/pci_mngr.rs index 902828aa6b6..b27f38d79c0 100644 --- a/src/vmm/src/device_manager/pci_mngr.rs +++ b/src/vmm/src/device_manager/pci_mngr.rs @@ -431,6 +431,12 @@ impl<'a> Persist<'a> for PciDevices { transport_state, }) } + VirtioDeviceType::VhostUserGeneric => { + warn!( + "Skipping generic vhost-user device. VhostUserGeneric does not support \ + snapshotting yet" + ); + } } } diff --git a/src/vmm/src/device_manager/persist.rs b/src/vmm/src/device_manager/persist.rs index 67fb13e47e3..a6f5776f2e2 100644 --- a/src/vmm/src/device_manager/persist.rs +++ b/src/vmm/src/device_manager/persist.rs @@ -299,6 +299,12 @@ impl<'a> Persist<'a> for MMIODeviceManager { device_info, }); } + VirtioDeviceType::VhostUserGeneric => { + warn!( + "Skipping generic vhost-user device. VhostUserGeneric does not support \ + snapshotting yet" + ); + } }; Ok(()) diff --git a/src/vmm/src/devices/virtio/device.rs b/src/vmm/src/devices/virtio/device.rs index 01950214b85..c2742e26f64 100644 --- a/src/vmm/src/devices/virtio/device.rs +++ b/src/vmm/src/devices/virtio/device.rs @@ -68,6 +68,10 @@ pub enum VirtioDeviceType { Vsock = virtio_ids::VIRTIO_ID_VSOCK as u8, Mem = virtio_ids::VIRTIO_ID_MEM as u8, Pmem = virtio_ids::VIRTIO_ID_PMEM as u8, + /// Sentinel used as the host-side MMIO map key for generic vhost-user + /// devices. The actual virtio device type ID visible to the guest is + /// returned by [`VirtioDevice::mmio_device_type_id`] instead. + VhostUserGeneric = 0xFF, } /// Trait for virtio devices to be driven by a virtio transport. @@ -122,6 +126,16 @@ pub trait VirtioDevice: AsAny + MutEventSubscriber + Send { fn interrupt_trigger(&self) -> &dyn VirtioInterrupt; + /// The virtio device type ID written to the MMIO device type register. + /// + /// For most devices this equals `self.device_type() as u32`. Devices that + /// use [`VirtioDeviceType::VhostUserGeneric`] as a host-side sentinel + /// override this to expose the real virtio spec device type ID to the + /// guest. + fn mmio_device_type_id(&self) -> u32 { + self.device_type() as u32 + } + /// The set of feature bits shifted by `page * 32`. fn avail_features_by_page(&self, page: u32) -> u32 { let avail_features = self.avail_features(); diff --git a/src/vmm/src/devices/virtio/mod.rs b/src/vmm/src/devices/virtio/mod.rs index b17db5b02ac..049c5602e18 100644 --- a/src/vmm/src/devices/virtio/mod.rs +++ b/src/vmm/src/devices/virtio/mod.rs @@ -27,6 +27,7 @@ pub mod rng; pub mod test_utils; pub mod transport; pub mod vhost_user; +pub mod vhost_user_generic; pub mod vhost_user_metrics; pub mod vsock; diff --git a/src/vmm/src/devices/virtio/transport/mmio.rs b/src/vmm/src/devices/virtio/transport/mmio.rs index 2c66189f034..a9ef11348d9 100644 --- a/src/vmm/src/devices/virtio/transport/mmio.rs +++ b/src/vmm/src/devices/virtio/transport/mmio.rs @@ -241,7 +241,7 @@ impl BusDevice for MmioTransport { let v = match offset { 0x0 => MMIO_MAGIC_VALUE, 0x04 => MMIO_VERSION, - 0x08 => self.locked_device().device_type() as u32, + 0x08 => self.locked_device().mmio_device_type_id(), 0x0c => VENDOR_ID, // vendor id 0x10 => { let mut features = self diff --git a/src/vmm/src/devices/virtio/vhost_user_generic/device.rs b/src/vmm/src/devices/virtio/vhost_user_generic/device.rs new file mode 100644 index 00000000000..7b3fef602e1 --- /dev/null +++ b/src/vmm/src/devices/virtio/vhost_user_generic/device.rs @@ -0,0 +1,612 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::Deref; +use std::sync::Arc; + +use log::error; +use utils::time::{ClockType, get_time_us}; +use vhost::vhost_user::Frontend; +use vhost::vhost_user::message::*; +use vmm_sys_util::eventfd::EventFd; + +use super::{QUEUE_SIZE, VhostUserGenericError}; +use crate::MutEventSubscriber; +use crate::devices::virtio::ActivateError; +use crate::devices::virtio::device::{ActiveState, DeviceState, VirtioDevice, VirtioDeviceType}; +use crate::devices::virtio::generated::virtio_config::VIRTIO_F_VERSION_1; +use crate::devices::virtio::generated::virtio_ring::VIRTIO_RING_F_EVENT_IDX; +use crate::devices::virtio::queue::Queue; +use crate::devices::virtio::transport::{VirtioInterrupt, VirtioInterruptType}; +use crate::devices::virtio::vhost_user::{VhostUserHandleBackend, VhostUserHandleImpl}; +use crate::devices::virtio::vhost_user_metrics::{ + VhostUserDeviceMetrics, VhostUserMetricsPerDevice, +}; +use crate::logger::{IncMetric, StoreMetric, log_dev_preview_warning}; +use crate::utils::u64_to_usize; +use crate::vmm_config::vhost_user_device::VhostUserDeviceConfig; +use crate::vstate::memory::GuestMemoryMmap; + +/// Maximum config space size in bytes. Used as the upper bound when fetching +/// config space from the backend. The backend may return fewer bytes. +const MAX_CONFIG_SPACE_SIZE: u32 = 256; + +const AVAILABLE_FEATURES: u64 = (1 << VIRTIO_F_VERSION_1) + | (1 << VIRTIO_RING_F_EVENT_IDX) + // vhost-user specific bit. Not defined in standard virtio spec. + // Specifies ability of frontend to negotiate protocol features. + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(); + +pub type VhostUserGeneric = VhostUserGenericImpl; + +/// Generic vhost-user frontend device. +/// +/// Unlike per-device-type vhost-user frontends, this device knows nothing +/// about the specific virtio device type being implemented. The backend is +/// fully responsible for handling the configuration space. This allows using +/// device types that Firecracker would never support natively (e.g. virtiofsd, +/// SPDK vhost-user-blk) without requiring a dedicated frontend for each. +pub struct VhostUserGenericImpl { + // Virtio fields. + pub avail_features: u64, + pub acked_features: u64, + /// Config space fetched from the backend via the CONFIG protocol feature. + pub config_space: Vec, + pub activate_evt: EventFd, + + // Transport related fields. + pub queues: Vec, + pub queue_evts: Vec, + pub device_state: DeviceState, + + // Implementation specific fields. + pub id: String, + /// The raw virtio device type ID written to the MMIO device type register. + /// Stored separately because VirtioDeviceType::VhostUserGeneric is used + /// as the host-side map key while the guest must see the real device type. + pub device_type_id: u32, + + // Vhost user protocol handle. + pub vu_handle: VhostUserHandleImpl, + pub vu_acked_protocol_features: u64, + pub metrics: Arc, +} + +// Need custom implementation because otherwise `Debug` is required for `vhost::Master` +impl std::fmt::Debug for VhostUserGenericImpl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VhostUserGenericImpl") + .field("avail_features", &self.avail_features) + .field("acked_features", &self.acked_features) + .field("config_space", &self.config_space) + .field("activate_evt", &self.activate_evt) + .field("queues", &self.queues) + .field("queue_evts", &self.queue_evts) + .field("device_state", &self.device_state) + .field("id", &self.id) + .field("device_type_id", &self.device_type_id) + .field("vu_handle", &self.vu_handle) + .field( + "vu_acked_protocol_features", + &self.vu_acked_protocol_features, + ) + .field("metrics", &self.metrics) + .finish() + } +} + +impl VhostUserGenericImpl { + pub fn new(config: VhostUserDeviceConfig) -> Result { + log_dev_preview_warning("generic vhost-user device", Option::None); + let start_time = get_time_us(ClockType::Monotonic); + + let requested_protocol_features = VhostUserProtocolFeatures::CONFIG; + + let mut vu_handle = VhostUserHandleImpl::::new(&config.socket, config.num_queues) + .map_err(VhostUserGenericError::VhostUser)?; + let (acked_features, acked_protocol_features) = vu_handle + .negotiate_features(AVAILABLE_FEATURES, requested_protocol_features) + .map_err(VhostUserGenericError::VhostUser)?; + + // The CONFIG protocol feature is required: the backend is responsible + // for the entire config space and we have no device-specific fallback. + if acked_protocol_features & VhostUserProtocolFeatures::CONFIG.bits() == 0 { + return Err(VhostUserGenericError::ConfigFeatureNotNegotiated); + } + + // Fetch the config space from the backend. + let buffer = [0u8; MAX_CONFIG_SPACE_SIZE as usize]; + let (_, config_space) = vu_handle + .vu + .get_config( + 0, + MAX_CONFIG_SPACE_SIZE, + VhostUserConfigFlags::WRITABLE, + &buffer, + ) + .map_err(VhostUserGenericError::Vhost)?; + + let activate_evt = + EventFd::new(libc::EFD_NONBLOCK).map_err(VhostUserGenericError::EventFd)?; + + let num_queues = config.num_queues as usize; + let queues = vec![Queue::new(config.queue_size.unwrap_or(QUEUE_SIZE)); num_queues]; + let queue_evts = (0..num_queues) + .map(|_| EventFd::new(libc::EFD_NONBLOCK).map_err(VhostUserGenericError::EventFd)) + .collect::, _>>()?; + let device_state = DeviceState::Inactive; + + // We negotiated features with the backend. Now these acked_features + // are available for the guest driver to choose from. + let avail_features = acked_features; + let acked_features = acked_features & VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(); + + let metrics_name = format!("vhost_user_generic_{}", config.id); + let metrics = VhostUserMetricsPerDevice::alloc(metrics_name); + let delta_us = get_time_us(ClockType::Monotonic) - start_time; + metrics.init_time_us.store(delta_us); + + Ok(Self { + avail_features, + acked_features, + config_space, + activate_evt, + + queues, + queue_evts, + device_state, + + id: config.id, + device_type_id: u32::from(config.device_type), + + vu_handle, + vu_acked_protocol_features: acked_protocol_features, + metrics, + }) + } + + /// Prepare device for being snapshotted. + pub fn prepare_save(&mut self) { + unimplemented!("VhostUserGeneric does not support snapshotting yet"); + } +} + +impl VirtioDevice for VhostUserGenericImpl +where + VhostUserGenericImpl: MutEventSubscriber, +{ + fn const_device_type() -> VirtioDeviceType { + VirtioDeviceType::VhostUserGeneric + } + + fn device_type(&self) -> VirtioDeviceType { + VirtioDeviceType::VhostUserGeneric + } + + /// Returns the real virtio device type ID as seen by the guest. + /// + /// Overrides the default implementation because `device_type()` returns + /// the host-side sentinel [`VirtioDeviceType::VhostUserGeneric`] while + /// the guest must see the actual virtio spec device type ID provided by + /// the user at configuration time. + fn mmio_device_type_id(&self) -> u32 { + self.device_type_id + } + + fn id(&self) -> &str { + &self.id + } + + fn avail_features(&self) -> u64 { + self.avail_features + } + + fn acked_features(&self) -> u64 { + self.acked_features + } + + fn set_acked_features(&mut self, acked_features: u64) { + self.acked_features = acked_features; + } + + fn queues(&self) -> &[Queue] { + &self.queues + } + + fn queues_mut(&mut self) -> &mut [Queue] { + &mut self.queues + } + + fn queue_events(&self) -> &[EventFd] { + &self.queue_evts + } + + fn interrupt_trigger(&self) -> &dyn VirtioInterrupt { + self.device_state + .active_state() + .expect("Device is not initialized") + .interrupt + .deref() + } + + fn read_config(&self, offset: u64, data: &mut [u8]) { + if let Some(config_space_bytes) = self.config_space.as_slice().get(u64_to_usize(offset)..) { + let len = config_space_bytes.len().min(data.len()); + data[..len].copy_from_slice(&config_space_bytes[..len]); + } else { + error!("Failed to read config space"); + self.metrics.cfg_fails.inc(); + } + } + + fn write_config(&mut self, _offset: u64, _data: &[u8]) { + // Config space is owned entirely by the backend. Writes from the + // guest driver are forwarded to the backend via the CONFIG protocol + // feature in a future implementation. + } + + fn activate( + &mut self, + mem: GuestMemoryMmap, + interrupt: Arc, + ) -> Result<(), ActivateError> { + for q in self.queues.iter_mut() { + q.initialize(&mem) + .map_err(ActivateError::QueueMemoryError)?; + } + + let start_time = get_time_us(ClockType::Monotonic); + + let queue_refs: Vec<(usize, &Queue, &EventFd)> = self + .queues + .iter() + .zip(self.queue_evts.iter()) + .enumerate() + .map(|(i, (q, ev))| (i, q, ev)) + .collect(); + + // Setting features again, because now we negotiated them + // with the guest driver as well. + self.vu_handle + .set_features(self.acked_features) + .and_then(|()| { + self.vu_handle + .setup_backend(&mem, &queue_refs, interrupt.clone()) + }) + .map_err(|err| { + self.metrics.activate_fails.inc(); + ActivateError::VhostUser(err) + })?; + + self.device_state = DeviceState::Activated(ActiveState { mem, interrupt }); + let delta_us = get_time_us(ClockType::Monotonic) - start_time; + self.metrics.activate_time_us.store(delta_us); + Ok(()) + } + + fn is_activated(&self) -> bool { + self.device_state.is_activated() + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::undocumented_unsafe_blocks)] + + use std::os::unix::net::UnixStream; + + use event_manager::{EventOps, Events, MutEventSubscriber}; + use vhost::{VhostUserMemoryRegionInfo, VringConfigData}; + use vmm_sys_util::tempfile::TempFile; + + use super::*; + use crate::devices::virtio::test_utils::{VirtQueue, default_interrupt, default_mem}; + use crate::devices::virtio::vhost_user::tests::create_mem; + use crate::test_utils::create_tmp_socket; + use crate::vstate::memory::GuestAddress; + + fn default_config(socket: String) -> VhostUserDeviceConfig { + VhostUserDeviceConfig { + id: "test_device".to_string(), + device_type: 26, // VIRTIO_ID_FS + socket, + num_queues: 2, + queue_size: None, + } + } + + #[test] + fn test_new_no_features() { + struct MockMaster { + sock: UnixStream, + max_queue_num: u64, + is_owner: std::cell::UnsafeCell, + hdr_flags: std::cell::UnsafeCell, + } + + impl VhostUserHandleBackend for MockMaster { + fn from_stream(sock: UnixStream, max_queue_num: u64) -> Self { + Self { + sock, + max_queue_num, + is_owner: std::cell::UnsafeCell::new(false), + hdr_flags: std::cell::UnsafeCell::new(VhostUserHeaderFlag::empty()), + } + } + + fn set_owner(&self) -> Result<(), vhost::Error> { + unsafe { *self.is_owner.get() = true }; + Ok(()) + } + + fn set_hdr_flags(&self, flags: VhostUserHeaderFlag) { + unsafe { *self.hdr_flags.get() = flags }; + } + + fn get_features(&self) -> Result { + Ok(0) + } + + fn get_protocol_features(&mut self) -> Result { + Ok(VhostUserProtocolFeatures::empty()) + } + + fn set_protocol_features( + &mut self, + features: VhostUserProtocolFeatures, + ) -> Result<(), vhost::Error> { + let _ = features; + Ok(()) + } + } + + impl MutEventSubscriber for VhostUserGenericImpl { + fn process(&mut self, _: Events, _: &mut EventOps) {} + fn init(&mut self, _: &mut EventOps) {} + } + + let (_tmp_dir, tmp_socket_path) = create_tmp_socket(); + + // Backend without CONFIG feature must return an error. + let err = + VhostUserGenericImpl::::new(default_config(tmp_socket_path)).unwrap_err(); + assert!(matches!( + err, + VhostUserGenericError::ConfigFeatureNotNegotiated + )); + } + + #[test] + fn test_new_all_features() { + struct MockMaster { + sock: UnixStream, + max_queue_num: u64, + is_owner: std::cell::UnsafeCell, + protocol_features: VhostUserProtocolFeatures, + hdr_flags: std::cell::UnsafeCell, + } + + impl VhostUserHandleBackend for MockMaster { + fn from_stream(sock: UnixStream, max_queue_num: u64) -> Self { + Self { + sock, + max_queue_num, + is_owner: std::cell::UnsafeCell::new(false), + protocol_features: VhostUserProtocolFeatures::all(), + hdr_flags: std::cell::UnsafeCell::new(VhostUserHeaderFlag::empty()), + } + } + + fn set_owner(&self) -> Result<(), vhost::Error> { + unsafe { *self.is_owner.get() = true }; + Ok(()) + } + + fn set_hdr_flags(&self, flags: VhostUserHeaderFlag) { + unsafe { *self.hdr_flags.get() = flags }; + } + + fn get_features(&self) -> Result { + Ok(AVAILABLE_FEATURES) + } + + fn get_protocol_features(&mut self) -> Result { + Ok(self.protocol_features) + } + + fn set_protocol_features( + &mut self, + features: VhostUserProtocolFeatures, + ) -> Result<(), vhost::Error> { + self.protocol_features = features; + Ok(()) + } + + fn get_config( + &mut self, + _offset: u32, + _size: u32, + _flags: VhostUserConfigFlags, + _buf: &[u8], + ) -> Result<(VhostUserConfig, VhostUserConfigPayload), vhost::Error> { + Ok((VhostUserConfig::default(), vec![0x01, 0x02, 0x03])) + } + } + + impl MutEventSubscriber for VhostUserGenericImpl { + fn process(&mut self, _: Events, _: &mut EventOps) {} + fn init(&mut self, _: &mut EventOps) {} + } + + let (_tmp_dir, tmp_socket_path) = create_tmp_socket(); + let device = + VhostUserGenericImpl::::new(default_config(tmp_socket_path.clone())) + .unwrap(); + + assert!(unsafe { *device.vu_handle.vu.is_owner.get() }); + assert_eq!(device.vu_handle.vu.max_queue_num, 2); + assert_eq!(device.avail_features, AVAILABLE_FEATURES); + assert_eq!( + device.acked_features, + VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits() + ); + assert_eq!(device.config_space, vec![0x01, 0x02, 0x03]); + assert_eq!(device.device_type_id, 26); + assert_eq!(device.queues.len(), 2); + assert_eq!(device.queue_evts.len(), 2); + + // VirtioDevice trait methods + assert_eq!(device.id(), "test_device"); + assert_eq!(device.device_type(), VirtioDeviceType::VhostUserGeneric); + assert_eq!(device.mmio_device_type_id(), 26); + + // Valid read + let mut buf = vec![0u8; 3]; + device.read_config(0, &mut buf); + assert_eq!(buf, vec![0x01, 0x02, 0x03]); + + // Out-of-bounds offset returns zeroes + let mut buf = vec![0u8; 3]; + device.read_config(0xFF, &mut buf); + assert_eq!(buf, vec![0, 0, 0]); + + // Write is a no-op + let mut device = device; + device.write_config(0, &[0xFF]); + assert_eq!(device.config_space, vec![0x01, 0x02, 0x03]); + } + + #[test] + fn test_activate() { + struct MockMaster { + features_are_set: std::cell::UnsafeCell, + memory_is_set: std::cell::UnsafeCell, + vring_enabled: std::cell::UnsafeCell, + } + + impl VhostUserHandleBackend for MockMaster { + fn from_stream(_sock: UnixStream, _max_queue_num: u64) -> Self { + Self { + features_are_set: std::cell::UnsafeCell::new(false), + memory_is_set: std::cell::UnsafeCell::new(false), + vring_enabled: std::cell::UnsafeCell::new(false), + } + } + + fn set_owner(&self) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_hdr_flags(&self, _flags: VhostUserHeaderFlag) {} + + fn get_features(&self) -> Result { + // Must include PROTOCOL_FEATURES so that protocol feature + // negotiation (including CONFIG) takes place. + Ok(VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()) + } + + fn get_protocol_features(&mut self) -> Result { + Ok(VhostUserProtocolFeatures::CONFIG) + } + + fn set_protocol_features( + &mut self, + _features: VhostUserProtocolFeatures, + ) -> Result<(), vhost::Error> { + Ok(()) + } + + fn get_config( + &mut self, + _offset: u32, + _size: u32, + _flags: VhostUserConfigFlags, + _buf: &[u8], + ) -> Result<(VhostUserConfig, VhostUserConfigPayload), vhost::Error> { + Ok((VhostUserConfig::default(), vec![])) + } + + fn set_features(&self, _features: u64) -> Result<(), vhost::Error> { + unsafe { (*self.features_are_set.get()) = true }; + Ok(()) + } + + fn set_mem_table( + &self, + _regions: &[VhostUserMemoryRegionInfo], + ) -> Result<(), vhost::Error> { + unsafe { (*self.memory_is_set.get()) = true }; + Ok(()) + } + + fn set_vring_num(&self, _queue_index: usize, _num: u16) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_vring_addr( + &self, + _queue_index: usize, + _config_data: &VringConfigData, + ) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_vring_base(&self, _queue_index: usize, _base: u16) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_vring_call( + &self, + _queue_index: usize, + _fd: &EventFd, + ) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_vring_kick( + &self, + _queue_index: usize, + _fd: &EventFd, + ) -> Result<(), vhost::Error> { + Ok(()) + } + + fn set_vring_enable( + &mut self, + _queue_index: usize, + _enable: bool, + ) -> Result<(), vhost::Error> { + unsafe { (*self.vring_enabled.get()) = true }; + Ok(()) + } + } + + impl MutEventSubscriber for VhostUserGenericImpl { + fn process(&mut self, _: Events, _: &mut EventOps) {} + fn init(&mut self, _: &mut EventOps) {} + } + + let (_tmp_dir, tmp_socket_path) = create_tmp_socket(); + let mut device = + VhostUserGenericImpl::::new(default_config(tmp_socket_path)).unwrap(); + + let region_size = 0x10000; + let file = TempFile::new().unwrap().into_file(); + file.set_len(region_size as u64).unwrap(); + let regions = vec![(GuestAddress(0x0), region_size)]; + let guest_memory = create_mem(file, ®ions); + + for q in device.queues.iter_mut() { + let vq = VirtQueue::new(GuestAddress(0), &guest_memory, 16); + *q = vq.create_queue(); + } + + let interrupt = default_interrupt(); + device.activate(guest_memory, interrupt).unwrap(); + + assert!(unsafe { *device.vu_handle.vu.features_are_set.get() }); + assert!(unsafe { *device.vu_handle.vu.memory_is_set.get() }); + assert!(unsafe { *device.vu_handle.vu.vring_enabled.get() }); + assert!(device.is_activated()); + } +} diff --git a/src/vmm/src/devices/virtio/vhost_user_generic/event_handler.rs b/src/vmm/src/devices/virtio/vhost_user_generic/event_handler.rs new file mode 100644 index 00000000000..c2b46ccab0b --- /dev/null +++ b/src/vmm/src/devices/virtio/vhost_user_generic/event_handler.rs @@ -0,0 +1,80 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use event_manager::{EventOps, Events, MutEventSubscriber}; +use vmm_sys_util::epoll::EventSet; + +use super::VhostUserGeneric; +use crate::devices::virtio::device::VirtioDevice; +use crate::logger::{error, warn}; + +impl VhostUserGeneric { + const PROCESS_ACTIVATE: u32 = 0; + + fn register_activate_event(&self, ops: &mut EventOps) { + if let Err(err) = ops.add(Events::with_data( + &self.activate_evt, + Self::PROCESS_ACTIVATE, + EventSet::IN, + )) { + error!("Failed to register activate event: {}", err); + } + } + + fn process_activate_event(&self, ops: &mut EventOps) { + if let Err(err) = self.activate_evt.read() { + error!( + "Failed to consume generic vhost-user activate event: {:?}", + err + ); + } + if let Err(err) = ops.remove(Events::with_data( + &self.activate_evt, + Self::PROCESS_ACTIVATE, + EventSet::IN, + )) { + error!("Failed to un-register activate event: {}", err); + } + } +} + +impl MutEventSubscriber for VhostUserGeneric { + fn process(&mut self, event: Events, ops: &mut EventOps) { + let source = event.data(); + let event_set = event.event_set(); + let supported_events = EventSet::IN; + + if !supported_events.contains(event_set) { + warn!( + "Received unknown event: {:?} from source: {:?}", + event_set, source + ); + return; + } + + if self.is_activated() { + if Self::PROCESS_ACTIVATE == source { + self.process_activate_event(ops) + } else { + warn!("VhostUserGeneric: Spurious event received: {:?}", source) + } + } else { + warn!( + "VhostUserGeneric: The device is not yet activated. Spurious event received: {:?}", + source + ); + } + } + + fn init(&mut self, ops: &mut EventOps) { + // This function can be called during different points in the device lifetime: + // - shortly after device creation, + // - on device activation (is-activated already true at this point), + // - on device restore from snapshot. + if self.is_activated() { + warn!("VhostUserGeneric: unexpected init event"); + } else { + self.register_activate_event(ops); + } + } +} diff --git a/src/vmm/src/devices/virtio/vhost_user_generic/mod.rs b/src/vmm/src/devices/virtio/vhost_user_generic/mod.rs new file mode 100644 index 00000000000..5b9b6d866f6 --- /dev/null +++ b/src/vmm/src/devices/virtio/vhost_user_generic/mod.rs @@ -0,0 +1,30 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub mod device; +pub mod event_handler; +pub mod persist; + +use self::device::VhostUserGeneric; +use crate::devices::virtio::vhost_user::VhostUserError; +use crate::vstate::interrupts::InterruptError; + +/// Default queue size for the generic vhost-user device. +pub const QUEUE_SIZE: u16 = 256; + +/// Generic vhost-user device error. +#[derive(Debug, thiserror::Error, displaydoc::Display)] +pub enum VhostUserGenericError { + /// Snapshotting of generic vhost-user devices is not supported + SnapshottingNotSupported, + /// Vhost-user error: {0} + VhostUser(VhostUserError), + /// Vhost error: {0} + Vhost(vhost::Error), + /// Error opening eventfd: {0} + EventFd(std::io::Error), + /// Error creating irqfd: {0} + Interrupt(InterruptError), + /// CONFIG protocol feature is required but was not negotiated with the backend + ConfigFeatureNotNegotiated, +} diff --git a/src/vmm/src/devices/virtio/vhost_user_generic/persist.rs b/src/vmm/src/devices/virtio/vhost_user_generic/persist.rs new file mode 100644 index 00000000000..ba3f48bec4e --- /dev/null +++ b/src/vmm/src/devices/virtio/vhost_user_generic/persist.rs @@ -0,0 +1,38 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Defines the structures needed for saving/restoring generic vhost-user devices. + +use serde::{Deserialize, Serialize}; + +use super::VhostUserGenericError; +use super::device::VhostUserGeneric; +use crate::snapshot::Persist; + +/// Generic vhost-user device state. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VhostUserGenericState { + id: String, + device_type_id: u32, + socket_path: String, + num_queues: u64, + vu_acked_protocol_features: u64, + config_space: Vec, +} + +impl Persist<'_> for VhostUserGeneric { + type State = VhostUserGenericState; + type ConstructorArgs = (); + type Error = VhostUserGenericError; + + fn save(&self) -> Self::State { + unimplemented!("VhostUserGeneric does not support snapshotting yet"); + } + + fn restore( + _constructor_args: Self::ConstructorArgs, + _state: &Self::State, + ) -> Result { + Err(VhostUserGenericError::SnapshottingNotSupported) + } +} diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index 2e389606805..933386813ef 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -147,6 +147,7 @@ use crate::devices::virtio::mem::{VIRTIO_MEM_DEV_ID, VirtioMemError, VirtioMemSt use crate::devices::virtio::net::Net; use crate::devices::virtio::pmem::device::Pmem; use crate::devices::virtio::rng::Entropy; +use crate::devices::virtio::vhost_user_generic::device::VhostUserGeneric; use crate::devices::virtio::vsock::{Vsock, VsockUnixBackend}; use crate::logger::{METRICS, MetricsError, error, info, warn}; use crate::mmds::data_store::Mmds; @@ -161,6 +162,7 @@ use crate::vmm_config::machine_config::MachineConfig; use crate::vmm_config::memory_hotplug::MemoryHotplugConfig; use crate::vmm_config::mmds::MmdsConfig; use crate::vmm_config::net::NetworkInterfaceConfig; +use crate::vmm_config::vhost_user_device::VhostUserDeviceConfig; use crate::vmm_config::vsock::VsockDeviceConfig; use crate::vstate::memory::{GuestMemory, GuestMemoryMmap, GuestMemoryRegion}; use crate::vstate::vcpu::VcpuState; @@ -369,6 +371,7 @@ impl Vmm { let mut net = Vec::new(); let mut net_with_mmds = Vec::new(); let mut pmem = Vec::new(); + let mut vhost_user_devices = Vec::new(); let mut balloon = None; let mut vsock = None; let mut entropy = None; @@ -420,6 +423,17 @@ impl Vmm { memory_hotplug = Some(MemoryHotplugConfig::from(m)); } } + VirtioDeviceType::VhostUserGeneric => { + if let Some(d) = device.as_any().downcast_ref::() { + vhost_user_devices.push(VhostUserDeviceConfig { + id: d.id.clone(), + device_type: d.device_type_id as u8, + socket: d.vu_handle.socket_path.clone(), + num_queues: d.queues.len() as u64, + queue_size: d.queues.first().map(|q| q.size), + }); + } + } }); let mmds_config = mmds_ref.map(|mmds| { @@ -448,6 +462,7 @@ impl Vmm { vsock, entropy, pmem_devices: pmem, + vhost_user_devices, // serial_config is marked serde(skip) so that it doesnt end up in snapshots serial_config: None, memory_hotplug, diff --git a/src/vmm/src/logger/metrics.rs b/src/vmm/src/logger/metrics.rs index 2bd7330c61a..0ac01db5a01 100644 --- a/src/vmm/src/logger/metrics.rs +++ b/src/vmm/src/logger/metrics.rs @@ -432,6 +432,10 @@ pub struct PutRequestsMetrics { pub hotplug_memory_count: SharedIncMetric, /// Number of failed PUTs to /hotplug/memory pub hotplug_memory_fails: SharedIncMetric, + /// Number of PUTs triggering a vhost-user device attach. + pub vhost_user_count: SharedIncMetric, + /// Number of failures in attaching a vhost-user device. + pub vhost_user_fails: SharedIncMetric, } impl PutRequestsMetrics { /// Const default construction. @@ -463,6 +467,8 @@ impl PutRequestsMetrics { serial_fails: SharedIncMetric::new(), hotplug_memory_count: SharedIncMetric::new(), hotplug_memory_fails: SharedIncMetric::new(), + vhost_user_count: SharedIncMetric::new(), + vhost_user_fails: SharedIncMetric::new(), } } } diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index cc45aafe16f..135971c1b5e 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -30,6 +30,9 @@ use crate::vmm_config::mmds::{MmdsConfig, MmdsConfigError}; use crate::vmm_config::net::*; use crate::vmm_config::pmem::{PmemBuilder, PmemConfig, PmemConfigError}; use crate::vmm_config::serial::SerialConfig; +use crate::vmm_config::vhost_user_device::{ + VhostUserDeviceBuilder, VhostUserDeviceConfig, VhostUserDeviceConfigError, +}; use crate::vmm_config::vsock::*; use crate::vstate::memory; use crate::vstate::memory::{GuestRegionMmap, MemoryError}; @@ -67,6 +70,8 @@ pub enum ResourcesError { PmemDevice(#[from] PmemConfigError), /// Memory hotplug config error: {0} MemoryHotplugConfig(#[from] MemoryHotplugConfigError), + /// Generic vhost-user device error: {0} + VhostUserDevice(#[from] VhostUserDeviceConfigError), } #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] @@ -96,6 +101,8 @@ pub struct VmmConfig { pub entropy: Option, #[serde(default, rename = "pmem")] pub pmem_devices: Vec, + #[serde(default, rename = "vhost-user-devices")] + pub vhost_user_devices: Vec, #[serde(skip)] pub serial_config: Option, pub memory_hotplug: Option, @@ -121,6 +128,8 @@ pub struct VmResources { pub entropy: EntropyDeviceBuilder, /// The pmem devices. pub pmem: PmemBuilder, + /// The generic vhost-user devices. + pub vhost_user: VhostUserDeviceBuilder, /// The memory hotplug configuration. pub memory_hotplug: Option, /// The optional Mmds data store. @@ -216,6 +225,10 @@ impl VmResources { resources.build_pmem_device(pmem_config)?; } + for vu_config in vmm_config.vhost_user_devices.into_iter() { + resources.build_vhost_user_device(vu_config)?; + } + if let Some(serial_cfg) = vmm_config.serial_config { resources.serial_out_path = serial_cfg.serial_out_path; } @@ -379,6 +392,14 @@ impl VmResources { self.pmem.build(body, has_block_root) } + /// Builds a generic vhost-user device to be attached when the VM starts. + pub fn build_vhost_user_device( + &mut self, + body: VhostUserDeviceConfig, + ) -> Result<(), VhostUserDeviceConfigError> { + self.vhost_user.build(body) + } + /// Sets the memory hotplug configuration. pub fn set_memory_hotplug_config( &mut self, @@ -470,18 +491,19 @@ impl VmResources { &self, regions: &[(GuestAddress, usize)], ) -> Result, MemoryError> { - let vhost_user_device_used = self - .block - .devices - .iter() - .any(|b| b.lock().expect("Poisoned lock").is_vhost_user()); + let vhost_user_device_used = !self.vhost_user.devices.is_empty() + || self + .block + .devices + .iter() + .any(|b| b.lock().expect("Poisoned lock").is_vhost_user()); // Page faults are more expensive for shared memory mapping, including memfd. // For this reason, we only back guest memory with a memfd - // if a vhost-user-blk device is configured in the VM, otherwise we fall back to + // if a vhost-user device is configured in the VM, otherwise we fall back to // an anonymous private memory. // - // The vhost-user-blk branch is not currently covered by integration tests in Rust, + // The vhost-user branch is not currently covered by integration tests in Rust, // because that would require running a backend process. If in the future we converge to // a single way of backing guest memory for vhost-user and non-vhost-user cases, // that would not be worth the effort. @@ -535,6 +557,7 @@ impl From<&VmResources> for VmmConfig { vsock: resources.vsock.config(), entropy: resources.entropy.config(), pmem_devices: resources.pmem.configs(), + vhost_user_devices: resources.vhost_user.configs(), // serial_config is marked serde(skip) so that it doesnt end up in snapshots. serial_config: None, memory_hotplug: resources.memory_hotplug.clone(), @@ -649,6 +672,7 @@ mod tests { mmds_size_limit: HTTP_MAX_PAYLOAD_SIZE, entropy: Default::default(), pmem: Default::default(), + vhost_user: Default::default(), pci_enabled: false, serial_out_path: None, memory_hotplug: Default::default(), diff --git a/src/vmm/src/rpc_interface.rs b/src/vmm/src/rpc_interface.rs index 4617890a0e4..9a378c1a988 100644 --- a/src/vmm/src/rpc_interface.rs +++ b/src/vmm/src/rpc_interface.rs @@ -41,6 +41,7 @@ use crate::vmm_config::net::{ use crate::vmm_config::pmem::{PmemConfig, PmemConfigError}; use crate::vmm_config::serial::SerialConfig; use crate::vmm_config::snapshot::{CreateSnapshotParams, LoadSnapshotParams, SnapshotType}; +use crate::vmm_config::vhost_user_device::{VhostUserDeviceConfig, VhostUserDeviceConfigError}; use crate::vmm_config::vsock::{VsockConfigError, VsockDeviceConfig}; use crate::vmm_config::{self, RateLimiterUpdate}; @@ -87,6 +88,9 @@ pub enum VmmAction { /// `NetworkInterfaceConfig` as input. This action can only be called before the microVM has /// booted. InsertNetworkDevice(NetworkInterfaceConfig), + /// Add a generic vhost-user device using the `VhostUserDeviceConfig` as input. This action + /// can only be called before the microVM has booted. + InsertVhostUserDevice(VhostUserDeviceConfig), /// Load the microVM state using as input the `LoadSnapshotParams`. This action can only be /// called before the microVM has booted. If this action is successful, the loaded microVM will /// be in `Paused` state. Should change this state to `Resumed` for the microVM to run. @@ -199,6 +203,8 @@ pub enum VmmActionError { OperationNotSupportedPreBoot, /// Start microvm error: {0} StartMicrovm(#[from] StartMicrovmError), + /// Generic vhost-user device error: {0} + VhostUserDevice(#[from] VhostUserDeviceConfigError), /// Vsock config error: {0} VsockConfig(#[from] VsockConfigError), } @@ -456,6 +462,7 @@ impl<'a> PrebootApiController<'a> { InsertBlockDevice(config) => self.insert_block_device(config), InsertPmemDevice(config) => self.insert_pmem_device(config), InsertNetworkDevice(config) => self.insert_net_device(config), + InsertVhostUserDevice(config) => self.insert_vhost_user_device(config), LoadSnapshot(config) => self .load_snapshot(&config) .map_err(VmmActionError::LoadSnapshot), @@ -536,6 +543,17 @@ impl<'a> PrebootApiController<'a> { .map_err(VmmActionError::PmemDevice) } + fn insert_vhost_user_device( + &mut self, + cfg: VhostUserDeviceConfig, + ) -> Result { + self.boot_path = true; + self.vm_resources + .build_vhost_user_device(cfg) + .map(|()| VmmData::Empty) + .map_err(VmmActionError::VhostUserDevice) + } + fn set_balloon_device(&mut self, cfg: BalloonDeviceConfig) -> Result { self.boot_path = true; self.vm_resources @@ -803,6 +821,7 @@ impl RuntimeApiController { | InsertBlockDevice(_) | InsertPmemDevice(_) | InsertNetworkDevice(_) + | InsertVhostUserDevice(_) | LoadSnapshot(_) | PutCpuConfiguration(_) | SetBalloonDevice(_) diff --git a/src/vmm/src/vmm_config/mod.rs b/src/vmm/src/vmm_config/mod.rs index 9a4c104ce3a..206fbc22602 100644 --- a/src/vmm/src/vmm_config/mod.rs +++ b/src/vmm/src/vmm_config/mod.rs @@ -33,6 +33,8 @@ pub mod pmem; /// Wrapper for configuring microVM snapshots and the microVM state. pub mod serial; pub mod snapshot; +/// Wrapper for configuring generic vhost-user devices attached to the microVM. +pub mod vhost_user_device; /// Wrapper for configuring the vsock devices attached to the microVM. pub mod vsock; diff --git a/src/vmm/src/vmm_config/vhost_user_device.rs b/src/vmm/src/vmm_config/vhost_user_device.rs new file mode 100644 index 00000000000..42d1ded66e9 --- /dev/null +++ b/src/vmm/src/vmm_config/vhost_user_device.rs @@ -0,0 +1,84 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, Mutex}; + +use serde::{Deserialize, Serialize}; + +use crate::devices::virtio::vhost_user_generic::VhostUserGenericError; +use crate::devices::virtio::vhost_user_generic::device::VhostUserGeneric; + +/// Errors associated with operations on a generic vhost-user device. +#[derive(Debug, thiserror::Error, displaydoc::Display)] +pub enum VhostUserDeviceConfigError { + /// Unable to create the generic vhost-user device: {0} + CreateDevice(#[from] VhostUserGenericError), +} + +/// Use this structure to set up a generic vhost-user device before booting the kernel. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct VhostUserDeviceConfig { + /// Unique identifier of the device. + pub id: String, + /// The virtio device type ID as defined in the virtio specification. + /// For example: 26 for virtio-fs, 8 for virtio-scsi. + /// The backend is responsible for handling the corresponding device protocol. + pub device_type: u8, + /// Path to the vhost-user backend Unix domain socket. + pub socket: String, + /// Number of virtqueues to configure for this device. + pub num_queues: u64, + /// Queue size. Defaults to 256 if not specified. + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_size: Option, +} + +/// Wrapper for the collection that holds all generic vhost-user devices. +#[derive(Debug, Default)] +pub struct VhostUserDeviceBuilder { + /// The list of generic vhost-user devices. + pub devices: Vec>>, +} + +impl VhostUserDeviceBuilder { + /// Build a device from the config and add it to the collection. + /// + /// If a device with the same ID already exists, it is replaced. + pub fn build( + &mut self, + config: VhostUserDeviceConfig, + ) -> Result<(), VhostUserDeviceConfigError> { + let position = self + .devices + .iter() + .position(|d| d.lock().expect("Poisoned lock").id == config.id); + + let device = Arc::new(Mutex::new(VhostUserGeneric::new(config)?)); + + if let Some(index) = position { + self.devices[index] = device; + } else { + self.devices.push(device); + } + + Ok(()) + } + + /// Returns a vec with the structures used to configure the devices. + pub fn configs(&self) -> Vec { + self.devices + .iter() + .map(|d| { + let d = d.lock().expect("Poisoned lock"); + VhostUserDeviceConfig { + id: d.id.clone(), + device_type: d.device_type_id as u8, + socket: d.vu_handle.socket_path.clone(), + num_queues: d.queues.len() as u64, + queue_size: d.queues.first().map(|q| q.size), + } + }) + .collect() + } +} diff --git a/tests/framework/http_api.py b/tests/framework/http_api.py index b59ae9024df..7696868737e 100644 --- a/tests/framework/http_api.py +++ b/tests/framework/http_api.py @@ -184,3 +184,4 @@ def __init__(self, api_usocket_full_name, *, validate=True, on_error=None): self.pmem = Resource(self, "/pmem", "id") self.serial = Resource(self, "/serial") self.memory_hotplug = Resource(self, "/hotplug/memory") + self.vhost_user_device = Resource(self, "/vhost-user-devices", "id") diff --git a/tests/framework/microvm.py b/tests/framework/microvm.py index 023589fe2c9..cf9d2fad598 100644 --- a/tests/framework/microvm.py +++ b/tests/framework/microvm.py @@ -952,6 +952,41 @@ def add_vhost_user_drive( self.disks_vhost_user[drive_id] = backend + def add_vhost_user_generic_device( + self, + device_id, + path_on_host, + device_type, + num_queues, + is_read_only=False, + queue_size=None, + backend_type=VhostUserBlkBackendType.CROSVM, + ): + """Add a generic vhost-user device backed by an external process.""" + + prev = self.disks_vhost_user.pop(device_id, None) + if prev: + prev.kill() + + backend = VhostUserBlkBackend.with_backend( + backend_type, path_on_host, self.chroot(), device_id, is_read_only + ) + + socket = backend.spawn(self.jailer.uid, self.jailer.gid) + + kwargs = { + "id": device_id, + "device_type": device_type, + "socket": socket, + "num_queues": num_queues, + } + if queue_size is not None: + kwargs["queue_size"] = queue_size + + self.api.vhost_user_device.put(**kwargs) + + self.disks_vhost_user[device_id] = backend + def patch_drive(self, drive_id, file=None): """Modify/patch an existing block device.""" if file: diff --git a/tests/integration_tests/functional/test_vhost_user_generic.py b/tests/integration_tests/functional/test_vhost_user_generic.py new file mode 100644 index 00000000000..031f8c4cea4 --- /dev/null +++ b/tests/integration_tests/functional/test_vhost_user_generic.py @@ -0,0 +1,153 @@ +# Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for the generic vhost-user device.""" + +import shutil +from pathlib import Path + +import pytest + +from host_tools.fcmetrics import FcDeviceMetrics + + +# virtio-block device type ID +VIRTIO_BLK_TYPE = 2 + + +@pytest.fixture +def uvm_vhost_user_generic_plain(microvm_factory, guest_kernel, pci_enabled): + """Builds a plain VM with no root volume""" + return microvm_factory.build( + guest_kernel, None, pci=pci_enabled, monitor_memory=False + ) + + +@pytest.fixture +def uvm_vhost_user_generic_booted_ro(uvm_vhost_user_generic_plain, rootfs): + """Returns a VM with a generic vhost-user block rootfs (read-only)""" + vm = uvm_vhost_user_generic_plain + + # We need to setup ssh keys manually because we did not specify rootfs + # in microvm_factory.build method + ssh_key = rootfs.with_suffix(".id_rsa") + vm.ssh_key = ssh_key + vm.spawn() + # The generic device does not set root= in the kernel command line + # automatically, so we pass it explicitly via boot_args. + boot_args = "reboot=k panic=1 nomodule swiotlb=noforce console=ttyS0 root=/dev/vda ro" + if not vm.pci_enabled: + boot_args += " pci=off" + vm.basic_config(add_root_device=False, boot_args=boot_args) + vm.add_vhost_user_generic_device( + "rootfs", + rootfs, + device_type=VIRTIO_BLK_TYPE, + num_queues=1, + is_read_only=True, + ) + vm.add_net_iface() + vm.start() + + return vm + + +@pytest.fixture +def uvm_vhost_user_generic_booted_rw(uvm_vhost_user_generic_plain, rootfs): + """Returns a VM with a generic vhost-user block rootfs (read-write)""" + vm = uvm_vhost_user_generic_plain + + # We need to setup ssh keys manually because we did not specify rootfs + # in microvm_factory.build method + ssh_key = rootfs.with_suffix(".id_rsa") + vm.ssh_key = ssh_key + vm.spawn() + boot_args = "reboot=k panic=1 nomodule swiotlb=noforce console=ttyS0 root=/dev/vda rw" + if not vm.pci_enabled: + boot_args += " pci=off" + vm.basic_config(add_root_device=False, boot_args=boot_args) + # Create a rw rootfs file that is unique to the microVM + rootfs_rw = Path(vm.chroot()) / "rootfs" + shutil.copy(rootfs, rootfs_rw) + vm.add_vhost_user_generic_device( + "rootfs", + rootfs_rw, + device_type=VIRTIO_BLK_TYPE, + num_queues=1, + is_read_only=False, + ) + vm.add_net_iface() + vm.start() + + return vm + + +def _check_block_size(ssh_connection, dev_path, size): + """ + Checks the size of the block device. + """ + _, stdout, stderr = ssh_connection.run("blockdev --getsize64 {}".format(dev_path)) + assert stderr == "" + assert stdout.strip() == str(size) + + +def _check_drives(test_microvm, assert_dict, keys_array): + """ + Checks the info on the block devices. + """ + _, stdout, stderr = test_microvm.ssh.run("blockdev --report") + assert stderr == "" + blockdev_out_lines = stdout.splitlines() + for key in keys_array: + line = int(key.split("-")[0]) + col = int(key.split("-")[1]) + blockdev_out_line_cols = blockdev_out_lines[line].split() + assert blockdev_out_line_cols[col] == assert_dict[key] + + +def test_vhost_user_generic_block(uvm_vhost_user_generic_booted_ro): + """ + Test booting a VM with a generic vhost-user device + configured as a virtio-block root device (read-only). + """ + + vm = uvm_vhost_user_generic_booted_ro + vhost_user_generic_metrics = FcDeviceMetrics( + "vhost_user_generic", 1, aggr_supported=False + ) + + assert_dict = { + "1-0": "ro", + "1-6": "/dev/vda", + } + _check_drives(vm, assert_dict, assert_dict.keys()) + vhost_user_generic_metrics.validate(vm) + + +def test_vhost_user_generic_block_rw(uvm_vhost_user_generic_booted_rw): + """ + Test booting a VM with a generic vhost-user device + configured as a virtio-block root device (read-write). + """ + + vm = uvm_vhost_user_generic_booted_rw + + assert_dict = { + "1-0": "rw", + "1-6": "/dev/vda", + } + _check_drives(vm, assert_dict, assert_dict.keys()) + + +def test_vhost_user_generic_disconnect(uvm_vhost_user_generic_booted_ro): + """ + Test that even if backend is killed, Firecracker is still responsive. + """ + + vm = uvm_vhost_user_generic_booted_ro + + # Killing the backend + vm.disks_vhost_user["rootfs"].kill() + del vm.disks_vhost_user["rootfs"] + + # Verify that Firecracker is still responsive + _config = vm.api.vm_config.get().json()