Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions bin/propolis-server/src/lib/spec/api_request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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/.

//! Converts device descriptions from an
//! [`propolis_api_types::InstanceEnsureRequest`] into elements that can be
//! added to a spec.

use propolis_api_types::{
instance_spec::{
components::{
backends::{
BlobStorageBackend, CrucibleStorageBackend,
VirtioNetworkBackend,
},
devices::{NvmeDisk, VirtioDisk, VirtioNic},
},
v0::{
NetworkBackendV0, NetworkDeviceV0, StorageBackendV0,
StorageDeviceV0,
},
PciPath,
},
DiskRequest, NetworkInterfaceRequest, Slot,
};
use thiserror::Error;

use super::{ParsedNetworkDevice, ParsedStorageDevice};

#[derive(Debug, Error)]
pub(crate) enum DeviceRequestError {
#[error("invalid storage interface {0} for disk in slot {1}")]
InvalidStorageInterface(String, u8),

#[error("invalid PCI slot {0} for device type {1:?}")]
PciSlotInvalid(u8, SlotType),

#[error("error serializing {0}")]
SerializationError(String, #[source] serde_json::error::Error),
}

/// A type of PCI device. Device numbers on the PCI bus are partitioned by slot
/// type. If a client asks to attach a device of type X to PCI slot Y, the
/// server will assign the Yth device number in X's partition. The partitioning
/// scheme is defined by the implementation of the `slot_to_pci_path` utility
/// function.
#[derive(Clone, Copy, Debug)]
pub(crate) enum SlotType {
Nic,
Disk,
CloudInit,
}

/// Translates a device type and PCI slot (as presented in an instance creation
/// request) into a concrete PCI path. See the documentation for [`SlotType`].
fn slot_to_pci_path(
slot: Slot,
ty: SlotType,
) -> Result<PciPath, DeviceRequestError> {
match ty {
// Slots for NICS: 0x08 -> 0x0F
SlotType::Nic if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x8, 0),
// Slots for Disks: 0x10 -> 0x17
SlotType::Disk if slot.0 <= 7 => PciPath::new(0, slot.0 + 0x10, 0),
// Slot for CloudInit
SlotType::CloudInit if slot.0 == 0 => PciPath::new(0, slot.0 + 0x18, 0),
_ => return Err(DeviceRequestError::PciSlotInvalid(slot.0, ty)),
}
.map_err(|_| DeviceRequestError::PciSlotInvalid(slot.0, ty))
}

pub(super) fn parse_disk_from_request(
disk: &DiskRequest,
) -> Result<ParsedStorageDevice, DeviceRequestError> {
let pci_path = slot_to_pci_path(disk.slot, SlotType::Disk)?;
let device_spec = match disk.device.as_ref() {
"virtio" => StorageDeviceV0::VirtioDisk(VirtioDisk {
backend_name: disk.name.to_string(),
pci_path,
}),
"nvme" => StorageDeviceV0::NvmeDisk(NvmeDisk {
backend_name: disk.name.to_string(),
pci_path,
}),
_ => {
return Err(DeviceRequestError::InvalidStorageInterface(
disk.device.clone(),
disk.slot.0,
))
}
};

let device_name = disk.name.clone();
let backend_name = format!("{}-backend", disk.name);
let backend_spec = StorageBackendV0::Crucible(CrucibleStorageBackend {
request_json: serde_json::to_string(&disk.volume_construction_request)
.map_err(|e| {
DeviceRequestError::SerializationError(disk.name.clone(), e)
})?,
readonly: disk.read_only,
});

Ok(ParsedStorageDevice {
device_name,
device_spec,
backend_name,
backend_spec,
})
}

pub(super) fn parse_cloud_init_from_request(
base64: String,
) -> Result<ParsedStorageDevice, DeviceRequestError> {
let name = "cloud-init";
let pci_path = slot_to_pci_path(Slot(0), SlotType::CloudInit)?;
let backend_name = name.to_string();
let backend_spec =
StorageBackendV0::Blob(BlobStorageBackend { base64, readonly: true });

let device_name = name.to_string();
let device_spec = StorageDeviceV0::VirtioDisk(VirtioDisk {
backend_name: name.to_string(),
pci_path,
});

Ok(ParsedStorageDevice {
device_name,
device_spec,
backend_name,
backend_spec,
})
}

pub(super) fn parse_nic_from_request(
nic: &NetworkInterfaceRequest,
) -> Result<ParsedNetworkDevice, DeviceRequestError> {
let pci_path = slot_to_pci_path(nic.slot, SlotType::Nic)?;
let (device_name, backend_name) = super::pci_path_to_nic_names(pci_path);
let device_spec = NetworkDeviceV0::VirtioNic(VirtioNic {
backend_name: backend_name.clone(),
pci_path,
});

let backend_spec = NetworkBackendV0::Virtio(VirtioNetworkBackend {
vnic_name: nic.name.to_string(),
});

Ok(ParsedNetworkDevice {
device_name,
device_spec,
backend_name,
backend_spec,
})
}
40 changes: 23 additions & 17 deletions bin/propolis-server/src/lib/spec/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ use propolis_api_types::instance_spec::{
board::Board,
devices::{PciPciBridge, QemuPvpanic, SerialPort, SerialPortNumber},
},
v0::{
DeviceSpecV0, InstanceSpecV0, NetworkBackendV0, NetworkDeviceV0,
StorageBackendV0, StorageDeviceV0,
},
v0::{DeviceSpecV0, InstanceSpecV0, NetworkDeviceV0, StorageDeviceV0},
PciPath,
};
use thiserror::Error;

#[cfg(feature = "falcon")]
use propolis_api_types::instance_spec::components::{
backends::DlpiNetworkBackend,
devices::{P9fs, SoftNpuP9, SoftNpuPciPort, SoftNpuPort},
use propolis_api_types::instance_spec::{
components::{
backends::DlpiNetworkBackend,
devices::{P9fs, SoftNpuP9, SoftNpuPciPort, SoftNpuPort},
},
v0::NetworkBackendV0,
};

use super::{ParsedNetworkDevice, ParsedStorageDevice};

/// Errors that can arise while building an instance spec from component parts.
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Error)]
Expand All @@ -46,7 +48,7 @@ pub(crate) enum SpecBuilderError {
SoftNpuPortInUse(String),
}

pub(super) struct SpecBuilder {
pub(crate) struct SpecBuilder {
spec: InstanceSpecV0,
pci_paths: BTreeSet<PciPath>,
}
Expand Down Expand Up @@ -100,10 +102,12 @@ impl SpecBuilder {
/// Adds a storage device with an associated backend.
pub(super) fn add_storage_device(
&mut self,
device_name: String,
device_spec: StorageDeviceV0,
backend_name: String,
backend_spec: StorageBackendV0,
ParsedStorageDevice {
device_name,
device_spec,
backend_name,
backend_spec,
}: ParsedStorageDevice,
) -> Result<&Self, SpecBuilderError> {
if self.spec.devices.storage_devices.contains_key(&device_name) {
return Err(SpecBuilderError::DeviceNameInUse(device_name));
Expand All @@ -128,12 +132,14 @@ impl SpecBuilder {
}

/// Adds a network device with an associated backend.
pub fn add_network_device(
pub(super) fn add_network_device(
&mut self,
device_name: String,
device_spec: NetworkDeviceV0,
backend_name: String,
backend_spec: NetworkBackendV0,
ParsedNetworkDevice {
device_name,
device_spec,
backend_name,
backend_spec,
}: ParsedNetworkDevice,
) -> Result<&Self, SpecBuilderError> {
if self.spec.devices.network_devices.contains_key(&device_name) {
return Err(SpecBuilderError::DeviceNameInUse(device_name));
Expand Down
125 changes: 118 additions & 7 deletions bin/propolis-server/src/lib/spec/config_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ use propolis_api_types::instance_spec::components::devices::{

use crate::config;

use super::{ParsedNetworkDevice, ParsedStorageDevice};

#[derive(Debug, Error)]
pub(crate) enum ConfigTomlError {
#[error("unrecognized device type {0:?}")]
Expand All @@ -48,6 +50,9 @@ pub(crate) enum ConfigTomlError {
#[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),

Expand All @@ -66,6 +71,119 @@ pub(crate) enum ConfigTomlError {
NoP9Target(String),
}

#[cfg(feature = "falcon")]
#[derive(Default)]
pub(super) struct ParsedSoftNpu {
pub(super) pci_ports: Vec<SoftNpuPciPort>,
pub(super) ports: Vec<SoftNpuPort>,
pub(super) p9_devices: Vec<SoftNpuP9>,
pub(super) p9fs: Vec<P9fs>,
}

#[derive(Default)]
pub(super) struct ParsedConfig {
pub(super) disks: Vec<ParsedStorageDevice>,
pub(super) nics: Vec<ParsedNetworkDevice>,
pub(super) pci_bridges: Vec<ParsedPciPciBridge>,

#[cfg(feature = "falcon")]
pub(super) softnpu: ParsedSoftNpu,
}

impl TryFrom<&config::Config> for ParsedConfig {
type Error = ConfigTomlError;

fn try_from(config: &config::Config) -> Result<Self, Self::Error> {
let mut parsed = Self::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 = match &device_spec {
StorageDeviceV0::VirtioDisk(disk) => {
disk.backend_name.clone()
}
StorageDeviceV0::NvmeDisk(disk) => {
disk.backend_name.clone()
}
};

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(ParsedStorageDevice {
device_name: device_name.to_owned(),
device_spec,
backend_name,
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_ports.push(
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_devices.push(
parse_softnpu_p9_from_config(device_name, device)?,
);
}
#[cfg(feature = "falcon")]
"pci-virtio-9p" => {
parsed
.softnpu
.p9fs
.push(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,
Expand Down Expand Up @@ -154,13 +272,6 @@ pub(super) fn parse_storage_device_from_config(
})
}

pub(super) struct ParsedNetworkDevice {
pub(super) device_name: String,
pub(super) device_spec: NetworkDeviceV0,
pub(super) backend_name: String,
pub(super) backend_spec: NetworkBackendV0,
}

pub(super) fn parse_network_device_from_config(
name: &str,
device: &config::Device,
Expand Down
Loading