From dea2a6a87497c811179aa88e099193cdb024c022 Mon Sep 17 00:00:00 2001 From: Mike Zeller Date: Mon, 9 Mar 2026 18:03:25 -0400 Subject: [PATCH 1/4] [jj-spr] initial version Created using jj-spr 0.1.0 --- bin/propolis-cli/src/main.rs | 37 +- bin/propolis-server/src/lib/initializer.rs | 36 ++ .../src/lib/spec/api_spec_v0.rs | 347 +++++++++--------- bin/propolis-server/src/lib/spec/builder.rs | 25 ++ bin/propolis-server/src/lib/spec/mod.rs | 52 ++- bin/propolis-server/src/lib/vm/ensure.rs | 1 + .../src/add_vsock/api.rs | 62 ++++ .../src/add_vsock/components/devices.rs | 21 ++ .../src/add_vsock/components/mod.rs | 5 + .../src/add_vsock/instance_spec.rs | 202 ++++++++++ .../src/add_vsock/mod.rs | 11 + .../propolis-api-types-versions/src/latest.rs | 15 +- crates/propolis-api-types-versions/src/lib.rs | 2 + crates/propolis-config-toml/src/spec.rs | 42 +-- crates/propolis-server-api/src/lib.rs | 50 ++- lib/propolis/src/hw/virtio/vsock.rs | 4 +- .../propolis-server-2.0.0-d68a9f.json.gitstub | 1 + ...json => propolis-server-3.0.0-10da2b.json} | 50 ++- .../propolis-server-latest.json | 2 +- phd-tests/framework/src/disk/crucible.rs | 8 +- phd-tests/framework/src/disk/file.rs | 6 +- phd-tests/framework/src/disk/in_memory.rs | 6 +- phd-tests/framework/src/disk/mod.rs | 4 +- phd-tests/framework/src/test_vm/config.rs | 12 +- phd-tests/framework/src/test_vm/mod.rs | 6 +- phd-tests/framework/src/test_vm/spec.rs | 4 +- 26 files changed, 761 insertions(+), 250 deletions(-) create mode 100644 crates/propolis-api-types-versions/src/add_vsock/api.rs create mode 100644 crates/propolis-api-types-versions/src/add_vsock/components/devices.rs create mode 100644 crates/propolis-api-types-versions/src/add_vsock/components/mod.rs create mode 100644 crates/propolis-api-types-versions/src/add_vsock/instance_spec.rs create mode 100644 crates/propolis-api-types-versions/src/add_vsock/mod.rs create mode 100644 openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub rename openapi/propolis-server/{propolis-server-2.0.0-d68a9f.json => propolis-server-3.0.0-10da2b.json} (97%) diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index 49fd3980c..3e3367cf6 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -17,11 +17,11 @@ use clap::{Args, Parser, Subcommand}; use futures::{future, SinkExt}; use newtype_uuid::{GenericUuid, TypedUuid, TypedUuidKind, TypedUuidTag}; use propolis_client::instance_spec::{ - BlobStorageBackend, Board, Chipset, ComponentV0, CrucibleStorageBackend, + BlobStorageBackend, Board, Chipset, Component, CrucibleStorageBackend, GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceMetadata, InstanceProperties, InstanceSpec, InstanceSpecGetResponse, NvmeDisk, PciPath, QemuPvpanic, ReplacementComponent, SerialPort, SerialPortNumber, - SpecKey, VirtioDisk, + SpecKey, VirtioDisk, VirtioSocket, }; use propolis_client::support::nvme_serial_from_str; use propolis_client::types::{ @@ -207,7 +207,7 @@ struct VmConfig { fn add_component_to_spec( spec: &mut InstanceSpec, id: SpecKey, - component: ComponentV0, + component: Component, ) -> anyhow::Result<()> { use std::collections::btree_map::Entry; match spec.components.entry(id) { @@ -236,7 +236,7 @@ struct DiskRequest { #[derive(Clone, Debug)] struct ParsedDiskRequest { device_id: SpecKey, - device_spec: ComponentV0, + device_spec: Component, backend_id: SpecKey, backend_spec: CrucibleStorageBackend, } @@ -255,11 +255,11 @@ impl DiskRequest { format!("processing disk request {:?}", self.name) })?; let device_spec = match self.device.as_ref() { - "virtio" => ComponentV0::VirtioDisk(VirtioDisk { + "virtio" => Component::VirtioDisk(VirtioDisk { backend_id: backend_id.clone(), pci_path, }), - "nvme" => ComponentV0::NvmeDisk(NvmeDisk { + "nvme" => Component::NvmeDisk(NvmeDisk { backend_id: backend_id.clone(), pci_path, serial_number: nvme_serial_from_str(&self.name, b' '), @@ -373,7 +373,7 @@ impl VmConfig { add_component_to_spec( &mut spec, backend_id, - ComponentV0::CrucibleStorageBackend(backend_spec), + Component::CrucibleStorageBackend(backend_spec), )?; } @@ -389,7 +389,7 @@ impl VmConfig { add_component_to_spec( &mut spec, SpecKey::Name(CLOUD_INIT_NAME.to_owned()), - ComponentV0::VirtioDisk(VirtioDisk { + Component::VirtioDisk(VirtioDisk { backend_id: SpecKey::Name( CLOUD_INIT_BACKEND_NAME.to_owned(), ), @@ -400,7 +400,7 @@ impl VmConfig { add_component_to_spec( &mut spec, SpecKey::Name(CLOUD_INIT_BACKEND_NAME.to_owned()), - ComponentV0::BlobStorageBackend(BlobStorageBackend { + Component::BlobStorageBackend(BlobStorageBackend { base64: bytes, readonly: true, }), @@ -415,7 +415,7 @@ impl VmConfig { add_component_to_spec( &mut spec, SpecKey::Name(name.to_owned()), - ComponentV0::SerialPort(SerialPort { num: port }), + Component::SerialPort(SerialPort { num: port }), )?; } @@ -423,12 +423,12 @@ impl VmConfig { if !spec .components .iter() - .any(|(_, c)| matches!(c, ComponentV0::SoftNpuPort(_))) + .any(|(_, c)| matches!(c, Component::SoftNpuPort(_))) { add_component_to_spec( &mut spec, SpecKey::Name("com4".to_owned()), - ComponentV0::SerialPort(SerialPort { + Component::SerialPort(SerialPort { num: SerialPortNumber::Com4, }), )?; @@ -437,7 +437,18 @@ impl VmConfig { add_component_to_spec( &mut spec, SpecKey::Name("pvpanic".to_owned()), - ComponentV0::QemuPvpanic(QemuPvpanic { enable_isa: true }), + Component::QemuPvpanic(QemuPvpanic { enable_isa: true }), + )?; + + add_component_to_spec( + &mut spec, + SpecKey::Name("vsock".to_owned()), + Component::VirtioSocket(VirtioSocket { + // TODO (PullRequest): Update these values to what omicron will + // use. + guest_cid: 16, + pci_path: PciPath::new(0, 0x19, 0).unwrap(), + }), )?; Ok(spec) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index 6f616ce1e..efceb127d 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -4,6 +4,7 @@ use std::convert::TryInto; use std::fs::File; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::num::{NonZeroU8, NonZeroUsize}; use std::os::unix::fs::FileTypeExt; use std::sync::Arc; @@ -476,6 +477,41 @@ impl MachineInitializer<'_> { Ok(()) } + pub fn initialize_vsock( + &mut self, + chipset: &RegisteredChipset, + ) -> Result<(), MachineInitError> { + use propolis::vsock::proxy::VsockPortMapping; + + // Port 8008 - VM Attestation RFD 605 + const ATTESTATION_PORT: u16 = 8008; + const ATTESTATION: SocketAddr = SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + ATTESTATION_PORT, + ); + + if let Some(vsock) = &self.spec.vsock { + let bdf: pci::Bdf = vsock.spec.pci_path.into(); + + let mappings = vec![VsockPortMapping::new( + ATTESTATION_PORT.into(), + ATTESTATION, + )]; + + let device = virtio::PciVirtioSock::new( + 256, + vsock.spec.guest_cid as u32, + self.log.new(slog::o!("dev" => "virtio-sock")), + mappings, + ); + + self.devices.insert(vsock.id.clone(), device.clone()); + chipset.pci_attach(bdf, device); + } + + Ok(()) + } + async fn create_storage_backend_from_spec( &mut self, backend_spec: &StorageBackend, diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index 704084ca8..9cb18d832 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -70,9 +70,13 @@ impl From for v1::instance_spec::InstanceSpec { #[cfg(feature = "falcon")] softnpu, - // Not part of `v1::instance_spec::InstanceSpec`. Added in `InstanceSpec` in API - // Version 2.0.0. + // Not part of `v1::instance_spec::InstanceSpec`. Added in + // `InstanceSpec` in API Version 2.0.0. smbios_type1_input: _, + + // Not part of `v1::instance_spec::InstanceSpec`. Added in + // `InstanceSpec` in API Version 3.0.0. + vsock: _, } = val; // Inserts a component entry into the supplied map, asserting first that @@ -240,182 +244,191 @@ impl TryFrom for Spec { fn try_from( value: v1::instance_spec::InstanceSpec, ) -> Result { - let mut builder = SpecBuilder::with_instance_spec_board(value.board)?; - let mut devices: Vec<(SpecKey, v1::instance_spec::Component)> = vec![]; - let mut boot_settings = None; - let mut storage_backends: BTreeMap = - BTreeMap::new(); - let mut viona_backends: BTreeMap = - BTreeMap::new(); - let mut dlpi_backends: BTreeMap = - BTreeMap::new(); - - for (id, component) in value.components.into_iter() { - match component { - v1::instance_spec::Component::CrucibleStorageBackend(_) - | v1::instance_spec::Component::FileStorageBackend(_) - | v1::instance_spec::Component::BlobStorageBackend(_) => { - storage_backends.insert( - id, - component.try_into().expect( - "component is known to be a storage backend", - ), - ); - } - v1::instance_spec::Component::VirtioNetworkBackend(viona) => { - viona_backends.insert(id, viona); - } - v1::instance_spec::Component::DlpiNetworkBackend(dlpi) => { - dlpi_backends.insert(id, dlpi); - } - device => { - devices.push((id, device)); - } + Ok(v1_to_spec_builder(value)?.finish()) + } +} + +/// Parses a v1 instance spec into a [`SpecBuilder`], validating component +/// names, PCI paths, and backend references along the way. Callers can add +/// additional (non-v1) components to the builder before calling `finish()`. +pub(crate) fn v1_to_spec_builder( + value: v1::instance_spec::InstanceSpec, +) -> Result { + let mut builder = SpecBuilder::with_instance_spec_board(value.board)?; + let mut devices: Vec<(SpecKey, v1::instance_spec::Component)> = vec![]; + let mut boot_settings = None; + let mut storage_backends: BTreeMap = + BTreeMap::new(); + let mut viona_backends: BTreeMap = + BTreeMap::new(); + let mut dlpi_backends: BTreeMap = + BTreeMap::new(); + + for (id, component) in value.components.into_iter() { + match component { + v1::instance_spec::Component::CrucibleStorageBackend(_) + | v1::instance_spec::Component::FileStorageBackend(_) + | v1::instance_spec::Component::BlobStorageBackend(_) => { + storage_backends.insert( + id, + component.try_into().expect( + "component is known to be a storage backend", + ), + ); + } + v1::instance_spec::Component::VirtioNetworkBackend(viona) => { + viona_backends.insert(id, viona); + } + v1::instance_spec::Component::DlpiNetworkBackend(dlpi) => { + dlpi_backends.insert(id, dlpi); + } + device => { + devices.push((id, device)); } } + } - for (device_id, device_spec) in devices { - match device_spec { - v1::instance_spec::Component::VirtioDisk(_) - | v1::instance_spec::Component::NvmeDisk(_) => { - let device_spec = StorageDevice::try_from(device_spec) - .expect("component is known to be a disk"); - - let (_, backend_spec) = storage_backends - .remove_entry(device_spec.backend_id()) - .ok_or_else(|| { - ApiSpecError::StorageBackendNotFound { - backend: device_spec.backend_id().to_owned(), - device: device_id.clone(), - } - })?; - - builder.add_storage_device( - device_id, - Disk { device_spec, backend_spec }, - )?; - } - v1::instance_spec::Component::VirtioNic(nic) => { - let (_, backend_spec) = viona_backends - .remove_entry(&nic.backend_id) - .ok_or_else(|| { - ApiSpecError::NetworkBackendNotFound { - backend: nic.backend_id.clone(), - device: device_id.clone(), - } - })?; - - builder.add_network_device( - device_id, - Nic { device_spec: nic, backend_spec }, - )?; - } - v1::instance_spec::Component::SerialPort(port) => { - builder.add_serial_port(device_id, port.num)?; - } - v1::instance_spec::Component::PciPciBridge(bridge) => { - builder.add_pci_bridge(device_id, bridge)?; - } - v1::instance_spec::Component::QemuPvpanic(pvpanic) => { - builder.add_pvpanic_device(QemuPvpanic { - id: device_id, - spec: pvpanic, + for (device_id, device_spec) in devices { + match device_spec { + v1::instance_spec::Component::VirtioDisk(_) + | v1::instance_spec::Component::NvmeDisk(_) => { + let device_spec = StorageDevice::try_from(device_spec) + .expect("component is known to be a disk"); + + let (_, backend_spec) = storage_backends + .remove_entry(device_spec.backend_id()) + .ok_or_else(|| { + ApiSpecError::StorageBackendNotFound { + backend: device_spec.backend_id().to_owned(), + device: device_id.clone(), + } })?; - } - v1::instance_spec::Component::BootSettings(settings) => { - // The builder returns an error if its caller tries to add - // a boot option that isn't in the set of attached disks. - // Since there may be more disk devices left in the - // component map, just capture the boot order for now and - // apply it to the builder later. - boot_settings = Some((device_id, settings)); - } - #[cfg(not(feature = "failure-injection"))] - v1::instance_spec::Component::MigrationFailureInjector(_) => { - return Err(ApiSpecError::FeatureCompiledOut { - component: device_id, - feature: "failure-injection", - }); - } - #[cfg(feature = "failure-injection")] - v1::instance_spec::Component::MigrationFailureInjector(mig) => { - builder.add_migration_failure_device(MigrationFailure { - id: device_id, - spec: mig, + + builder.add_storage_device( + device_id, + Disk { device_spec, backend_spec }, + )?; + } + v1::instance_spec::Component::VirtioNic(nic) => { + let (_, backend_spec) = viona_backends + .remove_entry(&nic.backend_id) + .ok_or_else(|| { + ApiSpecError::NetworkBackendNotFound { + backend: nic.backend_id.clone(), + device: device_id.clone(), + } })?; - } - #[cfg(not(feature = "falcon"))] - v1::instance_spec::Component::SoftNpuPciPort(_) - | v1::instance_spec::Component::SoftNpuPort(_) - | v1::instance_spec::Component::SoftNpuP9(_) - | v1::instance_spec::Component::P9fs(_) => { - return Err(ApiSpecError::FeatureCompiledOut { - component: device_id, - feature: "falcon", - }); - } - #[cfg(feature = "falcon")] - v1::instance_spec::Component::SoftNpuPciPort(port) => { - builder.set_softnpu_pci_port(port)?; - } - #[cfg(feature = "falcon")] - v1::instance_spec::Component::SoftNpuPort(port) => { - let (_, backend_spec) = dlpi_backends - .remove_entry(&port.backend_id) - .ok_or_else(|| { - ApiSpecError::NetworkBackendNotFound { - backend: port.backend_id.clone(), - device: device_id.clone(), - } - })?; - - let port = SoftNpuPort { - link_name: port.link_name, - backend_name: port.backend_id, - backend_spec, - }; - - builder.add_softnpu_port(device_id, port)?; - } - #[cfg(feature = "falcon")] - v1::instance_spec::Component::SoftNpuP9(p9) => { - builder.set_softnpu_p9(p9)?; - } - #[cfg(feature = "falcon")] - v1::instance_spec::Component::P9fs(p9fs) => { - builder.set_p9fs(p9fs)?; - } - v1::instance_spec::Component::CrucibleStorageBackend(_) - | v1::instance_spec::Component::FileStorageBackend(_) - | v1::instance_spec::Component::BlobStorageBackend(_) - | v1::instance_spec::Component::VirtioNetworkBackend(_) - | v1::instance_spec::Component::DlpiNetworkBackend(_) => { - unreachable!("already filtered out backends") - } + + builder.add_network_device( + device_id, + Nic { device_spec: nic, backend_spec }, + )?; } - } + v1::instance_spec::Component::SerialPort(port) => { + builder.add_serial_port(device_id, port.num)?; + } + v1::instance_spec::Component::PciPciBridge(bridge) => { + builder.add_pci_bridge(device_id, bridge)?; + } + v1::instance_spec::Component::QemuPvpanic(pvpanic) => { + builder.add_pvpanic_device(QemuPvpanic { + id: device_id, + spec: pvpanic, + })?; + } + v1::instance_spec::Component::BootSettings(settings) => { + // The builder returns an error if its caller tries to add + // a boot option that isn't in the set of attached disks. + // Since there may be more disk devices left in the + // component map, just capture the boot order for now and + // apply it to the builder later. + boot_settings = Some((device_id, settings)); + } + #[cfg(not(feature = "failure-injection"))] + v1::instance_spec::Component::MigrationFailureInjector(_) => { + return Err(ApiSpecError::FeatureCompiledOut { + component: device_id, + feature: "failure-injection", + }); + } + #[cfg(feature = "failure-injection")] + v1::instance_spec::Component::MigrationFailureInjector(mig) => { + builder.add_migration_failure_device(MigrationFailure { + id: device_id, + spec: mig, + })?; + } + #[cfg(not(feature = "falcon"))] + v1::instance_spec::Component::SoftNpuPciPort(_) + | v1::instance_spec::Component::SoftNpuPort(_) + | v1::instance_spec::Component::SoftNpuP9(_) + | v1::instance_spec::Component::P9fs(_) => { + return Err(ApiSpecError::FeatureCompiledOut { + component: device_id, + feature: "falcon", + }); + } + #[cfg(feature = "falcon")] + v1::instance_spec::Component::SoftNpuPciPort(port) => { + builder.set_softnpu_pci_port(port)?; + } + #[cfg(feature = "falcon")] + v1::instance_spec::Component::SoftNpuPort(port) => { + let (_, backend_spec) = dlpi_backends + .remove_entry(&port.backend_id) + .ok_or_else(|| { + ApiSpecError::NetworkBackendNotFound { + backend: port.backend_id.clone(), + device: device_id.clone(), + } + })?; - // Now that all disks have been attached, try to establish the boot - // order if one was supplied. - if let Some(settings) = boot_settings { - builder.add_boot_order( - settings.0, - settings.1.order.into_iter().map(Into::into), - )?; - } + let port = SoftNpuPort { + link_name: port.link_name, + backend_name: port.backend_id, + backend_spec, + }; - if let Some(backend) = storage_backends.into_keys().next() { - return Err(ApiSpecError::BackendNotUsed(backend)); + builder.add_softnpu_port(device_id, port)?; + } + #[cfg(feature = "falcon")] + v1::instance_spec::Component::SoftNpuP9(p9) => { + builder.set_softnpu_p9(p9)?; + } + #[cfg(feature = "falcon")] + v1::instance_spec::Component::P9fs(p9fs) => { + builder.set_p9fs(p9fs)?; + } + v1::instance_spec::Component::CrucibleStorageBackend(_) + | v1::instance_spec::Component::FileStorageBackend(_) + | v1::instance_spec::Component::BlobStorageBackend(_) + | v1::instance_spec::Component::VirtioNetworkBackend(_) + | v1::instance_spec::Component::DlpiNetworkBackend(_) => { + unreachable!("already filtered out backends") + } } + } - if let Some(backend) = viona_backends.into_keys().next() { - return Err(ApiSpecError::BackendNotUsed(backend)); - } + // Now that all disks have been attached, try to establish the boot + // order if one was supplied. + if let Some(settings) = boot_settings { + builder.add_boot_order( + settings.0, + settings.1.order.into_iter().map(Into::into), + )?; + } - if let Some(backend) = dlpi_backends.into_keys().next() { - return Err(ApiSpecError::BackendNotUsed(backend)); - } + if let Some(backend) = storage_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); + } - Ok(builder.finish()) + if let Some(backend) = viona_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); } + + if let Some(backend) = dlpi_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); + } + + Ok(builder) } diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index ddd5c5e5b..da5b802cf 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -24,6 +24,7 @@ use crate::spec::SerialPortDevice; use super::{ Board, BootOrderEntry, BootSettings, Disk, Nic, QemuPvpanic, SerialPort, + VirtioSocket, }; #[cfg(feature = "failure-injection")] @@ -50,6 +51,9 @@ pub(crate) enum SpecBuilderError { #[error("pvpanic device already specified")] PvpanicInUse, + #[error("vsock device already specified")] + VsockInUse, + #[cfg(feature = "failure-injection")] #[error("migration failure injection already enabled")] MigrationFailureInjectionInUse, @@ -269,6 +273,27 @@ impl SpecBuilder { Ok(self) } + pub fn add_vsock_device( + &mut self, + vsock: VirtioSocket, + ) -> Result<&Self, SpecBuilderError> { + if self.component_names.contains(&vsock.id) { + return Err(SpecBuilderError::ComponentNameInUse(vsock.id)); + } + + if self.spec.vsock.is_some() { + return Err(SpecBuilderError::VsockInUse); + } + + // TODO validate guest_cid does not have reserved bits set + + self.register_pci_device(vsock.spec.pci_path)?; + self.component_names.insert(vsock.id.clone()); + self.spec.vsock = Some(vsock); + + Ok(self) + } + #[cfg(feature = "failure-injection")] pub fn add_migration_failure_device( &mut self, diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 353ffd4cf..6516c0c29 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -28,12 +28,15 @@ use propolis_api_types::instance_spec::{ devices::{ NvmeDisk, PciPciBridge, QemuPvpanic as QemuPvpanicDesc, SerialPortNumber, VirtioDisk, VirtioNic, + VirtioSocket as VirtioSocketDesc, }, }, PciPath, SpecKey, }; -use propolis_api_types::instance_spec::{InstanceSpec, SmbiosType1Input}; -use propolis_api_types_versions::v1; +use propolis_api_types::instance_spec::{ + Component, InstanceSpec, SmbiosType1Input, +}; +use propolis_api_types_versions::{v1, v2}; use thiserror::Error; #[cfg(feature = "failure-injection")] @@ -53,9 +56,18 @@ pub(crate) mod builder; impl From for InstanceSpec { fn from(val: Spec) -> Self { let smbios = val.smbios_type1_input.clone(); - let v1::instance_spec::InstanceSpec { board, components } = - v1::instance_spec::InstanceSpec::from(val); - InstanceSpec { board, components, smbios } + let vsock = val.vsock.clone(); + + let v1_spec: v1::instance_spec::InstanceSpec = val.into(); + let v2_spec = + v2::instance_spec::InstanceSpec { smbios, ..v1_spec.into() }; + let mut spec: InstanceSpec = v2_spec.into(); + + if let Some(vsock) = vsock { + spec.components + .insert(vsock.id, Component::VirtioSocket(vsock.spec)); + } + spec } } @@ -64,9 +76,25 @@ impl TryFrom for Spec { type Error = ApiSpecError; fn try_from(value: InstanceSpec) -> Result { - let InstanceSpec { board, components, smbios } = value; - let v1 = v1::instance_spec::InstanceSpec { board, components }; - let mut spec: Spec = v1.try_into()?; + // Extract vsock before conversion since it's v3-only and will be + // filtered out during the v3→v2→v1 chain. + let mut vsock_entry = None; + for (id, component) in &value.components { + if let Component::VirtioSocket(v) = component { + vsock_entry = Some(VirtioSocket { id: id.clone(), spec: *v }); + break; + } + } + + let v2_spec: v2::instance_spec::InstanceSpec = value.into(); + let smbios = v2_spec.smbios.clone(); + let v1_spec: v1::instance_spec::InstanceSpec = v2_spec.into(); + + let mut builder = api_spec_v0::v1_to_spec_builder(v1_spec)?; + if let Some(vsock) = vsock_entry { + builder.add_vsock_device(vsock)?; + } + let mut spec = builder.finish(); spec.smbios_type1_input = smbios; Ok(spec) } @@ -98,6 +126,8 @@ pub(crate) struct Spec { pub pci_pci_bridges: BTreeMap, pub pvpanic: Option, + pub vsock: Option, + #[cfg(feature = "failure-injection")] pub migration_failure: Option, @@ -332,6 +362,12 @@ pub struct QemuPvpanic { pub spec: QemuPvpanicDesc, } +#[derive(Clone, Debug)] +pub struct VirtioSocket { + pub id: SpecKey, + pub spec: VirtioSocketDesc, +} + #[cfg(feature = "failure-injection")] #[derive(Clone, Debug)] pub struct MigrationFailure { diff --git a/bin/propolis-server/src/lib/vm/ensure.rs b/bin/propolis-server/src/lib/vm/ensure.rs index 6f6d7de49..040c91c85 100644 --- a/bin/propolis-server/src/lib/vm/ensure.rs +++ b/bin/propolis-server/src/lib/vm/ensure.rs @@ -563,6 +563,7 @@ async fn initialize_vm_objects( &properties, ))?; init.initialize_network_devices(&chipset).await?; + init.initialize_vsock(&chipset)?; #[cfg(feature = "failure-injection")] init.initialize_test_devices(); diff --git a/crates/propolis-api-types-versions/src/add_vsock/api.rs b/crates/propolis-api-types-versions/src/add_vsock/api.rs new file mode 100644 index 000000000..0d5435cbb --- /dev/null +++ b/crates/propolis-api-types-versions/src/add_vsock/api.rs @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! API request and response types for the ADD_VSOCK API version. + +use std::{collections::BTreeMap, net::SocketAddr}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::instance_spec::InstanceSpec; +use crate::v1::instance::{InstanceProperties, ReplacementComponent}; +use crate::v1::instance_spec::SpecKey; +use crate::v2; + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "method", content = "value")] +pub enum InstanceInitializationMethod { + Spec { + spec: InstanceSpec, + }, + MigrationTarget { + migration_id: Uuid, + src_addr: SocketAddr, + replace_components: BTreeMap, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceEnsureRequest { + pub properties: InstanceProperties, + pub init: InstanceInitializationMethod, +} + +impl From + for InstanceInitializationMethod +{ + fn from(old: v2::api::InstanceInitializationMethod) -> Self { + match old { + v2::api::InstanceInitializationMethod::Spec { spec } => { + Self::Spec { spec: spec.into() } + } + v2::api::InstanceInitializationMethod::MigrationTarget { + migration_id, + src_addr, + replace_components, + } => Self::MigrationTarget { + migration_id, + src_addr, + replace_components, + }, + } + } +} + +impl From for InstanceEnsureRequest { + fn from(old: v2::api::InstanceEnsureRequest) -> Self { + Self { properties: old.properties, init: old.init.into() } + } +} diff --git a/crates/propolis-api-types-versions/src/add_vsock/components/devices.rs b/crates/propolis-api-types-versions/src/add_vsock/components/devices.rs new file mode 100644 index 000000000..6ec8766f6 --- /dev/null +++ b/crates/propolis-api-types-versions/src/add_vsock/components/devices.rs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v1::instance_spec::PciPath; + +/// A socket device that presents a virtio-socket interface to the guest. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(deny_unknown_fields)] +pub struct VirtioSocket { + /// The guest's Context ID. + pub guest_cid: u64, + + /// The PCI path at which to attach this device. + pub pci_path: PciPath, +} diff --git a/crates/propolis-api-types-versions/src/add_vsock/components/mod.rs b/crates/propolis-api-types-versions/src/add_vsock/components/mod.rs new file mode 100644 index 000000000..0b45a47b1 --- /dev/null +++ b/crates/propolis-api-types-versions/src/add_vsock/components/mod.rs @@ -0,0 +1,5 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +pub mod devices; diff --git a/crates/propolis-api-types-versions/src/add_vsock/instance_spec.rs b/crates/propolis-api-types-versions/src/add_vsock/instance_spec.rs new file mode 100644 index 000000000..08caf469d --- /dev/null +++ b/crates/propolis-api-types-versions/src/add_vsock/instance_spec.rs @@ -0,0 +1,202 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::collections::BTreeMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v1::components::backends; +use crate::v1::components::board; +use crate::v1::components::devices as v1_devices; +use crate::v1::instance::{InstanceProperties, InstanceState}; +use crate::v1::instance_spec::Component as V1Component; +use crate::v1::instance_spec::SpecKey; +use crate::v2; +use crate::v2::instance_spec::SmbiosType1Input; + +pub use super::components::devices::VirtioSocket; + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde( + deny_unknown_fields, + tag = "type", + content = "component", + rename_all = "snake_case" +)] +pub enum Component { + VirtioDisk(v1_devices::VirtioDisk), + NvmeDisk(v1_devices::NvmeDisk), + VirtioNic(v1_devices::VirtioNic), + SerialPort(v1_devices::SerialPort), + PciPciBridge(v1_devices::PciPciBridge), + QemuPvpanic(v1_devices::QemuPvpanic), + BootSettings(v1_devices::BootSettings), + VirtioSocket(VirtioSocket), + SoftNpuPciPort(v1_devices::SoftNpuPciPort), + SoftNpuPort(v1_devices::SoftNpuPort), + SoftNpuP9(v1_devices::SoftNpuP9), + P9fs(v1_devices::P9fs), + MigrationFailureInjector(v1_devices::MigrationFailureInjector), + CrucibleStorageBackend(backends::CrucibleStorageBackend), + FileStorageBackend(backends::FileStorageBackend), + BlobStorageBackend(backends::BlobStorageBackend), + VirtioNetworkBackend(backends::VirtioNetworkBackend), + DlpiNetworkBackend(backends::DlpiNetworkBackend), +} + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +pub struct InstanceSpec { + pub board: board::Board, + pub components: BTreeMap, + pub smbios: Option, +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "value")] +pub enum InstanceSpecStatus { + WaitingForMigrationSource, + Present(InstanceSpec), +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceSpecGetResponse { + pub properties: InstanceProperties, + pub state: InstanceState, + pub spec: InstanceSpecStatus, +} + +#[derive(thiserror::Error, Debug)] +#[error("no such v1 component: {0:?}")] +pub struct InvalidV1Component(Component); + +impl TryFrom for V1Component { + type Error = InvalidV1Component; + + fn try_from(value: Component) -> Result { + Ok(match value { + Component::VirtioDisk(c) => V1Component::VirtioDisk(c), + Component::NvmeDisk(c) => V1Component::NvmeDisk(c), + Component::VirtioNic(c) => V1Component::VirtioNic(c), + Component::SerialPort(c) => V1Component::SerialPort(c), + Component::PciPciBridge(c) => V1Component::PciPciBridge(c), + Component::QemuPvpanic(c) => V1Component::QemuPvpanic(c), + Component::BootSettings(c) => V1Component::BootSettings(c), + component @ Component::VirtioSocket(_) => { + return Err(InvalidV1Component(component)) + } + Component::SoftNpuPciPort(c) => V1Component::SoftNpuPciPort(c), + Component::SoftNpuPort(c) => V1Component::SoftNpuPort(c), + Component::SoftNpuP9(c) => V1Component::SoftNpuP9(c), + Component::P9fs(c) => V1Component::P9fs(c), + Component::MigrationFailureInjector(c) => { + V1Component::MigrationFailureInjector(c) + } + Component::CrucibleStorageBackend(c) => { + V1Component::CrucibleStorageBackend(c) + } + Component::FileStorageBackend(c) => { + V1Component::FileStorageBackend(c) + } + Component::BlobStorageBackend(c) => { + V1Component::BlobStorageBackend(c) + } + Component::VirtioNetworkBackend(c) => { + V1Component::VirtioNetworkBackend(c) + } + Component::DlpiNetworkBackend(c) => { + V1Component::DlpiNetworkBackend(c) + } + }) + } +} + +impl From for v2::instance_spec::InstanceSpec { + fn from(new: InstanceSpec) -> Self { + Self { + board: new.board, + components: new + .components + .into_iter() + .filter_map(|(k, v)| { + V1Component::try_from(v).ok().map(|c| (k, c)) + }) + .collect(), + smbios: new.smbios, + } + } +} + +impl From for Component { + fn from(old: V1Component) -> Self { + match old { + V1Component::VirtioDisk(c) => Component::VirtioDisk(c), + V1Component::NvmeDisk(c) => Component::NvmeDisk(c), + V1Component::VirtioNic(c) => Component::VirtioNic(c), + V1Component::SerialPort(c) => Component::SerialPort(c), + V1Component::PciPciBridge(c) => Component::PciPciBridge(c), + V1Component::QemuPvpanic(c) => Component::QemuPvpanic(c), + V1Component::BootSettings(c) => Component::BootSettings(c), + V1Component::SoftNpuPciPort(c) => Component::SoftNpuPciPort(c), + V1Component::SoftNpuPort(c) => Component::SoftNpuPort(c), + V1Component::SoftNpuP9(c) => Component::SoftNpuP9(c), + V1Component::P9fs(c) => Component::P9fs(c), + V1Component::MigrationFailureInjector(c) => { + Component::MigrationFailureInjector(c) + } + V1Component::CrucibleStorageBackend(c) => { + Component::CrucibleStorageBackend(c) + } + V1Component::FileStorageBackend(c) => { + Component::FileStorageBackend(c) + } + V1Component::BlobStorageBackend(c) => { + Component::BlobStorageBackend(c) + } + V1Component::VirtioNetworkBackend(c) => { + Component::VirtioNetworkBackend(c) + } + V1Component::DlpiNetworkBackend(c) => { + Component::DlpiNetworkBackend(c) + } + } + } +} + +impl From for v2::instance_spec::InstanceSpecStatus { + fn from(new: InstanceSpecStatus) -> Self { + match new { + InstanceSpecStatus::WaitingForMigrationSource => { + Self::WaitingForMigrationSource + } + InstanceSpecStatus::Present(spec) => Self::Present(spec.into()), + } + } +} + +impl From + for v2::instance_spec::InstanceSpecGetResponse +{ + fn from(new: InstanceSpecGetResponse) -> Self { + Self { + properties: new.properties, + state: new.state, + spec: new.spec.into(), + } + } +} + +impl From for InstanceSpec { + fn from(old: v2::instance_spec::InstanceSpec) -> Self { + Self { + board: old.board, + components: old + .components + .into_iter() + .map(|(k, v)| (k, Component::from(v))) + .collect(), + smbios: old.smbios, + } + } +} diff --git a/crates/propolis-api-types-versions/src/add_vsock/mod.rs b/crates/propolis-api-types-versions/src/add_vsock/mod.rs new file mode 100644 index 000000000..f765674ea --- /dev/null +++ b/crates/propolis-api-types-versions/src/add_vsock/mod.rs @@ -0,0 +1,11 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `ADD_VSOCK` of the Propolis Server API. +//! +//! This version adds support for the virtio-socket device. + +pub mod api; +pub mod components; +pub mod instance_spec; diff --git a/crates/propolis-api-types-versions/src/latest.rs b/crates/propolis-api-types-versions/src/latest.rs index 802663468..160d1f648 100644 --- a/crates/propolis-api-types-versions/src/latest.rs +++ b/crates/propolis-api-types-versions/src/latest.rs @@ -41,6 +41,8 @@ pub mod components { pub use crate::v1::components::devices::SoftNpuPort; pub use crate::v1::components::devices::VirtioDisk; pub use crate::v1::components::devices::VirtioNic; + + pub use crate::v3::components::devices::VirtioSocket; } } @@ -68,12 +70,11 @@ pub mod instance { pub use crate::v1::instance::InstanceStateRequested; pub use crate::v1::instance::ReplacementComponent; - pub use crate::v2::api::InstanceEnsureRequest; - pub use crate::v2::api::InstanceInitializationMethod; + pub use crate::v3::api::InstanceEnsureRequest; + pub use crate::v3::api::InstanceInitializationMethod; } pub mod instance_spec { - pub use crate::v1::instance_spec::Component; pub use crate::v1::instance_spec::CpuidIdent; pub use crate::v1::instance_spec::CpuidValues; pub use crate::v1::instance_spec::CpuidVendor; @@ -81,10 +82,12 @@ pub mod instance_spec { pub use crate::v1::instance_spec::SpecKey; pub use crate::v1::instance_spec::VersionedInstanceSpec; - pub use crate::v2::instance_spec::InstanceSpec; - pub use crate::v2::instance_spec::InstanceSpecGetResponse; - pub use crate::v2::instance_spec::InstanceSpecStatus; pub use crate::v2::instance_spec::SmbiosType1Input; + + pub use crate::v3::instance_spec::Component; + pub use crate::v3::instance_spec::InstanceSpec; + pub use crate::v3::instance_spec::InstanceSpecGetResponse; + pub use crate::v3::instance_spec::InstanceSpecStatus; } pub mod migration { diff --git a/crates/propolis-api-types-versions/src/lib.rs b/crates/propolis-api-types-versions/src/lib.rs index d745931f0..d9853a806 100644 --- a/crates/propolis-api-types-versions/src/lib.rs +++ b/crates/propolis-api-types-versions/src/lib.rs @@ -35,3 +35,5 @@ pub mod latest; pub mod v1; #[path = "programmable_smbios/mod.rs"] pub mod v2; +#[path = "add_vsock/mod.rs"] +pub mod v3; diff --git a/crates/propolis-config-toml/src/spec.rs b/crates/propolis-config-toml/src/spec.rs index ab86fb719..b148ef37b 100644 --- a/crates/propolis-config-toml/src/spec.rs +++ b/crates/propolis-config-toml/src/spec.rs @@ -11,10 +11,10 @@ use std::{ use propolis_client::{ instance_spec::{ - ComponentV0, Cpuid, CpuidVendor, DlpiNetworkBackend, - FileStorageBackend, MigrationFailureInjector, NvmeDisk, P9fs, PciPath, - PciPciBridge, SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, - VirtioDisk, VirtioNetworkBackend, VirtioNic, + Component, Cpuid, CpuidVendor, DlpiNetworkBackend, FileStorageBackend, + MigrationFailureInjector, NvmeDisk, P9fs, PciPath, PciPciBridge, + SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk, + VirtioNetworkBackend, VirtioNic, }, support::nvme_serial_from_str, }; @@ -70,7 +70,7 @@ pub enum TomlToSpecError { #[derive(Clone, Debug, Default)] pub struct SpecConfig { pub enable_pcie: bool, - pub components: BTreeMap, + pub components: BTreeMap, } // Inspired by `api_spec_v0.rs`'s `insert_component` and @@ -84,7 +84,7 @@ pub struct SpecConfig { fn spec_component_add( spec: &mut SpecConfig, key: SpecKey, - component: ComponentV0, + component: Component, ) -> Result<(), TomlToSpecError> { if spec.components.contains_key(&key) { return Err(TomlToSpecError::DuplicateSpecKey(key)); @@ -135,7 +135,7 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, SpecKey::Name(MIGRATION_FAILURE_DEVICE_NAME.to_owned()), - ComponentV0::MigrationFailureInjector( + Component::MigrationFailureInjector( MigrationFailureInjector { fail_exports, fail_imports }, ), )?; @@ -174,13 +174,13 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, device_id, - ComponentV0::VirtioNic(device_spec), + Component::VirtioNic(device_spec), )?; spec_component_add( &mut spec, backend_id, - ComponentV0::VirtioNetworkBackend(backend_spec), + Component::VirtioNetworkBackend(backend_spec), )?; } "softnpu-pci-port" => { @@ -194,9 +194,7 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, device_id, - ComponentV0::SoftNpuPciPort(SoftNpuPciPort { - pci_path, - }), + Component::SoftNpuPciPort(SoftNpuPciPort { pci_path }), )?; } "softnpu-port" => { @@ -211,7 +209,7 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, device_id, - ComponentV0::SoftNpuPort(SoftNpuPort { + Component::SoftNpuPort(SoftNpuPort { link_name: device_name.to_string(), backend_id: backend_name.clone(), }), @@ -220,7 +218,7 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, backend_name, - ComponentV0::DlpiNetworkBackend(DlpiNetworkBackend { + Component::DlpiNetworkBackend(DlpiNetworkBackend { vnic_name: vnic_name.to_owned(), }), )?; @@ -236,14 +234,14 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, device_id, - ComponentV0::SoftNpuP9(SoftNpuP9 { pci_path }), + Component::SoftNpuP9(SoftNpuP9 { pci_path }), )?; } "pci-virtio-9p" => { spec_component_add( &mut spec, device_id, - ComponentV0::P9fs(parse_p9fs_from_config( + Component::P9fs(parse_p9fs_from_config( device_name, device, )?), @@ -269,7 +267,7 @@ impl TryFrom<&super::Config> for SpecConfig { spec_component_add( &mut spec, SpecKey::Name(format!("pci-bridge-{}", bridge.pci_path)), - ComponentV0::PciPciBridge(PciPciBridge { + Component::PciPciBridge(PciPciBridge { downstream_bus: bridge.downstream_bus, pci_path, }), @@ -283,7 +281,7 @@ impl TryFrom<&super::Config> for SpecConfig { fn parse_storage_device_from_config( name: &str, device: &super::Device, -) -> Result<(ComponentV0, SpecKey), TomlToSpecError> { +) -> Result<(Component, SpecKey), TomlToSpecError> { enum Interface { Virtio, Nvme, @@ -322,9 +320,9 @@ fn parse_storage_device_from_config( Ok(( match interface { Interface::Virtio => { - ComponentV0::VirtioDisk(VirtioDisk { backend_id, pci_path }) + Component::VirtioDisk(VirtioDisk { backend_id, pci_path }) } - Interface::Nvme => ComponentV0::NvmeDisk(NvmeDisk { + Interface::Nvme => Component::NvmeDisk(NvmeDisk { backend_id, pci_path, serial_number: nvme_serial_from_str(name, b' '), @@ -337,9 +335,9 @@ fn parse_storage_device_from_config( fn parse_storage_backend_from_config( name: &str, backend: &super::BlockDevice, -) -> Result { +) -> Result { let backend_spec = match backend.bdtype.as_str() { - "file" => ComponentV0::FileStorageBackend(FileStorageBackend { + "file" => Component::FileStorageBackend(FileStorageBackend { path: backend .options .get("path") diff --git a/crates/propolis-server-api/src/lib.rs b/crates/propolis-server-api/src/lib.rs index 5d20b55bf..e821c07b1 100644 --- a/crates/propolis-server-api/src/lib.rs +++ b/crates/propolis-server-api/src/lib.rs @@ -8,7 +8,7 @@ use dropshot::{ WebsocketChannelResult, WebsocketConnection, }; use dropshot_api_manager_types::api_versions; -use propolis_api_types_versions::{latest, v1}; +use propolis_api_types_versions::{latest, v1, v2}; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -22,6 +22,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (3, ADD_VSOCK), (2, PROGRAMMABLE_SMBIOS), (1, INITIAL), ]); @@ -45,7 +46,7 @@ pub trait PropolisServerApi { #[endpoint { method = PUT, path = "/instance", - versions = VERSION_PROGRAMMABLE_SMBIOS.. + versions = VERSION_ADD_VSOCK.. }] async fn instance_ensure( rqctx: RequestContext, @@ -55,6 +56,26 @@ pub trait PropolisServerApi { HttpError, >; + #[endpoint { + operation_id = "instance_ensure", + method = PUT, + path = "/instance", + versions = VERSION_PROGRAMMABLE_SMBIOS..VERSION_ADD_VSOCK + }] + async fn instance_ensure_v2( + rqctx: RequestContext, + request: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + Self::instance_ensure( + rqctx, + request.map(latest::instance::InstanceEnsureRequest::from), + ) + .await + } + #[endpoint { operation_id = "instance_ensure", method = PUT, @@ -68,9 +89,9 @@ pub trait PropolisServerApi { HttpResponseCreated, HttpError, > { - Self::instance_ensure( + Self::instance_ensure_v2( rqctx, - request.map(latest::instance::InstanceEnsureRequest::from), + request.map(v2::api::InstanceEnsureRequest::from), ) .await } @@ -78,7 +99,7 @@ pub trait PropolisServerApi { #[endpoint { method = GET, path = "/instance/spec", - versions = VERSION_PROGRAMMABLE_SMBIOS.. + versions = VERSION_ADD_VSOCK.. }] async fn instance_spec_get( rqctx: RequestContext, @@ -87,6 +108,23 @@ pub trait PropolisServerApi { HttpError, >; + #[endpoint { + operation_id = "instance_spec_get", + method = GET, + path = "/instance/spec", + versions = VERSION_PROGRAMMABLE_SMBIOS..VERSION_ADD_VSOCK + }] + async fn instance_spec_get_v2( + rqctx: RequestContext, + ) -> Result< + HttpResponseOk, + HttpError, + > { + Ok(Self::instance_spec_get(rqctx) + .await? + .map(v2::instance_spec::InstanceSpecGetResponse::from)) + } + #[endpoint { operation_id = "instance_spec_get", method = GET, @@ -99,7 +137,7 @@ pub trait PropolisServerApi { HttpResponseOk, HttpError, > { - Ok(Self::instance_spec_get(rqctx) + Ok(Self::instance_spec_get_v2(rqctx) .await? .map(v1::instance_spec::InstanceSpecGetResponse::from)) } diff --git a/lib/propolis/src/hw/virtio/vsock.rs b/lib/propolis/src/hw/virtio/vsock.rs index 6d68c2fdc..8be8a2d48 100644 --- a/lib/propolis/src/hw/virtio/vsock.rs +++ b/lib/propolis/src/hw/virtio/vsock.rs @@ -250,12 +250,14 @@ impl PciVirtio for PciVirtioSock { impl Lifecycle for PciVirtioSock { fn type_name(&self) -> &'static str { - "pci-virtio-vsock" + "pci-virtio-socket" } fn reset(&self) { self.virtio_state.reset(self); } fn migrate(&'_ self) -> Migrator<'_> { + // TODO (MTZ): + // We need to support migration propolis#1065 Migrator::NonMigratable } } diff --git a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub b/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub new file mode 100644 index 000000000..faa9b4d85 --- /dev/null +++ b/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub @@ -0,0 +1 @@ +fd3636877061da7e951cb1fbce365f7cbf40933c:openapi/propolis-server/propolis-server-2.0.0-d68a9f.json diff --git a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json b/openapi/propolis-server/propolis-server-3.0.0-10da2b.json similarity index 97% rename from openapi/propolis-server/propolis-server-2.0.0-d68a9f.json rename to openapi/propolis-server/propolis-server-3.0.0-10da2b.json index 6c49482e1..2b0b7c77f 100644 --- a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json +++ b/openapi/propolis-server/propolis-server-3.0.0-10da2b.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "2.0.0" + "version": "3.0.0" }, "paths": { "/instance": { @@ -579,7 +579,7 @@ } ] }, - "ComponentV0": { + "Component": { "oneOf": [ { "type": "object", @@ -714,6 +714,25 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioSocket" + }, + "type": { + "type": "string", + "enum": [ + "virtio_socket" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, { "type": "object", "properties": { @@ -1434,7 +1453,7 @@ "components": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/ComponentV0" + "$ref": "#/components/schemas/Component" } }, "smbios": { @@ -2032,6 +2051,31 @@ ], "additionalProperties": false }, + "VirtioSocket": { + "description": "A socket device that presents a virtio-socket interface to the guest.", + "type": "object", + "properties": { + "guest_cid": { + "description": "The guest's Context ID.", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach this device.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "guest_cid", + "pci_path" + ], + "additionalProperties": false + }, "VolumeStatus": { "type": "object", "properties": { diff --git a/openapi/propolis-server/propolis-server-latest.json b/openapi/propolis-server/propolis-server-latest.json index a28007449..0f7255733 120000 --- a/openapi/propolis-server/propolis-server-latest.json +++ b/openapi/propolis-server/propolis-server-latest.json @@ -1 +1 @@ -propolis-server-2.0.0-d68a9f.json \ No newline at end of file +propolis-server-3.0.0-10da2b.json \ No newline at end of file diff --git a/phd-tests/framework/src/disk/crucible.rs b/phd-tests/framework/src/disk/crucible.rs index 6b9694200..e98538f37 100644 --- a/phd-tests/framework/src/disk/crucible.rs +++ b/phd-tests/framework/src/disk/crucible.rs @@ -13,7 +13,7 @@ use std::{ use anyhow::Context; use propolis_client::{ - instance_spec::{ComponentV0, CrucibleStorageBackend}, + instance_spec::{Component, CrucibleStorageBackend}, CrucibleOpts, VolumeConstructionRequest, }; use rand::{rngs::StdRng, RngCore, SeedableRng}; @@ -161,7 +161,7 @@ impl super::DiskConfig for CrucibleDisk { &self.device_name } - fn backend_spec(&self) -> ComponentV0 { + fn backend_spec(&self) -> Component { self.inner.lock().unwrap().backend_spec(self.disk_id) } @@ -381,10 +381,10 @@ impl Inner { }) } - fn backend_spec(&self, disk_id: Uuid) -> ComponentV0 { + fn backend_spec(&self, disk_id: Uuid) -> Component { let vcr = self.vcr(disk_id); - ComponentV0::CrucibleStorageBackend(CrucibleStorageBackend { + Component::CrucibleStorageBackend(CrucibleStorageBackend { request_json: serde_json::to_string(&vcr) .expect("VolumeConstructionRequest should serialize"), readonly: false, diff --git a/phd-tests/framework/src/disk/file.rs b/phd-tests/framework/src/disk/file.rs index c57fd4731..69f4e89c2 100644 --- a/phd-tests/framework/src/disk/file.rs +++ b/phd-tests/framework/src/disk/file.rs @@ -5,7 +5,7 @@ //! Abstractions for disks with a raw file backend. use camino::{Utf8Path, Utf8PathBuf}; -use propolis_client::instance_spec::{ComponentV0, FileStorageBackend}; +use propolis_client::instance_spec::{Component, FileStorageBackend}; use std::num::NonZeroUsize; use tracing::{debug, error, warn}; use uuid::Uuid; @@ -128,8 +128,8 @@ impl super::DiskConfig for FileBackedDisk { &self.device_name } - fn backend_spec(&self) -> ComponentV0 { - ComponentV0::FileStorageBackend(FileStorageBackend { + fn backend_spec(&self) -> Component { + Component::FileStorageBackend(FileStorageBackend { path: self.file.path().to_string(), readonly: false, block_size: 512, diff --git a/phd-tests/framework/src/disk/in_memory.rs b/phd-tests/framework/src/disk/in_memory.rs index 1218f326f..5d7c10112 100644 --- a/phd-tests/framework/src/disk/in_memory.rs +++ b/phd-tests/framework/src/disk/in_memory.rs @@ -4,7 +4,7 @@ //! Abstractions for disks with an in-memory backend. -use propolis_client::instance_spec::{BlobStorageBackend, ComponentV0}; +use propolis_client::instance_spec::{BlobStorageBackend, Component}; use super::DiskConfig; use crate::disk::DeviceName; @@ -34,13 +34,13 @@ impl DiskConfig for InMemoryDisk { &self.device_name } - fn backend_spec(&self) -> ComponentV0 { + fn backend_spec(&self) -> Component { let base64 = base64::Engine::encode( &base64::engine::general_purpose::STANDARD, self.contents.as_slice(), ); - ComponentV0::BlobStorageBackend(BlobStorageBackend { + Component::BlobStorageBackend(BlobStorageBackend { base64, readonly: self.readonly, }) diff --git a/phd-tests/framework/src/disk/mod.rs b/phd-tests/framework/src/disk/mod.rs index e7325b0c0..64fd40fca 100644 --- a/phd-tests/framework/src/disk/mod.rs +++ b/phd-tests/framework/src/disk/mod.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use in_memory::InMemoryDisk; -use propolis_client::instance_spec::ComponentV0; +use propolis_client::instance_spec::Component; use thiserror::Error; use crate::{ @@ -119,7 +119,7 @@ pub trait DiskConfig: std::fmt::Debug + Send + Sync { fn device_name(&self) -> &DeviceName; /// Yields the backend spec for this disk's storage backend. - fn backend_spec(&self) -> ComponentV0; + fn backend_spec(&self) -> Component; /// Yields the guest OS kind of the guest image the disk was created from, /// or `None` if the disk was not created from a guest image. diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index 6e17c84e3..819257de9 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -8,7 +8,7 @@ use anyhow::Context; use cpuid_utils::CpuidIdent; use propolis_client::{ instance_spec::{ - Board, BootOrderEntry, BootSettings, Chipset, ComponentV0, Cpuid, + Board, BootOrderEntry, BootSettings, Chipset, Component, Cpuid, CpuidEntry, CpuidVendor, GuestHypervisorInterface, InstanceMetadata, InstanceSpec, MigrationFailureInjector, NvmeDisk, PciPath, SerialPort, SerialPortNumber, SpecKey, VirtioDisk, @@ -315,13 +315,13 @@ impl<'dr> VmConfig<'dr> { let device_name = hdl.device_name().clone(); let backend_name = device_name.clone().into_backend_name(); let device_spec = match req.interface { - DiskInterface::Virtio => ComponentV0::VirtioDisk(VirtioDisk { + DiskInterface::Virtio => Component::VirtioDisk(VirtioDisk { backend_id: SpecKey::Name( backend_name.clone().into_string(), ), pci_path, }), - DiskInterface::Nvme => ComponentV0::NvmeDisk(NvmeDisk { + DiskInterface::Nvme => Component::NvmeDisk(NvmeDisk { backend_id: SpecKey::Name( backend_name.clone().into_string(), ), @@ -350,14 +350,14 @@ impl<'dr> VmConfig<'dr> { let _old = spec.components.insert( "com1".into(), - ComponentV0::SerialPort(SerialPort { num: SerialPortNumber::Com1 }), + Component::SerialPort(SerialPort { num: SerialPortNumber::Com1 }), ); assert!(_old.is_none()); if let Some(boot_order) = boot_order.as_ref() { let _old = spec.components.insert( "boot-settings".into(), - ComponentV0::BootSettings(BootSettings { + Component::BootSettings(BootSettings { order: boot_order .iter() .map(|item| BootOrderEntry { @@ -372,7 +372,7 @@ impl<'dr> VmConfig<'dr> { if let Some(mig) = migration_failure.as_ref() { let _old = spec.components.insert( "migration-failure".into(), - ComponentV0::MigrationFailureInjector(mig.clone()), + Component::MigrationFailureInjector(mig.clone()), ); assert!(_old.is_none()); } diff --git a/phd-tests/framework/src/test_vm/mod.rs b/phd-tests/framework/src/test_vm/mod.rs index 98686861b..5450810c8 100644 --- a/phd-tests/framework/src/test_vm/mod.rs +++ b/phd-tests/framework/src/test_vm/mod.rs @@ -29,7 +29,7 @@ use core::result::Result as StdResult; use futures::FutureExt; use propolis_client::{ instance_spec::{ - ComponentV0, InstanceProperties, InstanceSpecGetResponse, + Component, InstanceProperties, InstanceSpecGetResponse, ReplacementComponent, }, support::{InstanceSerialConsoleHelper, WSClientOffset}, @@ -707,7 +707,7 @@ impl TestVm { let mut map = ReplacementComponents::new(); for (id, comp) in &self.spec.instance_spec().components { match comp { - ComponentV0::MigrationFailureInjector(inj) => { + Component::MigrationFailureInjector(inj) => { map.insert( id.to_string(), ReplacementComponent::MigrationFailureInjector( @@ -715,7 +715,7 @@ impl TestVm { ), ); } - ComponentV0::CrucibleStorageBackend(be) => { + Component::CrucibleStorageBackend(be) => { map.insert( id.to_string(), ReplacementComponent::CrucibleStorageBackend( diff --git a/phd-tests/framework/src/test_vm/spec.rs b/phd-tests/framework/src/test_vm/spec.rs index b755156f8..bf5c723ac 100644 --- a/phd-tests/framework/src/test_vm/spec.rs +++ b/phd-tests/framework/src/test_vm/spec.rs @@ -10,7 +10,7 @@ use crate::{ }; use camino::Utf8PathBuf; use propolis_client::instance_spec::{ - ComponentV0, InstanceMetadata, InstanceSpec, + Component, InstanceMetadata, InstanceSpec, }; use uuid::Uuid; @@ -90,7 +90,7 @@ impl VmSpec { .into_backend_name() .into_string() .into(); - if let Some(ComponentV0::CrucibleStorageBackend(_)) = + if let Some(Component::CrucibleStorageBackend(_)) = spec.components.get(&backend_name) { spec.components.insert(backend_name, backend_spec); From 19c5faa556ebab465ac6cf42a133125c41b9e2c5 Mon Sep 17 00:00:00 2001 From: Mike Zeller Date: Mon, 9 Mar 2026 18:04:25 -0400 Subject: [PATCH 2/4] cargo fmt Created using jj-spr 0.1.0 --- .../src/lib/spec/api_spec_v0.rs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index 9cb18d832..4aa0d3e21 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -271,9 +271,9 @@ pub(crate) fn v1_to_spec_builder( | v1::instance_spec::Component::BlobStorageBackend(_) => { storage_backends.insert( id, - component.try_into().expect( - "component is known to be a storage backend", - ), + component + .try_into() + .expect("component is known to be a storage backend"), ); } v1::instance_spec::Component::VirtioNetworkBackend(viona) => { @@ -297,11 +297,9 @@ pub(crate) fn v1_to_spec_builder( let (_, backend_spec) = storage_backends .remove_entry(device_spec.backend_id()) - .ok_or_else(|| { - ApiSpecError::StorageBackendNotFound { - backend: device_spec.backend_id().to_owned(), - device: device_id.clone(), - } + .ok_or_else(|| ApiSpecError::StorageBackendNotFound { + backend: device_spec.backend_id().to_owned(), + device: device_id.clone(), })?; builder.add_storage_device( @@ -312,11 +310,9 @@ pub(crate) fn v1_to_spec_builder( v1::instance_spec::Component::VirtioNic(nic) => { let (_, backend_spec) = viona_backends .remove_entry(&nic.backend_id) - .ok_or_else(|| { - ApiSpecError::NetworkBackendNotFound { - backend: nic.backend_id.clone(), - device: device_id.clone(), - } + .ok_or_else(|| ApiSpecError::NetworkBackendNotFound { + backend: nic.backend_id.clone(), + device: device_id.clone(), })?; builder.add_network_device( @@ -376,11 +372,9 @@ pub(crate) fn v1_to_spec_builder( v1::instance_spec::Component::SoftNpuPort(port) => { let (_, backend_spec) = dlpi_backends .remove_entry(&port.backend_id) - .ok_or_else(|| { - ApiSpecError::NetworkBackendNotFound { - backend: port.backend_id.clone(), - device: device_id.clone(), - } + .ok_or_else(|| ApiSpecError::NetworkBackendNotFound { + backend: port.backend_id.clone(), + device: device_id.clone(), })?; let port = SoftNpuPort { From 37b199e3d61cd6819d42dc75525867424f478420 Mon Sep 17 00:00:00 2001 From: Mike Zeller Date: Mon, 16 Mar 2026 17:46:40 -0400 Subject: [PATCH 3/4] fixes Created using jj-spr 0.1.0 --- bin/propolis-cli/src/main.rs | 13 +---- bin/propolis-server/src/lib/initializer.rs | 16 ++++-- bin/propolis-standalone/src/config.rs | 2 +- bin/propolis-standalone/src/main.rs | 7 ++- crates/propolis-config-toml/src/spec.rs | 29 +++++++++- lib/propolis/src/hw/virtio/vsock.rs | 9 ++- lib/propolis/src/vsock/mod.rs | 28 ++++++++++ lib/propolis/src/vsock/packet.rs | 32 ++++++----- lib/propolis/src/vsock/poller.rs | 65 +++++++++++----------- lib/propolis/src/vsock/proxy.rs | 3 +- 10 files changed, 131 insertions(+), 73 deletions(-) diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index 3e3367cf6..d4b22f9a8 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -21,7 +21,7 @@ use propolis_client::instance_spec::{ GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceMetadata, InstanceProperties, InstanceSpec, InstanceSpecGetResponse, NvmeDisk, PciPath, QemuPvpanic, ReplacementComponent, SerialPort, SerialPortNumber, - SpecKey, VirtioDisk, VirtioSocket, + SpecKey, VirtioDisk, }; use propolis_client::support::nvme_serial_from_str; use propolis_client::types::{ @@ -440,17 +440,6 @@ impl VmConfig { Component::QemuPvpanic(QemuPvpanic { enable_isa: true }), )?; - add_component_to_spec( - &mut spec, - SpecKey::Name("vsock".to_owned()), - Component::VirtioSocket(VirtioSocket { - // TODO (PullRequest): Update these values to what omicron will - // use. - guest_cid: 16, - pci_path: PciPath::new(0, 0x19, 0).unwrap(), - }), - )?; - Ok(spec) } } diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index efceb127d..a4fc6bb70 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -47,6 +47,7 @@ use propolis::hw::uart::LpcUart; use propolis::hw::{nvme, virtio}; use propolis::intr_pins; use propolis::vmm::{self, Builder, Machine}; +use propolis::vsock::GuestCid; use propolis_api_types::instance::InstanceProperties; use propolis_api_types::instance_spec::components::devices::SerialPortNumber; use propolis_api_types::instance_spec::{self, SpecKey}; @@ -483,9 +484,9 @@ impl MachineInitializer<'_> { ) -> Result<(), MachineInitError> { use propolis::vsock::proxy::VsockPortMapping; - // Port 8008 - VM Attestation RFD 605 - const ATTESTATION_PORT: u16 = 8008; - const ATTESTATION: SocketAddr = SocketAddr::new( + // OANA Port 605 - VM Attestation RFD 605 + const ATTESTATION_PORT: u16 = 605; + const ATTESTATION_ADDR: SocketAddr = SocketAddr::new( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), ATTESTATION_PORT, ); @@ -495,13 +496,16 @@ impl MachineInitializer<'_> { let mappings = vec![VsockPortMapping::new( ATTESTATION_PORT.into(), - ATTESTATION, + ATTESTATION_ADDR, )]; + let guest_cid = GuestCid::try_from(vsock.spec.guest_cid) + .context("guest cid")?; + let device = virtio::PciVirtioSock::new( 256, - vsock.spec.guest_cid as u32, - self.log.new(slog::o!("dev" => "virtio-sock")), + guest_cid, + self.log.new(slog::o!("dev" => "virtio-socket")), mappings, ); diff --git a/bin/propolis-standalone/src/config.rs b/bin/propolis-standalone/src/config.rs index 3aac56a73..3cf1157c0 100644 --- a/bin/propolis-standalone/src/config.rs +++ b/bin/propolis-standalone/src/config.rs @@ -173,7 +173,7 @@ impl VionaDeviceParams { #[derive(Deserialize)] pub struct VsockDevice { - pub guest_cid: u32, + pub guest_cid: u64, pub port_mappings: Vec, } diff --git a/bin/propolis-standalone/src/main.rs b/bin/propolis-standalone/src/main.rs index 90135d80e..3b3eec631 100644 --- a/bin/propolis-standalone/src/main.rs +++ b/bin/propolis-standalone/src/main.rs @@ -16,6 +16,7 @@ use anyhow::Context; use clap::Parser; use futures::future::BoxFuture; use propolis::hw::qemu::pvpanic::QemuPvpanic; +use propolis::vsock::GuestCid; use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor}; use slog::{o, Drain}; use strum::IntoEnumIterator; @@ -1319,12 +1320,14 @@ fn setup_instance( guard.inventory.register(&pvpanic); } } - "pci-virtio-vsock" => { + "pci-virtio-socket" => { let config = config::VsockDevice::from_opts(&dev.options)?; let bdf = bdf.unwrap(); + let guest_cid = GuestCid::try_from(config.guest_cid) + .context("guest cid")?; let vsock = hw::virtio::PciVirtioSock::new( 512, - config.guest_cid, + guest_cid, log.new(slog::o!("dev" => "vsock")), config.port_mappings, ); diff --git a/crates/propolis-config-toml/src/spec.rs b/crates/propolis-config-toml/src/spec.rs index b148ef37b..2b6371892 100644 --- a/crates/propolis-config-toml/src/spec.rs +++ b/crates/propolis-config-toml/src/spec.rs @@ -14,7 +14,7 @@ use propolis_client::{ Component, Cpuid, CpuidVendor, DlpiNetworkBackend, FileStorageBackend, MigrationFailureInjector, NvmeDisk, P9fs, PciPath, PciPciBridge, SoftNpuP9, SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk, - VirtioNetworkBackend, VirtioNic, + VirtioNetworkBackend, VirtioNic, VirtioSocket, }, support::nvme_serial_from_str, }; @@ -65,6 +65,9 @@ pub enum TomlToSpecError { #[error("failed to get source for p9 device {0:?}")] NoP9Target(String), + + #[error("failed to get guest_cid for vsock device {0:?}")] + NoVsockGuestCid(String), } #[derive(Clone, Debug, Default)] @@ -247,6 +250,16 @@ impl TryFrom<&super::Config> for SpecConfig { )?), )?; } + "pci-virtio-socket" => { + spec_component_add( + &mut spec, + device_id, + Component::VirtioSocket(parse_vsock_from_config( + device_name, + device, + )?), + )?; + } _ => { return Err(TomlToSpecError::UnrecognizedDeviceType( driver.to_owned(), @@ -429,6 +442,20 @@ fn parse_p9fs_from_config( }) } +fn parse_vsock_from_config( + name: &str, + device: &super::Device, +) -> Result { + let guest_cid = device + .get("guest_cid") + .ok_or_else(|| TomlToSpecError::NoVsockGuestCid(name.to_owned()))?; + let pci_path: PciPath = device + .get("pci-path") + .ok_or_else(|| TomlToSpecError::InvalidPciPath(name.to_owned()))?; + + Ok(VirtioSocket { guest_cid, pci_path }) +} + /// Translate a parsed TOML-provided `CpuidEntry` into a `propolis-server` /// API-style `CpuidEntry`. /// diff --git a/lib/propolis/src/hw/virtio/vsock.rs b/lib/propolis/src/hw/virtio/vsock.rs index 8be8a2d48..f39b47c30 100644 --- a/lib/propolis/src/hw/virtio/vsock.rs +++ b/lib/propolis/src/hw/virtio/vsock.rs @@ -20,6 +20,7 @@ use crate::vsock::packet::VsockPacket; use crate::vsock::packet::VsockPacketError; use crate::vsock::packet::VsockPacketHeader; use crate::vsock::proxy::VsockPortMapping; +use crate::vsock::GuestCid; use crate::vsock::VsockBackend; use crate::vsock::VsockProxy; @@ -151,7 +152,7 @@ impl VsockVq { } pub struct PciVirtioSock { - cid: u32, + cid: GuestCid, backend: VsockProxy, virtio_state: PciVirtioState, pci_state: pci::DeviceState, @@ -160,7 +161,7 @@ pub struct PciVirtioSock { impl PciVirtioSock { pub fn new( queue_size: u16, - cid: u32, + cid: GuestCid, log: Logger, port_mappings: Vec, ) -> Arc { @@ -200,9 +201,7 @@ impl VirtioDevice for PciVirtioSock { VSOCK_DEV_REGS.process(&mut rwo, |id, rwo| match rwo { RWOp::Read(ro) => match id { VsockReg::GuestCid => { - ro.write_u32(self.cid); - // The upper 32 bits are reserved and zeroed. - ro.fill(0); + ro.write_u64(self.cid.get()); } }, RWOp::Write(_) => {} diff --git a/lib/propolis/src/vsock/mod.rs b/lib/propolis/src/vsock/mod.rs index 709aad407..64ec0c57f 100644 --- a/lib/propolis/src/vsock/mod.rs +++ b/lib/propolis/src/vsock/mod.rs @@ -18,6 +18,34 @@ pub use proxy::VsockProxy; /// Well-known CID for the host pub(crate) const VSOCK_HOST_CID: u64 = 2; +#[derive(Debug, thiserror::Error)] +#[error("guest cid {0} contains reserved bits")] +pub struct InvalidGuestCid(u64); + +#[derive(Debug, Copy, Clone)] +pub struct GuestCid(u64); + +impl GuestCid { + pub const fn get(&self) -> u64 { + self.0 + } +} + +impl TryFrom for GuestCid { + type Error = InvalidGuestCid; + + fn try_from(value: u64) -> Result { + match value { + // Within the virtio spec cid 0,1, and 2 have special meaning. + cid @ 0..=2 => Err(InvalidGuestCid(cid)), + // The upper 32 bits of the cid are reserved + cid if cid >> 32 != 0 => Err(InvalidGuestCid(value)), + // This cid is valid + cid => Ok(GuestCid(cid)), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum VsockError { #[error("failed to send virt queue notification for queue {}", queue)] diff --git a/lib/propolis/src/vsock/packet.rs b/lib/propolis/src/vsock/packet.rs index ec341ebde..84c9ef5a0 100644 --- a/lib/propolis/src/vsock/packet.rs +++ b/lib/propolis/src/vsock/packet.rs @@ -7,7 +7,7 @@ use zerocopy::byteorder::little_endian::{U16, U32, U64}; use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::vsock::proxy::CONN_TX_BUF_SIZE; -use crate::vsock::VSOCK_HOST_CID; +use crate::vsock::{GuestCid, VSOCK_HOST_CID}; bitflags! { /// Shutdown flags for VIRTIO_VSOCK_OP_SHUTDOWN @@ -67,7 +67,7 @@ pub enum VsockPacketOp { /// Represents the required vsock fields required to send a packet to a guest. pub struct VsockGuestAddr { /// Guest context ID - pub guest_cid: u32, + pub guest_cid: GuestCid, /// Host port pub src_port: u32, /// Guest port @@ -146,19 +146,25 @@ impl VsockPacketHeader { } } - pub const fn set_src_cid(&mut self, cid: u32) -> &mut Self { - // The spec states: - // - // The upper 32 bits of src_cid and dst_cid are reserved and zeroed. - self.src_cid = U64::new(cid as u64); + pub const fn set_host_src_cid(&mut self) -> &mut Self { + self.src_cid = U64::new(VSOCK_HOST_CID); self } - pub const fn set_dst_cid(&mut self, cid: u32) -> &mut Self { - // The spec states: - // - // The upper 32 bits of src_cid and dst_cid are reserved and zeroed. - self.dst_cid = U64::new(cid as u64); + #[cfg(test)] + pub const fn set_src_cid(&mut self, cid: GuestCid) -> &mut Self { + self.src_cid = U64::new(cid.get()); + self + } + + #[cfg(test)] + pub const fn set_dst_cid_raw(&mut self, cid: u64) -> &mut Self { + self.dst_cid = U64::new(cid); + self + } + + pub const fn set_dst_cid(&mut self, cid: GuestCid) -> &mut Self { + self.dst_cid = U64::new(cid.get()); self } @@ -225,7 +231,7 @@ impl VsockPacket { fn new(addr: VsockGuestAddr, op: VsockPacketOp) -> Self { let mut header = VsockPacketHeader::new(); header - .set_src_cid(VSOCK_HOST_CID as u32) + .set_host_src_cid() .set_dst_cid(addr.guest_cid) .set_src_port(addr.src_port) .set_dst_port(addr.dst_port) diff --git a/lib/propolis/src/vsock/poller.rs b/lib/propolis/src/vsock/poller.rs index 75b073898..904598528 100644 --- a/lib/propolis/src/vsock/poller.rs +++ b/lib/propolis/src/vsock/poller.rs @@ -25,6 +25,7 @@ use crate::vsock::packet::VsockSocketType; use crate::vsock::proxy::ConnKey; use crate::vsock::proxy::VsockPortMapping; use crate::vsock::proxy::VsockProxyConn; +use crate::vsock::GuestCid; use crate::vsock::VSOCK_HOST_CID; use super::packet::VsockGuestAddr; @@ -105,7 +106,7 @@ enum RxEvent { pub struct VsockPoller { log: Logger, /// The guest context id - guest_cid: u32, + guest_cid: GuestCid, /// Port mappings we are proxying packets to and from port_mappings: IdHashMap, /// The event port fd. @@ -126,7 +127,7 @@ impl VsockPoller { /// This poller is responsible for driving virtio-socket connections between /// the guest VM and host sockets. pub fn new( - cid: u32, + cid: GuestCid, queues: VsockVq, log: Logger, port_mappings: IdHashMap, @@ -312,7 +313,7 @@ impl VsockPoller { } // If the packet is not coming from our guest drop it. - if packet.header.src_cid() != u64::from(self.guest_cid) { + if packet.header.src_cid() != self.guest_cid.get() { // Note that we could send a RST here but technically we should // not know how to address this guest cid as it's not the one // we assigned to our guest. @@ -798,7 +799,7 @@ impl PortEvent { impl VsockGuestAddr { /// Helper function to construct a `[VsockGuestAddr]` from a guest context /// ID and a `[ConnKey]`. - fn from_conn_key(guest_cid: u32, key: ConnKey) -> Self { + fn from_conn_key(guest_cid: GuestCid, key: ConnKey) -> Self { Self { guest_cid, src_port: key.host_port, dst_port: key.guest_port } } } @@ -821,7 +822,7 @@ mod tests { VsockPacketFlags, VsockPacketHeader, VsockPacketOp, VsockSocketType, }; use crate::vsock::proxy::{VsockPortMapping, CONN_TX_BUF_SIZE}; - use crate::vsock::VSOCK_HOST_CID; + use crate::vsock::{GuestCid, VSOCK_HOST_CID}; use super::VsockPoller; @@ -967,7 +968,7 @@ mod tests { fn request_receives_response() { let vsock_port = 3000; let guest_port = 1234; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (_listener, backends) = bind_test_backend(vsock_port); let mut harness = VsockTestHarness::new(); @@ -982,7 +983,7 @@ mod tests { let mut hdr = VsockPacketHeader::new(); hdr.set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1000,7 +1001,7 @@ mod tests { let (resp_hdr, _) = harness.read_vsock_packet(0); assert_eq!(resp_hdr.op(), Some(VsockPacketOp::Response)); assert_eq!(resp_hdr.src_cid(), VSOCK_HOST_CID); - assert_eq!(resp_hdr.dst_cid(), guest_cid as u64); + assert_eq!(resp_hdr.dst_cid(), guest_cid.get()); assert_eq!(resp_hdr.src_port(), vsock_port); assert_eq!(resp_hdr.dst_port(), guest_port); assert_eq!(resp_hdr.socket_type(), Some(VsockSocketType::Stream)); @@ -1011,7 +1012,7 @@ mod tests { #[test] fn rw_with_invalid_socket_type_receives_rst() { - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let mut harness = VsockTestHarness::new(); let vq = harness.make_vsock_vq(); @@ -1026,7 +1027,7 @@ mod tests { let mut hdr = VsockPacketHeader::new(); hdr.set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(5555) .set_dst_port(8080) .set_len(0) @@ -1044,7 +1045,7 @@ mod tests { let (resp_hdr, _) = harness.read_vsock_packet(0); assert_eq!(resp_hdr.op(), Some(VsockPacketOp::Reset)); assert_eq!(resp_hdr.src_cid(), VSOCK_HOST_CID); - assert_eq!(resp_hdr.dst_cid(), guest_cid as u64); + assert_eq!(resp_hdr.dst_cid(), guest_cid.get()); assert_eq!(resp_hdr.src_port(), 8080); assert_eq!(resp_hdr.dst_port(), 5555); @@ -1056,7 +1057,7 @@ mod tests { fn request_then_rw_delivers_data() { let vsock_port = 3000; let guest_port = 1234; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1076,7 +1077,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1100,7 +1101,7 @@ mod tests { let mut rw_hdr = VsockPacketHeader::new(); rw_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(payload.len() as u32) @@ -1128,7 +1129,7 @@ mod tests { fn credit_update_sent_after_flushing_half_buffer() { let vsock_port = 4000; let guest_port = 2000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1149,7 +1150,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1181,7 +1182,7 @@ mod tests { let mut rw_hdr = VsockPacketHeader::new(); rw_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(payload.len() as u32) @@ -1214,7 +1215,7 @@ mod tests { let (hdr, _) = harness.read_vsock_packet(i); if hdr.op() == Some(VsockPacketOp::CreditUpdate) { assert_eq!(hdr.src_cid(), VSOCK_HOST_CID); - assert_eq!(hdr.dst_cid(), guest_cid as u64); + assert_eq!(hdr.dst_cid(), guest_cid.get()); assert_eq!(hdr.src_port(), vsock_port); assert_eq!(hdr.dst_port(), guest_port); assert_eq!(hdr.buf_alloc(), CONN_TX_BUF_SIZE as u32); @@ -1232,7 +1233,7 @@ mod tests { fn rst_removes_established_connection() { let vsock_port = 5000; let guest_port = 3000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1252,7 +1253,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1274,7 +1275,7 @@ mod tests { let mut rst_hdr = VsockPacketHeader::new(); rst_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1308,7 +1309,7 @@ mod tests { fn end_to_end_guest_to_host() { let vsock_port = 7000; let guest_port = 5000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1329,7 +1330,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1355,7 +1356,7 @@ mod tests { let mut rw_hdr = VsockPacketHeader::new(); rw_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(payload.len() as u32) @@ -1399,7 +1400,7 @@ mod tests { fn rx_blocked_resumes_when_descriptors_available() { let vsock_port = 6000; let guest_port = 4000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1418,7 +1419,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1474,7 +1475,7 @@ mod tests { let vsock_port = 8000; let guest_port = 6000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1499,7 +1500,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1580,7 +1581,7 @@ mod tests { let mut rw_hdr = VsockPacketHeader::new(); rw_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(payload.len() as u32) @@ -1681,7 +1682,7 @@ mod tests { fn host_socket_eof_sends_shutdown() { let vsock_port = 9000; let guest_port = 7000; - let guest_cid: u32 = 50; + let guest_cid = GuestCid::try_from(50).unwrap(); let (listener, backends) = bind_test_backend(vsock_port); listener.set_nonblocking(false).unwrap(); @@ -1702,7 +1703,7 @@ mod tests { let mut req_hdr = VsockPacketHeader::new(); req_hdr .set_src_cid(guest_cid) - .set_dst_cid(VSOCK_HOST_CID as u32) + .set_dst_cid_raw(VSOCK_HOST_CID) .set_src_port(guest_port) .set_dst_port(vsock_port) .set_len(0) @@ -1731,7 +1732,7 @@ mod tests { assert_eq!(hdr.op(), Some(VsockPacketOp::Shutdown)); assert_eq!(hdr.src_cid(), VSOCK_HOST_CID); - assert_eq!(hdr.dst_cid(), guest_cid as u64); + assert_eq!(hdr.dst_cid(), guest_cid.get()); assert_eq!(hdr.src_port(), vsock_port); assert_eq!(hdr.dst_port(), guest_port); assert_eq!( diff --git a/lib/propolis/src/vsock/proxy.rs b/lib/propolis/src/vsock/proxy.rs index 853f9c9ac..67efe15a5 100644 --- a/lib/propolis/src/vsock/proxy.rs +++ b/lib/propolis/src/vsock/proxy.rs @@ -25,6 +25,7 @@ use crate::vsock::packet::VsockPacket; use crate::vsock::packet::VsockPacketHeader; use crate::vsock::poller::VsockPoller; use crate::vsock::poller::VsockPollerNotify; +use crate::vsock::GuestCid; use crate::vsock::VsockBackend; use crate::vsock::VsockError; @@ -326,7 +327,7 @@ pub struct VsockProxy { impl VsockProxy { pub fn new( - cid: u32, + cid: GuestCid, queues: VsockVq, log: Logger, port_mappings: IdHashMap, From 6692a7a746b8bf67aa7e3ee8a4b028280f8c6da7 Mon Sep 17 00:00:00 2001 From: Mike Zeller Date: Mon, 16 Mar 2026 18:06:13 -0400 Subject: [PATCH 4/4] it's always non-illumos Created using jj-spr 0.1.0 --- lib/propolis/src/vsock/poller_stub.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/propolis/src/vsock/poller_stub.rs b/lib/propolis/src/vsock/poller_stub.rs index 71bcdd1ee..ca759b00c 100644 --- a/lib/propolis/src/vsock/poller_stub.rs +++ b/lib/propolis/src/vsock/poller_stub.rs @@ -9,6 +9,7 @@ use slog::Logger; use crate::hw::virtio::vsock::VsockVq; use crate::vsock::proxy::VsockPortMapping; +use crate::vsock::GuestCid; bitflags! { pub struct PollEvents: i32 { @@ -31,7 +32,7 @@ pub struct VsockPoller; impl VsockPoller { pub fn new( - _cid: u32, + _cid: GuestCid, _queues: VsockVq, _log: Logger, _port_mappings: IdHashMap,