-
Notifications
You must be signed in to change notification settings - Fork 2.6k
feat(virtio): add generic vhost-user frontend device #5773
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
ebbc801
d6f530b
ef84896
dafe574
ce025ec
91ae093
0695d0e
5e292b9
388b31d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 forwarded** to the backend. The | ||
| backend owns the configuration space in read-only mode from the guest | ||
| perspective. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a Firecracker limitation?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, This is an implementation limitation, not a protocol one. The vhost-user protocol supports forwarding writes via VHOST_USER_SET_CONFIG. I deferred it since most backends (virtiofsd, SPDK) don't rely on guest-initiated config writes, and the existing vhost-user block device in Firecracker follows the same pattern. Happy to add it in this PR if you think it should be included. |
||
| - **The backend must be started before Firecracker.** Firecracker | ||
| connects to the socket during device configuration and will return an | ||
| error if the backend is not available. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before Firecracker is started, or before the device is attached? I’m guessing you mean the latter.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right, before the device is attached via the PUT /vhost-user-devices/{id} API call. Will fix the wording, thanks. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<ParsedRequest, RequestError> { | ||
| 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::<VhostUserDeviceConfig>(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)); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing this PR, I found that a value of
"num_queues": 1did not work, but instead would fail with --And a value of
"num_queues": 3did not work either, but would allowmount -t virtiofs myfs /mntbutls /mntwould hang.Only
"num_queues": 2worked correctly for me, where I was able to successfully use the shared directory to read/write files.