From add77d221f1620e887d195793d7f73610b0f614d Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Wed, 13 Nov 2024 23:05:45 +0000 Subject: [PATCH] rename propolis-server-config and add spec helper Now that config TOMLs no longer configure Propolis servers, rename the propolis-server-config crate to propolis-config-toml, then add code to convert a config TOML into a set of instance spec configuration options. Most of this code already existed in propolis-server's config TOML processor, which is now deleted. Also tweak a couple of field names in the SoftNPU port spec type to clarify that the name that a port has *within a SoftNPU setup* is distinct from its instance spec component key. --- Cargo.lock | 25 +- Cargo.toml | 2 +- .../src/lib/spec/api_spec_v0.rs | 3 +- .../src/lib/spec/config_toml.rs | 384 ----------------- bin/propolis-server/src/lib/spec/mod.rs | 1 + .../src/instance_spec/components/devices.rs | 8 +- .../Cargo.toml | 4 +- .../src/lib.rs | 16 +- crates/propolis-config-toml/src/spec.rs | 392 ++++++++++++++++++ lib/propolis-client/Cargo.toml | 1 + lib/propolis-client/src/lib.rs | 7 + lib/propolis-client/src/support.rs | 21 +- openapi/propolis-server.json | 10 +- phd-tests/framework/src/test_vm/config.rs | 13 +- phd-tests/framework/src/test_vm/spec.rs | 9 +- 15 files changed, 448 insertions(+), 448 deletions(-) delete mode 100644 bin/propolis-server/src/lib/spec/config_toml.rs rename crates/{propolis-server-config => propolis-config-toml}/Cargo.toml (75%) rename crates/{propolis-server-config => propolis-config-toml}/src/lib.rs (93%) create mode 100644 crates/propolis-config-toml/src/spec.rs diff --git a/Cargo.lock b/Cargo.lock index 03dad4eca..e6878d8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4298,6 +4298,7 @@ dependencies = [ "base64 0.21.7", "futures", "progenitor", + "propolis_api_types", "rand", "reqwest 0.12.7", "schemars", @@ -4310,6 +4311,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "propolis-config-toml" +version = "0.0.0" +dependencies = [ + "cpuid_profile_config", + "propolis-client", + "serde", + "serde_derive", + "thiserror", + "toml 0.7.8", + "uuid", +] + [[package]] name = "propolis-mock-server" version = "0.0.0" @@ -4408,17 +4422,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "propolis-server-config" -version = "0.0.0" -dependencies = [ - "cpuid_profile_config", - "serde", - "serde_derive", - "thiserror", - "toml 0.7.8", -] - [[package]] name = "propolis-standalone" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 45aa599f9..a6ee60268 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ cpuid_profile_config = { path = "crates/cpuid-profile-config" } dladm = { path = "crates/dladm" } nvpair = { path = "crates/nvpair" } nvpair_sys = { path = "crates/nvpair/sys" } -propolis-server-config = { path = "crates/propolis-server-config" } +propolis-config-toml = { path = "crates/propolis-config-toml" } propolis_api_types = { path = "crates/propolis-api-types" } propolis_types = { path = "crates/propolis-types" } rfb = { path = "crates/rfb" } 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 1f88e7e11..6dca51918 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -196,7 +196,7 @@ impl From for InstanceSpecV0 { &mut spec, port_name.clone(), ComponentV0::SoftNpuPort(SoftNpuPortSpec { - name: port_name, + link_name: port.link_name, backend_name: port.backend_name.clone(), }), ); @@ -346,6 +346,7 @@ impl TryFrom for Spec { })?; let port = SoftNpuPort { + link_name: port.link_name, backend_name: port.backend_name, backend_spec, }; diff --git a/bin/propolis-server/src/lib/spec/config_toml.rs b/bin/propolis-server/src/lib/spec/config_toml.rs deleted file mode 100644 index 141c26630..000000000 --- a/bin/propolis-server/src/lib/spec/config_toml.rs +++ /dev/null @@ -1,384 +0,0 @@ -// 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/. - -//! Functions for converting a config TOML into instance spec elements. - -use std::str::{FromStr, ParseBoolError}; - -use propolis_api_types::instance_spec::{ - components::{ - backends::{FileStorageBackend, VirtioNetworkBackend}, - devices::{NvmeDisk, PciPciBridge, VirtioDisk, VirtioNic}, - }, - PciPath, -}; -use thiserror::Error; - -#[cfg(feature = "falcon")] -use propolis_api_types::instance_spec::components::devices::{ - P9fs, SoftNpuP9, SoftNpuPciPort, -}; - -use crate::config; - -use super::{ - Disk, Nic, ParsedDiskRequest, ParsedNicRequest, ParsedPciBridgeRequest, - StorageBackend, StorageDevice, -}; - -#[cfg(feature = "falcon")] -use super::{ParsedSoftNpu, ParsedSoftNpuPort, SoftNpuPort}; - -#[derive(Debug, Error)] -pub(crate) enum ConfigTomlError { - #[error("unrecognized device type {0:?}")] - UnrecognizedDeviceType(String), - - #[error("invalid value {0:?} for enable-pcie flag in chipset")] - EnablePcieParseFailed(String), - - #[error("failed to get PCI path for device {0:?}")] - InvalidPciPath(String), - - #[error("failed to parse PCI path string {0:?}")] - PciPathParseFailed(String, #[source] std::io::Error), - - #[error("invalid storage device kind {kind:?} for device {name:?}")] - InvalidStorageDeviceType { kind: String, name: String }, - - #[error("no backend name for storage device {0:?}")] - NoBackendNameForStorageDevice(String), - - #[error("invalid storage backend kind {kind:?} for backend {name:?}")] - InvalidStorageBackendType { kind: String, name: String }, - - #[error("couldn't find storage device {device:?}'s backend {backend:?}")] - StorageDeviceBackendNotFound { device: String, backend: String }, - - #[error("couldn't get path for file backend {0:?}")] - InvalidFileBackendPath(String), - - #[error("failed to parse read-only option for file backend {0:?}")] - FileBackendReadonlyParseFailed(String, #[source] ParseBoolError), - - #[error("failed to get VNIC name for device {0:?}")] - NoVnicName(String), - - #[cfg(feature = "falcon")] - #[error("failed to get source for p9 device {0:?}")] - NoP9Source(String), - - #[cfg(feature = "falcon")] - #[error("failed to get source for p9 device {0:?}")] - NoP9Target(String), -} - -#[derive(Default)] -pub(super) struct ParsedConfig { - pub(super) enable_pcie: bool, - pub(super) disks: Vec, - pub(super) nics: Vec, - pub(super) pci_bridges: Vec, - - #[cfg(feature = "falcon")] - pub(super) softnpu: ParsedSoftNpu, -} - -impl TryFrom<&config::Config> for ParsedConfig { - type Error = ConfigTomlError; - - fn try_from(config: &config::Config) -> Result { - let mut parsed = ParsedConfig { - enable_pcie: config - .chipset - .options - .get("enable-pcie") - .map(|v| { - v.as_bool().ok_or_else(|| { - ConfigTomlError::EnablePcieParseFailed(v.to_string()) - }) - }) - .transpose()? - .unwrap_or(false), - ..Default::default() - }; - - for (device_name, device) in config.devices.iter() { - let driver = device.driver.as_str(); - match driver { - // If this is a storage device, parse its "block_dev" property - // to get the name of its corresponding backend. - "pci-virtio-block" | "pci-nvme" => { - let device_spec = - parse_storage_device_from_config(device_name, device)?; - - let backend_name = device_spec.backend_name(); - let backend_config = - config.block_devs.get(backend_name).ok_or_else( - || ConfigTomlError::StorageDeviceBackendNotFound { - device: device_name.to_owned(), - backend: backend_name.to_owned(), - }, - )?; - - let backend_spec = parse_storage_backend_from_config( - backend_name, - backend_config, - )?; - - parsed.disks.push(ParsedDiskRequest { - name: device_name.to_owned(), - disk: Disk { device_spec, backend_spec }, - }); - } - "pci-virtio-viona" => { - parsed.nics.push(parse_network_device_from_config( - device_name, - device, - )?); - } - #[cfg(feature = "falcon")] - "softnpu-pci-port" => { - parsed.softnpu.pci_port = - Some(parse_softnpu_pci_port_from_config( - device_name, - device, - )?); - } - #[cfg(feature = "falcon")] - "softnpu-port" => { - parsed.softnpu.ports.push(parse_softnpu_port_from_config( - device_name, - device, - )?); - } - #[cfg(feature = "falcon")] - "softnpu-p9" => { - parsed.softnpu.p9_device = Some( - parse_softnpu_p9_from_config(device_name, device)?, - ); - } - #[cfg(feature = "falcon")] - "pci-virtio-9p" => { - parsed.softnpu.p9fs = - Some(parse_p9fs_from_config(device_name, device)?); - } - _ => { - return Err(ConfigTomlError::UnrecognizedDeviceType( - driver.to_owned(), - )) - } - } - } - - for bridge in config.pci_bridges.iter() { - parsed.pci_bridges.push(parse_pci_bridge_from_config(bridge)?); - } - - Ok(parsed) - } -} - -pub(super) fn parse_storage_backend_from_config( - name: &str, - backend: &config::BlockDevice, -) -> Result { - let backend_spec = match backend.bdtype.as_str() { - "file" => StorageBackend::File(FileStorageBackend { - path: backend - .options - .get("path") - .ok_or_else(|| { - ConfigTomlError::InvalidFileBackendPath(name.to_owned()) - })? - .as_str() - .ok_or_else(|| { - ConfigTomlError::InvalidFileBackendPath(name.to_owned()) - })? - .to_string(), - readonly: match backend.options.get("readonly") { - Some(toml::Value::Boolean(ro)) => Some(*ro), - Some(toml::Value::String(v)) => { - Some(v.parse::().map_err(|e| { - ConfigTomlError::FileBackendReadonlyParseFailed( - name.to_owned(), - e, - ) - })?) - } - _ => None, - } - .unwrap_or(false), - }), - _ => { - return Err(ConfigTomlError::InvalidStorageBackendType { - kind: backend.bdtype.clone(), - name: name.to_owned(), - }); - } - }; - - Ok(backend_spec) -} - -pub(super) fn parse_storage_device_from_config( - name: &str, - device: &config::Device, -) -> Result { - enum Interface { - Virtio, - Nvme, - } - - let interface = match device.driver.as_str() { - "pci-virtio-block" => Interface::Virtio, - "pci-nvme" => Interface::Nvme, - _ => { - return Err(ConfigTomlError::InvalidStorageDeviceType { - kind: device.driver.clone(), - name: name.to_owned(), - }); - } - }; - - let backend_name = device - .options - .get("block_dev") - .ok_or_else(|| { - ConfigTomlError::NoBackendNameForStorageDevice(name.to_owned()) - })? - .as_str() - .ok_or_else(|| { - ConfigTomlError::NoBackendNameForStorageDevice(name.to_owned()) - })? - .to_owned(); - - let pci_path: PciPath = device - .get("pci-path") - .ok_or_else(|| ConfigTomlError::InvalidPciPath(name.to_owned()))?; - - Ok(match interface { - Interface::Virtio => { - StorageDevice::Virtio(VirtioDisk { backend_name, pci_path }) - } - Interface::Nvme => { - StorageDevice::Nvme(NvmeDisk { backend_name, pci_path }) - } - }) -} - -pub(super) fn parse_network_device_from_config( - name: &str, - device: &config::Device, -) -> Result { - let vnic_name = device - .get_string("vnic") - .ok_or_else(|| ConfigTomlError::NoVnicName(name.to_owned()))?; - - let pci_path: PciPath = device - .get("pci-path") - .ok_or_else(|| ConfigTomlError::InvalidPciPath(name.to_owned()))?; - - let (device_name, backend_name) = super::pci_path_to_nic_names(pci_path); - let backend_spec = VirtioNetworkBackend { vnic_name: vnic_name.to_owned() }; - let device_spec = VirtioNic { - backend_name: backend_name.clone(), - // NICs added by the configuration TOML have no control plane- - // supplied correlation IDs. - interface_id: uuid::Uuid::nil(), - pci_path, - }; - - Ok(ParsedNicRequest { - name: device_name, - nic: Nic { device_spec, backend_spec }, - }) -} - -pub(super) fn parse_pci_bridge_from_config( - bridge: &config::PciBridge, -) -> Result { - let pci_path = PciPath::from_str(&bridge.pci_path).map_err(|e| { - ConfigTomlError::PciPathParseFailed(bridge.pci_path.to_string(), e) - })?; - - let name = format!("pci-bridge-{}", bridge.pci_path); - Ok(ParsedPciBridgeRequest { - name, - bridge: PciPciBridge { - downstream_bus: bridge.downstream_bus, - pci_path, - }, - }) -} - -#[cfg(feature = "falcon")] -pub(super) fn parse_softnpu_p9_from_config( - name: &str, - device: &config::Device, -) -> Result { - let pci_path: PciPath = device - .get("pci-path") - .ok_or_else(|| ConfigTomlError::InvalidPciPath(name.to_owned()))?; - - Ok(SoftNpuP9 { pci_path }) -} - -#[cfg(feature = "falcon")] -pub(super) fn parse_softnpu_pci_port_from_config( - name: &str, - device: &config::Device, -) -> Result { - let pci_path: PciPath = device - .get("pci-path") - .ok_or_else(|| ConfigTomlError::InvalidPciPath(name.to_owned()))?; - - Ok(SoftNpuPciPort { pci_path }) -} - -#[cfg(feature = "falcon")] -pub(super) fn parse_softnpu_port_from_config( - name: &str, - device: &config::Device, -) -> Result { - use propolis_api_types::instance_spec::components::backends::DlpiNetworkBackend; - - let vnic_name = device - .get_string("vnic") - .ok_or_else(|| ConfigTomlError::NoVnicName(name.to_owned()))?; - - Ok(ParsedSoftNpuPort { - name: name.to_owned(), - port: SoftNpuPort { - backend_name: vnic_name.to_owned(), - backend_spec: DlpiNetworkBackend { - vnic_name: vnic_name.to_owned(), - }, - }, - }) -} - -#[cfg(feature = "falcon")] -pub(super) fn parse_p9fs_from_config( - name: &str, - device: &config::Device, -) -> Result { - let source = device - .get_string("source") - .ok_or_else(|| ConfigTomlError::NoP9Source(name.to_owned()))?; - let target = device - .get_string("target") - .ok_or_else(|| ConfigTomlError::NoP9Target(name.to_owned()))?; - let pci_path: PciPath = device - .get("pci-path") - .ok_or_else(|| ConfigTomlError::InvalidPciPath(name.to_owned()))?; - - let chunk_size = device.get("chunk_size").unwrap_or(65536); - Ok(P9fs { - source: source.to_owned(), - target: target.to_owned(), - chunk_size, - pci_path, - }) -} diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 8ba51ba1d..c211693fd 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -293,6 +293,7 @@ pub struct MigrationFailure { #[cfg(feature = "falcon")] #[derive(Clone, Debug)] pub struct SoftNpuPort { + pub link_name: String, pub backend_name: String, pub backend_spec: DlpiNetworkBackend, } diff --git a/crates/propolis-api-types/src/instance_spec/components/devices.rs b/crates/propolis-api-types/src/instance_spec/components/devices.rs index 25505cbe6..dcbb8d7e4 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -139,17 +139,17 @@ pub struct SoftNpuPciPort { pub pci_path: PciPath, } -/// Describes a SoftNPU network port. +/// Describes a port in a SoftNPU emulated ASIC. /// /// This is only supported by Propolis servers compiled with the `falcon` /// feature. #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuPort { - /// The name of the SoftNpu port. - pub name: String, + /// The data link name for this port. + pub link_name: String, - /// The name of the device's backend. + /// The name of the port's associated DLPI backend. pub backend_name: String, } diff --git a/crates/propolis-server-config/Cargo.toml b/crates/propolis-config-toml/Cargo.toml similarity index 75% rename from crates/propolis-server-config/Cargo.toml rename to crates/propolis-config-toml/Cargo.toml index e5259e418..937bf1f7b 100644 --- a/crates/propolis-server-config/Cargo.toml +++ b/crates/propolis-config-toml/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "propolis-server-config" +name = "propolis-config-toml" version = "0.0.0" license = "MPL-2.0" edition = "2021" @@ -10,7 +10,9 @@ doctest = false [dependencies] cpuid_profile_config.workspace = true +propolis-client.workspace = true serde.workspace = true serde_derive.workspace = true toml.workspace = true thiserror.workspace = true +uuid.workspace = true diff --git a/crates/propolis-server-config/src/lib.rs b/crates/propolis-config-toml/src/lib.rs similarity index 93% rename from crates/propolis-server-config/src/lib.rs rename to crates/propolis-config-toml/src/lib.rs index 796683485..43189a934 100644 --- a/crates/propolis-server-config/src/lib.rs +++ b/crates/propolis-config-toml/src/lib.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::str::FromStr; use serde_derive::{Deserialize, Serialize}; @@ -11,15 +11,13 @@ use thiserror::Error; pub use cpuid_profile_config::CpuidProfile; +pub mod spec; + /// Configuration for the Propolis server. // NOTE: This is expected to change over time; portions of the hard-coded // configuration will likely become more dynamic. #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct Config { - pub bootrom: PathBuf, - - pub bootrom_version: Option, - #[serde(default, rename = "pci_bridge")] pub pci_bridges: Vec, @@ -38,8 +36,6 @@ pub struct Config { impl Default for Config { fn default() -> Self { Self { - bootrom: PathBuf::new(), - bootrom_version: None, pci_bridges: Vec::new(), chipset: Chipset { options: BTreeMap::new() }, devices: BTreeMap::new(), @@ -151,8 +147,7 @@ mod test { #[test] fn config_can_be_serialized_as_toml() { - let dummy_config = - Config { bootrom: "/boot".into(), ..Default::default() }; + let dummy_config = Config { ..Default::default() }; let serialized = toml::ser::to_string(&dummy_config).unwrap(); let deserialized: Config = toml::de::from_str(&serialized).unwrap(); assert_eq!(dummy_config, deserialized); @@ -161,7 +156,6 @@ mod test { #[test] fn parse_basic_config() { let raw = r#" -bootrom = "/path/to/bootrom" [chipset] chipset-opt = "copt" @@ -183,10 +177,8 @@ path = "/etc/passwd" "#; let cfg: Config = toml::de::from_str(raw).unwrap(); - use std::path::PathBuf; use toml::Value; - assert_eq!(cfg.bootrom, PathBuf::from("/path/to/bootrom")); assert_eq!(cfg.chipset.get_string("chipset-opt"), Some("copt")); assert!(cfg.devices.contains_key("drv0")); diff --git a/crates/propolis-config-toml/src/spec.rs b/crates/propolis-config-toml/src/spec.rs new file mode 100644 index 000000000..73584afdf --- /dev/null +++ b/crates/propolis-config-toml/src/spec.rs @@ -0,0 +1,392 @@ +// 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/. + +//! Functions for converting a [`super::Config`] into instance spec elements. + +use std::{ + collections::BTreeMap, + str::{FromStr, ParseBoolError}, +}; + +use propolis_client::{ + types::{ + ComponentV0, DlpiNetworkBackend, FileStorageBackend, + MigrationFailureInjector, NvmeDisk, P9fs, PciPciBridge, SoftNpuP9, + SoftNpuPciPort, SoftNpuPort, VirtioDisk, VirtioNetworkBackend, + VirtioNic, + }, + PciPath, +}; +use thiserror::Error; + +pub const MIGRATION_FAILURE_DEVICE_NAME: &str = "test-migration-failure"; + +type SpecKey = String; + +#[derive(Debug, Error)] +pub enum TomlToSpecError { + #[error("unrecognized device type {0:?}")] + UnrecognizedDeviceType(String), + + #[error("invalid value {0:?} for enable-pcie flag in chipset")] + EnablePcieParseFailed(String), + + #[error("failed to get PCI path for device {0:?}")] + InvalidPciPath(String), + + #[error("failed to parse PCI path string {0:?}")] + PciPathParseFailed(String, #[source] std::io::Error), + + #[error("invalid storage device kind {kind:?} for device {name:?}")] + InvalidStorageDeviceType { kind: String, name: String }, + + #[error("no backend name for storage device {0:?}")] + NoBackendNameForStorageDevice(String), + + #[error("invalid storage backend kind {kind:?} for backend {name:?}")] + InvalidStorageBackendType { kind: String, name: String }, + + #[error("couldn't find storage device {device:?}'s backend {backend:?}")] + StorageDeviceBackendNotFound { device: String, backend: String }, + + #[error("couldn't get path for file backend {0:?}")] + InvalidFileBackendPath(String), + + #[error("failed to parse read-only option for file backend {0:?}")] + FileBackendReadonlyParseFailed(String, #[source] ParseBoolError), + + #[error("failed to get VNIC name for device {0:?}")] + NoVnicName(String), + + #[error("failed to get source for p9 device {0:?}")] + NoP9Source(String), + + #[error("failed to get source for p9 device {0:?}")] + NoP9Target(String), +} + +#[derive(Clone, Debug, Default)] +pub struct SpecConfig { + pub enable_pcie: bool, + pub components: BTreeMap, +} + +impl TryFrom<&super::Config> for SpecConfig { + type Error = TomlToSpecError; + + fn try_from(config: &super::Config) -> Result { + let mut spec = SpecConfig { + enable_pcie: config + .chipset + .options + .get("enable-pcie") + .map(|v| { + v.as_bool().ok_or_else(|| { + TomlToSpecError::EnablePcieParseFailed(v.to_string()) + }) + }) + .transpose()? + .unwrap_or(false), + ..Default::default() + }; + + for (device_name, device) in config.devices.iter() { + let device_id = SpecKey::from(device_name.clone()); + let driver = device.driver.as_str(); + if device_name == MIGRATION_FAILURE_DEVICE_NAME { + const FAIL_EXPORTS: &str = "fail_exports"; + const FAIL_IMPORTS: &str = "fail_imports"; + let fail_exports = device + .options + .get(FAIL_EXPORTS) + .and_then(|val| val.as_integer()) + .unwrap_or(0) + .max(0) as u32; + let fail_imports = device + .options + .get(FAIL_IMPORTS) + .and_then(|val| val.as_integer()) + .unwrap_or(0) + .max(0) as u32; + + spec.components.insert( + MIGRATION_FAILURE_DEVICE_NAME.to_owned(), + ComponentV0::MigrationFailureInjector( + MigrationFailureInjector { fail_exports, fail_imports }, + ), + ); + + continue; + } + + match driver { + // If this is a storage device, parse its "block_dev" property + // to get the name of its corresponding backend. + "pci-virtio-block" | "pci-nvme" => { + let (device_spec, backend_id) = + parse_storage_device_from_config(device_name, device)?; + + let backend_name = backend_id.to_string(); + let backend_config = + config.block_devs.get(&backend_name).ok_or_else( + || TomlToSpecError::StorageDeviceBackendNotFound { + device: device_name.to_owned(), + backend: backend_name.to_string(), + }, + )?; + + let backend_spec = parse_storage_backend_from_config( + &backend_name, + backend_config, + )?; + + spec.components.insert(device_id, device_spec); + spec.components.insert(backend_id, backend_spec); + } + "pci-virtio-viona" => { + let ParsedNic { device_spec, backend_spec, backend_id } = + parse_network_device_from_config(device_name, device)?; + + spec.components + .insert(device_id, ComponentV0::VirtioNic(device_spec)); + + spec.components.insert( + backend_id, + ComponentV0::VirtioNetworkBackend(backend_spec), + ); + } + "softnpu-pci-port" => { + let pci_path: PciPath = + device.get("pci-path").ok_or_else(|| { + TomlToSpecError::InvalidPciPath( + device_name.to_owned(), + ) + })?; + + spec.components.insert( + device_id, + ComponentV0::SoftNpuPciPort(SoftNpuPciPort { + pci_path, + }), + ); + } + "softnpu-port" => { + let vnic_name = + device.get_string("vnic").ok_or_else(|| { + TomlToSpecError::NoVnicName(device_name.to_owned()) + })?; + + let backend_name = format!("{}:backend", device_id); + + spec.components.insert( + device_id, + ComponentV0::SoftNpuPort(SoftNpuPort { + link_name: device_name.to_string(), + backend_name: backend_name.clone(), + }), + ); + + spec.components.insert( + backend_name, + ComponentV0::DlpiNetworkBackend(DlpiNetworkBackend { + vnic_name: vnic_name.to_owned(), + }), + ); + } + "softnpu-p9" => { + let pci_path: PciPath = + device.get("pci-path").ok_or_else(|| { + TomlToSpecError::InvalidPciPath( + device_name.to_owned(), + ) + })?; + + spec.components.insert( + device_id, + ComponentV0::SoftNpuP9(SoftNpuP9 { pci_path }), + ); + } + "pci-virtio-9p" => { + spec.components.insert( + device_id, + ComponentV0::P9fs(parse_p9fs_from_config( + device_name, + device, + )?), + ); + } + _ => { + return Err(TomlToSpecError::UnrecognizedDeviceType( + driver.to_owned(), + )) + } + } + } + + for bridge in config.pci_bridges.iter() { + let pci_path = + PciPath::from_str(&bridge.pci_path).map_err(|e| { + TomlToSpecError::PciPathParseFailed( + bridge.pci_path.to_string(), + e, + ) + })?; + + spec.components.insert( + format!("pci-bridge-{}", bridge.pci_path), + ComponentV0::PciPciBridge(PciPciBridge { + downstream_bus: bridge.downstream_bus, + pci_path, + }), + ); + } + + Ok(spec) + } +} + +fn parse_storage_device_from_config( + name: &str, + device: &super::Device, +) -> Result<(ComponentV0, SpecKey), TomlToSpecError> { + enum Interface { + Virtio, + Nvme, + } + + let interface = match device.driver.as_str() { + "pci-virtio-block" => Interface::Virtio, + "pci-nvme" => Interface::Nvme, + _ => { + return Err(TomlToSpecError::InvalidStorageDeviceType { + kind: device.driver.clone(), + name: name.to_owned(), + }); + } + }; + + let backend_name = device + .options + .get("block_dev") + .ok_or_else(|| { + TomlToSpecError::NoBackendNameForStorageDevice(name.to_owned()) + })? + .as_str() + .ok_or_else(|| { + TomlToSpecError::NoBackendNameForStorageDevice(name.to_owned()) + })? + .to_string(); + + let pci_path: PciPath = device + .get("pci-path") + .ok_or_else(|| TomlToSpecError::InvalidPciPath(name.to_owned()))?; + + let id_to_return = backend_name.clone(); + Ok(( + match interface { + Interface::Virtio => { + ComponentV0::VirtioDisk(VirtioDisk { backend_name, pci_path }) + } + Interface::Nvme => { + ComponentV0::NvmeDisk(NvmeDisk { backend_name, pci_path }) + } + }, + id_to_return, + )) +} + +fn parse_storage_backend_from_config( + name: &str, + backend: &super::BlockDevice, +) -> Result { + let backend_spec = match backend.bdtype.as_str() { + "file" => ComponentV0::FileStorageBackend(FileStorageBackend { + path: backend + .options + .get("path") + .ok_or_else(|| { + TomlToSpecError::InvalidFileBackendPath(name.to_owned()) + })? + .as_str() + .ok_or_else(|| { + TomlToSpecError::InvalidFileBackendPath(name.to_owned()) + })? + .to_string(), + readonly: match backend.options.get("readonly") { + Some(toml::Value::Boolean(ro)) => Some(*ro), + Some(toml::Value::String(v)) => { + Some(v.parse::().map_err(|e| { + TomlToSpecError::FileBackendReadonlyParseFailed( + name.to_owned(), + e, + ) + })?) + } + _ => None, + } + .unwrap_or(false), + }), + _ => { + return Err(TomlToSpecError::InvalidStorageBackendType { + kind: backend.bdtype.clone(), + name: name.to_owned(), + }); + } + }; + + Ok(backend_spec) +} + +struct ParsedNic { + device_spec: VirtioNic, + backend_spec: VirtioNetworkBackend, + backend_id: SpecKey, +} + +fn parse_network_device_from_config( + name: &str, + device: &super::Device, +) -> Result { + let vnic_name = device + .get_string("vnic") + .ok_or_else(|| TomlToSpecError::NoVnicName(name.to_owned()))?; + + let pci_path: PciPath = device + .get("pci-path") + .ok_or_else(|| TomlToSpecError::InvalidPciPath(name.to_owned()))?; + + let backend_id = format!("{name}-backend"); + Ok(ParsedNic { + device_spec: VirtioNic { + backend_name: backend_id.clone(), + interface_id: uuid::Uuid::nil(), + pci_path, + }, + backend_spec: VirtioNetworkBackend { vnic_name: vnic_name.to_owned() }, + backend_id, + }) +} + +fn parse_p9fs_from_config( + name: &str, + device: &super::Device, +) -> Result { + let source = device + .get_string("source") + .ok_or_else(|| TomlToSpecError::NoP9Source(name.to_owned()))?; + let target = device + .get_string("target") + .ok_or_else(|| TomlToSpecError::NoP9Target(name.to_owned()))?; + let pci_path: PciPath = device + .get("pci-path") + .ok_or_else(|| TomlToSpecError::InvalidPciPath(name.to_owned()))?; + + let chunk_size = device.get("chunk_size").unwrap_or(65536); + Ok(P9fs { + source: source.to_owned(), + target: target.to_owned(), + chunk_size, + pci_path, + }) +} diff --git a/lib/propolis-client/Cargo.toml b/lib/propolis-client/Cargo.toml index 93b3cec86..355efa94a 100644 --- a/lib/propolis-client/Cargo.toml +++ b/lib/propolis-client/Cargo.toml @@ -11,6 +11,7 @@ async-trait.workspace = true base64.workspace = true futures.workspace = true progenitor.workspace = true +propolis_api_types.workspace = true rand.workspace = true reqwest = { workspace = true, features = ["json", "rustls-tls"] } schemars = { workspace = true, features = ["uuid1"] } diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index 7e780f831..03305aeaf 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -4,10 +4,17 @@ //! A client for the Propolis hypervisor frontend's server API. +// Re-export the PCI path type from propolis_api_types so that users can access +// its constructor and From impls. +pub use propolis_api_types::instance_spec::PciPath; + progenitor::generate_api!( spec = "../../openapi/propolis-server.json", interface = Builder, tags = Separate, + replace = { + PciPath = crate::PciPath, + }, patch = { // Some Crucible-related bits are re-exported through simulated // sled-agent and thus require JsonSchema diff --git a/lib/propolis-client/src/support.rs b/lib/propolis-client/src/support.rs index bcf5f10bc..228d309f3 100644 --- a/lib/propolis-client/src/support.rs +++ b/lib/propolis-client/src/support.rs @@ -18,28 +18,9 @@ use tokio_tungstenite::tungstenite::{Error as WSError, Message as WSMessage}; // re-export as an escape hatch for crate-version-matching problems pub use tokio_tungstenite::{tungstenite, WebSocketStream}; -use crate::types::{Chipset, I440Fx, PciPath}; +use crate::types::{Chipset, I440Fx}; use crate::Client as PropolisClient; -const PCI_DEV_PER_BUS: u8 = 32; -const PCI_FUNC_PER_DEV: u8 = 8; - -impl PciPath { - pub const fn new( - bus: u8, - device: u8, - function: u8, - ) -> Result { - if device > PCI_DEV_PER_BUS { - Err("device outside possible range") - } else if function > PCI_FUNC_PER_DEV { - Err("function outside possible range") - } else { - Ok(Self { bus, device, function }) - } - } -} - impl Default for Chipset { fn default() -> Self { Self::I440Fx(I440Fx { enable_pcie: false }) diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 1d69ebadf..b02e9036c 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -1851,21 +1851,21 @@ "additionalProperties": false }, "SoftNpuPort": { - "description": "Describes a SoftNPU network port.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", "type": "object", "properties": { "backend_name": { - "description": "The name of the device's backend.", + "description": "The name of the port's associated DLPI backend.", "type": "string" }, - "name": { - "description": "The name of the SoftNpu port.", + "link_name": { + "description": "The data link name for this port.", "type": "string" } }, "required": [ "backend_name", - "name" + "link_name" ], "additionalProperties": false }, diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index be39b19ca..9c3497dbd 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -6,11 +6,14 @@ use std::sync::Arc; use anyhow::Context; use cpuid_utils::CpuidIdent; -use propolis_client::types::{ - Board, BootOrderEntry, BootSettings, Chipset, ComponentV0, Cpuid, - CpuidEntry, CpuidVendor, InstanceMetadata, InstanceSpecV0, - MigrationFailureInjector, NvmeDisk, PciPath, SerialPort, SerialPortNumber, - VirtioDisk, +use propolis_client::{ + types::{ + Board, BootOrderEntry, BootSettings, Chipset, ComponentV0, Cpuid, + CpuidEntry, CpuidVendor, InstanceMetadata, InstanceSpecV0, + MigrationFailureInjector, NvmeDisk, SerialPort, SerialPortNumber, + VirtioDisk, + }, + PciPath, }; use uuid::Uuid; diff --git a/phd-tests/framework/src/test_vm/spec.rs b/phd-tests/framework/src/test_vm/spec.rs index 761fffd72..dc276b2df 100644 --- a/phd-tests/framework/src/test_vm/spec.rs +++ b/phd-tests/framework/src/test_vm/spec.rs @@ -9,8 +9,9 @@ use crate::{ guest_os::GuestOsKind, }; use camino::Utf8PathBuf; -use propolis_client::types::{ - ComponentV0, DiskRequest, InstanceMetadata, InstanceSpecV0, PciPath, Slot, +use propolis_client::{ + types::{ComponentV0, DiskRequest, InstanceMetadata, InstanceSpecV0, Slot}, + PciPath, }; use uuid::Uuid; @@ -91,11 +92,11 @@ impl VmSpec { } fn convert_to_slot(pci_path: PciPath) -> anyhow::Result { - match pci_path.device { + match pci_path.device() { dev @ 0x10..=0x17 => Ok(Slot(dev - 0x10)), _ => Err(anyhow::anyhow!( "PCI device number {} out of range", - pci_path.device + pci_path.device() )), } }