From eb3a76ac431d4839f73f8f7a77b16404af2bcae1 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Tue, 30 Jul 2024 20:47:43 +0000 Subject: [PATCH 1/7] refactor: flatten v0 specs --- bin/propolis-server/src/lib/initializer.rs | 6 +- bin/propolis-server/src/lib/server.rs | 2 +- .../src/lib/spec/api_spec_v0.rs | 367 ++++++++++-------- bin/propolis-server/src/lib/spec/builder.rs | 18 +- bin/propolis-server/src/lib/spec/mod.rs | 105 ++++- .../src/instance_spec/components/board.rs | 8 +- .../src/instance_spec/components/devices.rs | 4 - .../src/instance_spec/v0.rs | 99 +---- 8 files changed, 335 insertions(+), 274 deletions(-) diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index c0fd10b49..bd8eb4f2a 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -1009,9 +1009,9 @@ impl<'a> MachineInitializer<'a> { info!( self.log, "Generating bootorder with order: {:?}", - self.spec.boot_order.as_ref() + self.spec.boot_settings.as_ref() ); - let Some(boot_names) = self.spec.boot_order.as_ref() else { + let Some(boot_names) = self.spec.boot_settings.as_ref() else { return Ok(None); }; @@ -1033,7 +1033,7 @@ impl<'a> MachineInitializer<'a> { bdf }; - for boot_entry in boot_names.iter() { + for boot_entry in boot_names.order.iter() { // Theoretically we could support booting from network devices by // matching them here and adding their PCI paths, but exactly what // would happen is ill-understood. So, only check disks here. diff --git a/bin/propolis-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index ae44f8248..d4561d824 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -131,7 +131,7 @@ fn instance_spec_from_request( if let Some(boot_settings) = request.boot_settings.as_ref() { for item in boot_settings.order.iter() { - spec_builder.add_boot_option(item)?; + spec_builder.add_boot_option(item.clone())?; } } 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 804b4749e..0ea2a360d 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -8,39 +8,44 @@ use std::collections::HashMap; use propolis_api_types::instance_spec::{ - components::devices::SerialPort as SerialPortDesc, - v0::{InstanceSpecV0, NetworkBackendV0, NetworkDeviceV0, StorageDeviceV0}, + components::{ + backends::{DlpiNetworkBackend, VirtioNetworkBackend}, + board::Board as ApiBoard, + devices::SerialPort as SerialPortDesc, + }, + v0::{ComponentV0, InstanceSpecV0}, }; use thiserror::Error; #[cfg(feature = "falcon")] use propolis_api_types::instance_spec::components::devices::SoftNpuPort as SoftNpuPortSpec; -#[cfg(feature = "falcon")] -use crate::spec::SoftNpuPort; - use super::{ builder::{SpecBuilder, SpecBuilderError}, - Disk, Nic, QemuPvpanic, SerialPortDevice, Spec, + Disk, Nic, QemuPvpanic, SerialPortDevice, Spec, StorageBackend, + StorageDevice, }; +#[cfg(feature = "falcon")] +use super::SoftNpuPort; + #[derive(Debug, Error)] pub(crate) enum ApiSpecError { #[error(transparent)] Builder(#[from] SpecBuilderError), - #[error("backend {backend} not found for device {device}")] - BackendNotFound { backend: String, device: String }, + #[error("storage backend {backend} not found for device {device}")] + StorageBackendNotFound { backend: String, device: String }, - #[error("backend {0} not used by any device")] - BackendNotUsed(String), + #[error("network backend {backend} not found for device {device}")] + NetworkBackendNotFound { backend: String, device: String }, - #[error("network backend for guest NIC {0} is not a viona backend")] - GuestNicInvalidBackend(String), + #[cfg(not(feature = "falcon"))] + #[error("softnpu component {0} compiled out")] + SoftNpuCompiledOut(String), - #[cfg(feature = "falcon")] - #[error("network backend for device {0} is not a DLPI backend")] - NotDlpiBackend(String), + #[error("backend {0} not used by any device")] + BackendNotUsed(String), } impl From for InstanceSpecV0 { @@ -52,90 +57,117 @@ impl From for InstanceSpecV0 { // a unique name to each component they describe. The spec builder // upholds this invariant at spec creation time. #[track_caller] - fn insert_component( - map: &mut HashMap, + fn insert_component( + spec: &mut InstanceSpecV0, key: String, - val: T, + val: ComponentV0, ) { assert!( - !map.contains_key(&key), + !spec.components.contains_key(&key), "component name {} already exists in output spec", &key ); - map.insert(key, val); + spec.components.insert(key, val); } - let mut spec = InstanceSpecV0::default(); - spec.devices.board = val.board; + let board = ApiBoard { + cpus: val.board.cpus, + memory_mb: val.board.memory_mb, + chipset: val.board.chipset, + boot_settings: val.boot_settings.map(Into::into).unwrap_or( + propolis_api_types::BootSettings { order: Vec::new() }, + ), + }; + let mut spec = InstanceSpecV0 { board, ..Default::default() }; + for (disk_name, disk) in val.disks { let backend_name = disk.device_spec.backend_name().to_owned(); - insert_component( - &mut spec.devices.storage_devices, - disk_name, - disk.device_spec.into(), - ); + insert_component(&mut spec, disk_name, disk.device_spec.into()); - insert_component( - &mut spec.backends.storage_backends, - backend_name, - disk.backend_spec.into(), - ); + insert_component(&mut spec, backend_name, disk.backend_spec.into()); } for (nic_name, nic) in val.nics { let backend_name = nic.device_spec.backend_name.clone(); insert_component( - &mut spec.devices.network_devices, + &mut spec, nic_name, - NetworkDeviceV0::VirtioNic(nic.device_spec), + ComponentV0::VirtioNic(nic.device_spec), ); insert_component( - &mut spec.backends.network_backends, + &mut spec, backend_name, - NetworkBackendV0::Virtio(nic.backend_spec), + ComponentV0::VirtioNetworkBackend(nic.backend_spec), ); } for (name, desc) in val.serial { if desc.device == SerialPortDevice::Uart { insert_component( - &mut spec.devices.serial_ports, + &mut spec, name, - SerialPortDesc { num: desc.num }, + ComponentV0::SerialPort(SerialPortDesc { num: desc.num }), ); } } for (bridge_name, bridge) in val.pci_pci_bridges { insert_component( - &mut spec.devices.pci_pci_bridges, + &mut spec, bridge_name, - bridge, + ComponentV0::PciPciBridge(bridge), ); } - spec.devices.qemu_pvpanic = val.pvpanic.map(|pvpanic| pvpanic.spec); + if let Some(pvpanic) = val.pvpanic { + insert_component( + &mut spec, + pvpanic.name.clone(), + ComponentV0::QemuPvpanic(pvpanic.spec), + ); + } #[cfg(feature = "falcon")] { - spec.devices.softnpu_pci_port = val.softnpu.pci_port; - spec.devices.softnpu_p9 = val.softnpu.p9_device; - spec.devices.p9fs = val.softnpu.p9fs; + if let Some(softnpu_pci) = val.softnpu.pci_port { + insert_component( + &mut spec, + format!("softnpu-pci-{}", softnpu_pci.pci_path), + ComponentV0::SoftNpuPciPort(softnpu_pci), + ); + } + + if let Some(p9) = val.softnpu.p9_device { + insert_component( + &mut spec, + format!("softnpu-p9-{}", p9.pci_path), + ComponentV0::SoftNpuP9(p9), + ); + } + + if let Some(p9fs) = val.softnpu.p9fs { + insert_component( + &mut spec, + format!("p9fs-{}", p9fs.pci_path), + ComponentV0::P9fs(p9fs), + ); + } + for (port_name, port) in val.softnpu.ports { insert_component( - &mut spec.devices.softnpu_ports, + &mut spec, port_name.clone(), - SoftNpuPortSpec { + ComponentV0::SoftNpuPort(SoftNpuPortSpec { name: port_name, backend_name: port.backend_name.clone(), - }, + }), ); insert_component( - &mut spec.backends.network_backends, + &mut spec, port.backend_name, - NetworkBackendV0::Dlpi(port.backend_spec), + ComponentV0::DlpiNetworkBackend(port.backend_spec), ); } } @@ -147,125 +179,148 @@ impl From for InstanceSpecV0 { impl TryFrom for Spec { type Error = ApiSpecError; - fn try_from(mut value: InstanceSpecV0) -> Result { - let mut builder = SpecBuilder::with_board(value.devices.board); - - // Examine each storage device and peel its backend off of the input - // spec. - for (device_name, device_spec) in value.devices.storage_devices { - let backend_name = match &device_spec { - StorageDeviceV0::VirtioDisk(disk) => &disk.backend_name, - StorageDeviceV0::NvmeDisk(disk) => &disk.backend_name, - }; - - let (_, backend_spec) = value - .backends - .storage_backends - .remove_entry(backend_name) - .ok_or_else(|| ApiSpecError::BackendNotFound { - backend: backend_name.to_owned(), - device: device_name.clone(), - })?; - - builder.add_storage_device( - device_name, - Disk { - device_spec: device_spec.into(), - backend_spec: backend_spec.into(), - }, - )?; - } - - // Once all the devices have been checked, there should be no unpaired - // backends remaining. - if let Some(backend) = value.backends.storage_backends.keys().next() { - return Err(ApiSpecError::BackendNotUsed(backend.to_owned())); - } - - // Repeat this process for network devices. - for (device_name, device_spec) in value.devices.network_devices { - let NetworkDeviceV0::VirtioNic(device_spec) = device_spec; - let backend_name = &device_spec.backend_name; - let (_, backend_spec) = value - .backends - .network_backends - .remove_entry(backend_name) - .ok_or_else(|| ApiSpecError::BackendNotFound { - backend: backend_name.to_owned(), - device: device_name.clone(), - })?; - - let NetworkBackendV0::Virtio(backend_spec) = backend_spec else { - return Err(ApiSpecError::GuestNicInvalidBackend(device_name)); - }; - - builder.add_network_device( - device_name, - Nic { device_spec, backend_spec }, - )?; - } - - // SoftNPU ports can have network backends, so consume the SoftNPU - // device fields before checking to see if the network backend list is - // empty. - #[cfg(feature = "falcon")] - { - if let Some(softnpu_pci) = value.devices.softnpu_pci_port { - builder.set_softnpu_pci_port(softnpu_pci)?; - } - - if let Some(softnpu_p9) = value.devices.softnpu_p9 { - builder.set_softnpu_p9(softnpu_p9)?; - } - - if let Some(p9fs) = value.devices.p9fs { - builder.set_p9fs(p9fs)?; + fn try_from(value: InstanceSpecV0) -> Result { + let mut builder = SpecBuilder::with_board(&value.board); + let mut devices: Vec<(String, ComponentV0)> = vec![]; + let mut storage_backends: HashMap = + HashMap::new(); + let mut viona_backends: HashMap = + HashMap::new(); + let mut dlpi_backends: HashMap = + HashMap::new(); + + for (name, component) in value.components.into_iter() { + match component { + ComponentV0::CrucibleStorageBackend(_) + | ComponentV0::FileStorageBackend(_) + | ComponentV0::BlobStorageBackend(_) => { + storage_backends.insert( + name, + component.try_into().expect( + "component is known to be a storage backend", + ), + ); + } + ComponentV0::VirtioNetworkBackend(viona) => { + viona_backends.insert(name, viona); + } + ComponentV0::DlpiNetworkBackend(dlpi) => { + dlpi_backends.insert(name, dlpi); + } + device => { + devices.push((name, device)); + } } + } - for (port_name, port) in value.devices.softnpu_ports { - let (backend_name, backend_spec) = value - .backends - .network_backends - .remove_entry(&port.backend_name) - .ok_or_else(|| ApiSpecError::BackendNotFound { - backend: port.backend_name, - device: port_name.clone(), + for (device_name, device_spec) in devices { + match device_spec { + ComponentV0::VirtioDisk(_) | ComponentV0::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_name()) + .ok_or_else(|| { + ApiSpecError::StorageBackendNotFound { + backend: device_spec.backend_name().to_owned(), + device: device_name.clone(), + } + })?; + + builder.add_storage_device( + device_name, + Disk { device_spec, backend_spec }, + )?; + } + ComponentV0::VirtioNic(nic) => { + let (_, backend_spec) = viona_backends + .remove_entry(&nic.backend_name) + .ok_or_else(|| { + ApiSpecError::NetworkBackendNotFound { + backend: nic.backend_name.clone(), + device: device_name.clone(), + } + })?; + + builder.add_network_device( + device_name, + Nic { device_spec: nic, backend_spec }, + )?; + } + ComponentV0::SerialPort(port) => { + builder.add_serial_port(device_name, port.num)?; + } + ComponentV0::PciPciBridge(bridge) => { + builder.add_pci_bridge(device_name, bridge)?; + } + ComponentV0::QemuPvpanic(pvpanic) => { + builder.add_pvpanic_device(QemuPvpanic { + name: device_name, + spec: pvpanic, })?; - - let NetworkBackendV0::Dlpi(backend_spec) = backend_spec else { - return Err(ApiSpecError::NotDlpiBackend(port_name)); - }; - - builder.add_softnpu_port( - port_name, - SoftNpuPort { backend_name, backend_spec }, - )?; + } + #[cfg(not(feature = "falcon"))] + ComponentV0::SoftNpuPciPort(_) + | ComponentV0::SoftNpuPort(_) + | ComponentV0::SoftNpuP9(_) + | ComponentV0::P9fs(_) => { + return Err(ApiSpecError::SoftNpuCompiledOut(device_name)); + } + #[cfg(feature = "falcon")] + ComponentV0::SoftNpuPciPort(port) => { + builder.set_softnpu_pci_port(port)?; + } + #[cfg(feature = "falcon")] + ComponentV0::SoftNpuPort(port) => { + let (_, backend_spec) = dlpi_backends + .remove_entry(&port.backend_name) + .ok_or_else(|| { + ApiSpecError::NetworkBackendNotFound { + backend: port.backend_name.clone(), + device: device_name.clone(), + } + })?; + + let port = SoftNpuPort { + backend_name: port.backend_name, + backend_spec, + }; + + builder.add_softnpu_port(device_name, port)?; + } + #[cfg(feature = "falcon")] + ComponentV0::SoftNpuP9(p9) => { + builder.set_softnpu_p9(p9)?; + } + #[cfg(feature = "falcon")] + ComponentV0::P9fs(p9fs) => { + builder.set_p9fs(p9fs)?; + } + ComponentV0::CrucibleStorageBackend(_) + | ComponentV0::FileStorageBackend(_) + | ComponentV0::BlobStorageBackend(_) + | ComponentV0::VirtioNetworkBackend(_) + | ComponentV0::DlpiNetworkBackend(_) => { + unreachable!("already filtered out backends") + } } } - if let Some(backend) = value.backends.network_backends.keys().next() { - return Err(ApiSpecError::BackendNotUsed(backend.to_owned())); - } - - if let Some(boot_settings) = value.devices.boot_settings.as_ref() { - for item in boot_settings.order.iter() { - builder.add_boot_option(item)?; - } + for item in value.board.boot_settings.order.into_iter() { + builder.add_boot_option(item)?; } - for (name, serial_port) in value.devices.serial_ports { - builder.add_serial_port(name, serial_port.num)?; + if let Some(backend) = storage_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); } - for (name, bridge) in value.devices.pci_pci_bridges { - builder.add_pci_bridge(name, bridge)?; + if let Some(backend) = viona_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); } - if let Some(pvpanic) = value.devices.qemu_pvpanic { - builder.add_pvpanic_device(QemuPvpanic { - name: "pvpanic".to_string(), - spec: pvpanic, - })?; + if let Some(backend) = dlpi_backends.into_keys().next() { + return Err(ApiSpecError::BackendNotUsed(backend)); } Ok(builder.finish()) diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index 3357d8b43..acbfe90e5 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -9,7 +9,7 @@ use std::collections::{BTreeSet, HashSet}; use propolis_api_types::{ instance_spec::{ components::{ - board::{Board, Chipset, I440Fx}, + board::{Board as ApiBoard, Chipset, I440Fx}, devices::{PciPciBridge, SerialPortNumber}, }, PciPath, @@ -28,7 +28,7 @@ use crate::{config, spec::SerialPortDevice}; use super::{ api_request::{self, DeviceRequestError}, config_toml::{ConfigTomlError, ParsedConfig}, - Disk, Nic, QemuPvpanic, SerialPort, + Board, BootSettings, Disk, Nic, QemuPvpanic, SerialPort, }; #[cfg(feature = "falcon")] @@ -84,9 +84,9 @@ impl SpecBuilder { } } - pub(super) fn with_board(board: Board) -> Self { + pub(super) fn with_board(api_board: &ApiBoard) -> Self { Self { - spec: super::Spec { board, ..Default::default() }, + spec: super::Spec { board: api_board.into(), ..Default::default() }, ..Default::default() } } @@ -116,16 +116,18 @@ impl SpecBuilder { /// Add a boot option to the boot option list of the spec under construction. pub fn add_boot_option( &mut self, - item: &BootOrderEntry, + item: BootOrderEntry, ) -> Result<(), SpecBuilderError> { if !self.spec.disks.contains_key(item.name.as_str()) { return Err(SpecBuilderError::BootOptionMissing(item.name.clone())); } - let boot_order = self.spec.boot_order.get_or_insert(Vec::new()); + let boot_settings = self + .spec + .boot_settings + .get_or_insert(BootSettings { order: Vec::new() }); - boot_order - .push(crate::spec::BootOrderEntry { name: item.name.clone() }); + boot_settings.order.push(item.into()); Ok(()) } diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 230943537..0245afb16 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -22,15 +22,16 @@ use propolis_api_types::instance_spec::{ BlobStorageBackend, CrucibleStorageBackend, FileStorageBackend, VirtioNetworkBackend, }, - board::Board, + board::{Chipset, I440Fx}, devices::{ NvmeDisk, PciPciBridge, QemuPvpanic as QemuPvpanicDesc, SerialPortNumber, VirtioDisk, VirtioNic, }, }, - v0::{StorageBackendV0, StorageDeviceV0}, + v0::ComponentV0, PciPath, }; +use thiserror::Error; #[cfg(feature = "falcon")] use propolis_api_types::instance_spec::components::{ @@ -43,6 +44,10 @@ pub(crate) mod api_spec_v0; pub(crate) mod builder; mod config_toml; +#[derive(Debug, Error)] +#[error("input component type can't convert to output type")] +pub struct ComponentTypeMismatch; + /// An instance specification that describes a VM's configuration and /// components. /// @@ -57,7 +62,7 @@ pub(crate) struct Spec { pub board: Board, pub disks: HashMap, pub nics: HashMap, - pub boot_order: Option>, + pub boot_settings: Option, pub serial: HashMap, @@ -68,11 +73,71 @@ pub(crate) struct Spec { pub softnpu: SoftNpu, } +#[derive(Clone, Debug)] +pub(crate) struct Board { + pub cpus: u8, + pub memory_mb: u64, + pub chipset: Chipset, +} + +impl Default for Board { + fn default() -> Self { + Self { + cpus: 0, + memory_mb: 0, + chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), + } + } +} + +impl From<&propolis_api_types::instance_spec::components::board::Board> + for Board +{ + fn from( + value: &propolis_api_types::instance_spec::components::board::Board, + ) -> Self { + Board { + cpus: value.cpus, + memory_mb: value.memory_mb, + chipset: value.chipset, + } + } +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct BootSettings { + pub order: Vec, +} + +impl From for BootSettings { + fn from(value: propolis_api_types::BootSettings) -> Self { + Self { order: value.order.into_iter().map(Into::into).collect() } + } +} + +impl From for propolis_api_types::BootSettings { + fn from(value: BootSettings) -> Self { + Self { order: value.order.into_iter().map(Into::into).collect() } + } +} + #[derive(Clone, Debug, Default)] pub(crate) struct BootOrderEntry { pub name: String, } +impl From for BootOrderEntry { + fn from(value: propolis_api_types::BootOrderEntry) -> Self { + Self { name: value.name } + } +} + +impl From for propolis_api_types::BootOrderEntry { + fn from(value: BootOrderEntry) -> Self { + Self { name: value.name } + } +} + /// Describes the device half of a [`Disk`]. #[derive(Clone, Debug)] pub enum StorageDevice { @@ -103,7 +168,7 @@ impl StorageDevice { } } -impl From for StorageDeviceV0 { +impl From for ComponentV0 { fn from(value: StorageDevice) -> Self { match value { StorageDevice::Virtio(d) => Self::VirtioDisk(d), @@ -112,11 +177,14 @@ impl From for StorageDeviceV0 { } } -impl From for StorageDevice { - fn from(value: StorageDeviceV0) -> Self { +impl TryFrom for StorageDevice { + type Error = ComponentTypeMismatch; + + fn try_from(value: ComponentV0) -> Result { match value { - StorageDeviceV0::VirtioDisk(d) => Self::Virtio(d), - StorageDeviceV0::NvmeDisk(d) => Self::Nvme(d), + ComponentV0::VirtioDisk(d) => Ok(Self::Virtio(d)), + ComponentV0::NvmeDisk(d) => Ok(Self::Nvme(d)), + _ => Err(ComponentTypeMismatch), } } } @@ -147,22 +215,25 @@ impl StorageBackend { } } -impl From for StorageBackendV0 { +impl From for ComponentV0 { fn from(value: StorageBackend) -> Self { match value { - StorageBackend::Crucible(be) => Self::Crucible(be), - StorageBackend::File(be) => Self::File(be), - StorageBackend::Blob(be) => Self::Blob(be), + StorageBackend::Crucible(be) => Self::CrucibleStorageBackend(be), + StorageBackend::File(be) => Self::FileStorageBackend(be), + StorageBackend::Blob(be) => Self::BlobStorageBackend(be), } } } -impl From for StorageBackend { - fn from(value: StorageBackendV0) -> Self { +impl TryFrom for StorageBackend { + type Error = ComponentTypeMismatch; + + fn try_from(value: ComponentV0) -> Result { match value { - StorageBackendV0::Crucible(be) => Self::Crucible(be), - StorageBackendV0::File(be) => Self::File(be), - StorageBackendV0::Blob(be) => Self::Blob(be), + ComponentV0::CrucibleStorageBackend(be) => Ok(Self::Crucible(be)), + ComponentV0::FileStorageBackend(be) => Ok(Self::File(be)), + ComponentV0::BlobStorageBackend(be) => Ok(Self::Blob(be)), + _ => Err(ComponentTypeMismatch), } } } diff --git a/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index e9c3f59de..f8713949b 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -8,6 +8,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::BootSettings; + /// An Intel 440FX-compatible chipset. #[derive( Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, @@ -35,7 +37,7 @@ pub enum Chipset { } /// A VM's mainboard. -#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Board { /// The number of virtual logical processors attached to this VM. @@ -46,6 +48,9 @@ pub struct Board { /// The chipset to expose to guest software. pub chipset: Chipset, + + /// The boot device order to supply to the guest. + pub boot_settings: BootSettings, // TODO: Guest platform and CPU feature identification. // TODO: NUMA topology. } @@ -56,6 +61,7 @@ impl Default for Board { cpus: 0, memory_mb: 0, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), + boot_settings: BootSettings { order: vec![] }, } } } 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 1aa9b7225..40d1c007b 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -108,7 +108,6 @@ pub struct QemuPvpanic { // Structs for Falcon devices. These devices don't support live migration. // -#[cfg(feature = "falcon")] #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuPciPort { @@ -116,7 +115,6 @@ pub struct SoftNpuPciPort { pub pci_path: PciPath, } -#[cfg(feature = "falcon")] #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuPort { @@ -127,7 +125,6 @@ pub struct SoftNpuPort { pub backend_name: String, } -#[cfg(feature = "falcon")] #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuP9 { @@ -135,7 +132,6 @@ pub struct SoftNpuP9 { pub pci_path: PciPath, } -#[cfg(feature = "falcon")] #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct P9fs { diff --git a/crates/propolis-api-types/src/instance_spec/v0.rs b/crates/propolis-api-types/src/instance_spec/v0.rs index 31451dcdc..93b8e545d 100644 --- a/crates/propolis-api-types/src/instance_spec/v0.rs +++ b/crates/propolis-api-types/src/instance_spec/v0.rs @@ -2,23 +2,6 @@ // 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 0 of a fully-composed instance specification. -//! -//! V0 specs are split into 'device' and 'backend' halves that can be serialized -//! and deserialized independently. -//! -//! # Versioning and compatibility -//! -//! Changes to structs and enums in this module must be backward-compatible -//! (i.e. new code must be able to deserialize specs created by old versions of -//! the module). Breaking changes to the spec structure must be turned into a -//! new specification version. Note that adding a new component to one of the -//! existing enums in this module is not a back-compat breaking change. -//! -//! Data types in this module should have a `V0` suffix in their names to avoid -//! aliasing with type names in other versions (which can cause Dropshot to -//! create OpenAPI specs that are missing certain types; see dropshot#383). - use std::collections::HashMap; use crate::instance_spec::{components, SpecKey}; @@ -27,79 +10,27 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields, tag = "type", content = "component")] -pub enum StorageDeviceV0 { +pub enum ComponentV0 { VirtioDisk(components::devices::VirtioDisk), NvmeDisk(components::devices::NvmeDisk), -} - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields, tag = "type", content = "component")] -pub enum NetworkDeviceV0 { VirtioNic(components::devices::VirtioNic), -} - -#[derive(Default, Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct DeviceSpecV0 { - pub board: components::board::Board, - pub storage_devices: HashMap, - pub network_devices: HashMap, - pub serial_ports: HashMap, - pub pci_pci_bridges: HashMap, - - // This field has a default value (`None`) to allow for - // backwards-compatibility when upgrading from a Propolis - // version that does not support this device. If the pvpanic device was not - // present in the spec being deserialized, a `None` will be produced, - // rather than rejecting the spec. - #[serde(default)] - // Skip serializing this field if it is `None`. This is so that Propolis - // versions with support for this device are backwards-compatible with - // older versions that don't, as long as the spec doesn't define a pvpanic - // device --- if there is no panic device, skipping the field from the spec - // means that the older version will still accept the spec. - #[serde(skip_serializing_if = "Option::is_none")] - pub qemu_pvpanic: Option, - - // Same backwards compatibility reasoning as above. - #[serde(default)] - #[serde(skip_serializing_if = "Option::is_none")] - pub boot_settings: Option, - - #[cfg(feature = "falcon")] - pub softnpu_pci_port: Option, - #[cfg(feature = "falcon")] - pub softnpu_ports: HashMap, - #[cfg(feature = "falcon")] - pub softnpu_p9: Option, - #[cfg(feature = "falcon")] - pub p9fs: Option, -} - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields, tag = "type", content = "component")] -pub enum StorageBackendV0 { - Crucible(components::backends::CrucibleStorageBackend), - File(components::backends::FileStorageBackend), - Blob(components::backends::BlobStorageBackend), -} - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields, tag = "type", content = "component")] -pub enum NetworkBackendV0 { - Virtio(components::backends::VirtioNetworkBackend), - Dlpi(components::backends::DlpiNetworkBackend), -} - -#[derive(Default, Clone, Deserialize, Serialize, Debug, JsonSchema)] -pub struct BackendSpecV0 { - pub storage_backends: HashMap, - pub network_backends: HashMap, + SerialPort(components::devices::SerialPort), + PciPciBridge(components::devices::PciPciBridge), + QemuPvpanic(components::devices::QemuPvpanic), + SoftNpuPciPort(components::devices::SoftNpuPciPort), + SoftNpuPort(components::devices::SoftNpuPort), + SoftNpuP9(components::devices::SoftNpuP9), + P9fs(components::devices::P9fs), + CrucibleStorageBackend(components::backends::CrucibleStorageBackend), + FileStorageBackend(components::backends::FileStorageBackend), + BlobStorageBackend(components::backends::BlobStorageBackend), + VirtioNetworkBackend(components::backends::VirtioNetworkBackend), + DlpiNetworkBackend(components::backends::DlpiNetworkBackend), } #[derive(Default, Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct InstanceSpecV0 { - pub devices: DeviceSpecV0, - pub backends: BackendSpecV0, + pub board: components::board::Board, + pub components: HashMap, } From 05edf9777a5e1ab8d3b4a3362d2875a044d8073d Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Tue, 30 Jul 2024 23:11:36 +0000 Subject: [PATCH 2/7] regen openapi + fix up PHD --- lib/propolis-client/src/instance_spec.rs | 265 --- lib/propolis-client/src/lib.rs | 33 +- lib/propolis-client/src/support.rs | 20 +- openapi/propolis-server-falcon.json | 2028 --------------------- openapi/propolis-server.json | 688 ++++--- phd-tests/framework/src/disk/crucible.rs | 6 +- 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 | 71 +- phd-tests/framework/src/test_vm/mod.rs | 6 +- phd-tests/framework/src/test_vm/spec.rs | 39 +- phd-tests/tests/src/smoke.rs | 4 +- 13 files changed, 489 insertions(+), 2687 deletions(-) delete mode 100644 lib/propolis-client/src/instance_spec.rs delete mode 100644 openapi/propolis-server-falcon.json diff --git a/lib/propolis-client/src/instance_spec.rs b/lib/propolis-client/src/instance_spec.rs deleted file mode 100644 index 8c75bee98..000000000 --- a/lib/propolis-client/src/instance_spec.rs +++ /dev/null @@ -1,265 +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/. - -//! A builder for instance specs. - -use std::collections::BTreeSet; - -use thiserror::Error; - -use crate::types::{ - Board, Chipset, DeviceSpecV0, I440Fx, InstanceSpecV0, NetworkBackendV0, - NetworkDeviceV0, PciPath, PciPciBridge, SerialPort, SerialPortNumber, - StorageBackendV0, StorageDeviceV0, -}; - -#[cfg(feature = "falcon")] -use crate::types::{ - DlpiNetworkBackend, P9fs, SoftNpuP9, SoftNpuPciPort, SoftNpuPort, -}; - -/// Errors that can arise while building an instance spec from component parts. -#[derive(Debug, Error)] -pub enum SpecBuilderError { - #[error("A device with name {0} already exists")] - DeviceNameInUse(String), - - #[error("A backend with name {0} already exists")] - BackendNameInUse(String), - - #[error("A PCI device is already attached at {0:?}")] - PciPathInUse(PciPath), - - #[error("Serial port {0:?} is already specified")] - SerialPortInUse(SerialPortNumber), - - #[error("SoftNpu port {0:?} is already specified")] - SoftNpuPortInUse(String), -} - -/// A builder that constructs instance specs incrementally and catches basic -/// errors, such as specifying duplicate component names or specifying multiple -/// devices with the same PCI path. -pub struct SpecBuilderV0 { - spec: InstanceSpecV0, - pci_paths: BTreeSet, -} - -impl SpecBuilderV0 { - /// Creates a new instance spec with the supplied board configuration. - pub fn new(cpus: u8, memory_mb: u64, enable_pcie: bool) -> Self { - let board = Board { - cpus, - memory_mb, - chipset: Chipset::I440Fx(I440Fx { enable_pcie }), - }; - - Self { - spec: InstanceSpecV0 { - devices: DeviceSpecV0 { board, ..Default::default() }, - ..Default::default() - }, - pci_paths: Default::default(), - } - } - - /// Adds a PCI path to this builder's record of PCI locations with an - /// attached device. If the path is already in use, returns an error. - fn register_pci_device( - &mut self, - pci_path: PciPath, - ) -> Result<(), SpecBuilderError> { - if self.pci_paths.contains(&pci_path) { - Err(SpecBuilderError::PciPathInUse(pci_path)) - } else { - self.pci_paths.insert(pci_path); - Ok(()) - } - } - - /// Adds a storage device with an associated backend. - pub fn add_storage_device( - &mut self, - device_name: String, - device_spec: StorageDeviceV0, - backend_name: String, - backend_spec: StorageBackendV0, - ) -> Result<&Self, SpecBuilderError> { - if self.spec.devices.storage_devices.contains_key(&device_name) { - return Err(SpecBuilderError::DeviceNameInUse(device_name)); - } - - if self.spec.backends.storage_backends.contains_key(&backend_name) { - return Err(SpecBuilderError::BackendNameInUse(backend_name)); - } - self.register_pci_device(device_spec.pci_path())?; - let _old = - self.spec.devices.storage_devices.insert(device_name, device_spec); - - assert!(_old.is_none()); - let _old = self - .spec - .backends - .storage_backends - .insert(backend_name, backend_spec); - - assert!(_old.is_none()); - Ok(self) - } - - /// Adds a network device with an associated backend. - pub fn add_network_device( - &mut self, - device_name: String, - device_spec: NetworkDeviceV0, - backend_name: String, - backend_spec: NetworkBackendV0, - ) -> Result<&Self, SpecBuilderError> { - if self.spec.devices.network_devices.contains_key(&device_name) { - return Err(SpecBuilderError::DeviceNameInUse(device_name)); - } - - if self.spec.backends.network_backends.contains_key(&backend_name) { - return Err(SpecBuilderError::BackendNameInUse(backend_name)); - } - - self.register_pci_device(device_spec.pci_path())?; - let _old = - self.spec.devices.network_devices.insert(device_name, device_spec); - - assert!(_old.is_none()); - let _old = self - .spec - .backends - .network_backends - .insert(backend_name, backend_spec); - - assert!(_old.is_none()); - Ok(self) - } - - /// Adds a PCI-PCI bridge. - pub fn add_pci_bridge( - &mut self, - bridge_name: String, - bridge_spec: PciPciBridge, - ) -> Result<&Self, SpecBuilderError> { - if self.spec.devices.pci_pci_bridges.contains_key(&bridge_name) { - return Err(SpecBuilderError::DeviceNameInUse(bridge_name)); - } - - self.register_pci_device(bridge_spec.pci_path)?; - let _old = - self.spec.devices.pci_pci_bridges.insert(bridge_name, bridge_spec); - - assert!(_old.is_none()); - Ok(self) - } - - /// Adds a serial port. - pub fn add_serial_port( - &mut self, - port: SerialPortNumber, - ) -> Result<&Self, SpecBuilderError> { - if self - .spec - .devices - .serial_ports - .insert( - match port { - SerialPortNumber::Com1 => "com1", - SerialPortNumber::Com2 => "com2", - SerialPortNumber::Com3 => "com3", - SerialPortNumber::Com4 => "com4", - } - .to_string(), - SerialPort { num: port }, - ) - .is_some() - { - Err(SpecBuilderError::SerialPortInUse(port)) - } else { - Ok(self) - } - } - - /// Sets a boot order. Names here refer to devices included in this spec. - /// - /// Permissible to not set this if the implicit boot order is desired, but - /// the implicit boot order may be unstable across device addition and - /// removal. - /// - /// If any devices named in this order are not actually present in the - /// constructed spec, Propolis will return an error when the spec is - /// provided. - /// - /// XXX: this should certainly return `&mut Self` - all the builders here - /// should. check if any of these are chained..? - pub fn set_boot_order( - &mut self, - boot_order: Vec, - ) -> Result<&Self, SpecBuilderError> { - let boot_order = boot_order - .into_iter() - .map(|name| crate::types::BootOrderEntry { name }) - .collect(); - - let settings = crate::types::BootSettings { order: boot_order }; - - self.spec.devices.boot_settings = Some(settings); - - Ok(self) - } - - /// Yields the completed spec, consuming the builder. - pub fn finish(self) -> InstanceSpecV0 { - self.spec - } -} - -#[cfg(feature = "falcon")] -impl SpecBuilderV0 { - pub fn set_softnpu_pci_port( - &mut self, - pci_port: SoftNpuPciPort, - ) -> Result<&Self, SpecBuilderError> { - self.register_pci_device(pci_port.pci_path)?; - self.spec.devices.softnpu_pci_port = Some(pci_port); - Ok(self) - } - - pub fn add_softnpu_port( - &mut self, - key: String, - port: SoftNpuPort, - ) -> Result<&Self, SpecBuilderError> { - let _old = self.spec.backends.network_backends.insert( - port.backend_name.clone(), - NetworkBackendV0::Dlpi(DlpiNetworkBackend { - vnic_name: port.backend_name.clone(), - }), - ); - assert!(_old.is_none()); - if self.spec.devices.softnpu_ports.insert(key, port.clone()).is_some() { - Err(SpecBuilderError::SoftNpuPortInUse(port.name)) - } else { - Ok(self) - } - } - - pub fn set_softnpu_p9( - &mut self, - p9: SoftNpuP9, - ) -> Result<&Self, SpecBuilderError> { - self.register_pci_device(p9.pci_path)?; - self.spec.devices.softnpu_p9 = Some(p9); - Ok(self) - } - - pub fn set_p9fs(&mut self, p9fs: P9fs) -> Result<&Self, SpecBuilderError> { - self.register_pci_device(p9fs.pci_path)?; - self.spec.devices.p9fs = Some(p9fs); - Ok(self) - } -} diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index e0c769347..89d7f3687 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -4,7 +4,6 @@ //! A client for the Propolis hypervisor frontend's server API. -#[cfg(not(feature = "falcon"))] progenitor::generate_api!( spec = "../../openapi/propolis-server.json", interface = Builder, @@ -12,14 +11,12 @@ progenitor::generate_api!( patch = { // Add `Default` to types related to instance specs InstanceSpecV0 = { derives = [Default] }, - BackendSpecV0 = { derives = [Default] }, - DeviceSpecV0 = { derives = [Default] }, Board = { derives = [Default] }, // Some Crucible-related bits are re-exported through simulated // sled-agent and thus require JsonSchema BootOrderEntry = { derives = [schemars::JsonSchema] }, - BootSettings = { derives = [schemars::JsonSchema] }, + BootSettings = { derives = [Default, schemars::JsonSchema] }, DiskRequest = { derives = [schemars::JsonSchema] }, VolumeConstructionRequest = { derives = [schemars::JsonSchema] }, CrucibleOpts = { derives = [schemars::JsonSchema] }, @@ -33,32 +30,4 @@ progenitor::generate_api!( } ); -#[cfg(feature = "falcon")] -progenitor::generate_api!( - spec = "../../openapi/propolis-server-falcon.json", - interface = Builder, - tags = Separate, - patch = { - // Add `Default` to types related to instance specs - InstanceSpecV0 = { derives = [Default] }, - BackendSpecV0 = { derives = [Default] }, - DeviceSpecV0 = { derives = [Default] }, - Board = { derives = [Default] }, - - // Some Crucible-related bits are re-exported through simulated - // sled-agent and thus require JsonSchema - DiskRequest = { derives = [schemars::JsonSchema] }, - VolumeConstructionRequest = { derives = [schemars::JsonSchema] }, - CrucibleOpts = { derives = [schemars::JsonSchema] }, - Slot = { derives = [schemars::JsonSchema] }, - - PciPath = { derives = [ - Copy, Ord, Eq, PartialEq, PartialOrd - ] }, - - InstanceMetadata = { derives = [ PartialEq ] }, - } -); - -pub mod instance_spec; pub mod support; diff --git a/lib/propolis-client/src/support.rs b/lib/propolis-client/src/support.rs index 76abb2725..bcf5f10bc 100644 --- a/lib/propolis-client/src/support.rs +++ b/lib/propolis-client/src/support.rs @@ -18,9 +18,7 @@ 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, NetworkDeviceV0, PciPath, StorageDeviceV0, -}; +use crate::types::{Chipset, I440Fx, PciPath}; use crate::Client as PropolisClient; const PCI_DEV_PER_BUS: u8 = 32; @@ -48,22 +46,6 @@ impl Default for Chipset { } } -impl NetworkDeviceV0 { - pub fn pci_path(&self) -> PciPath { - match self { - NetworkDeviceV0::VirtioNic(dev) => dev.pci_path, - } - } -} -impl StorageDeviceV0 { - pub fn pci_path(&self) -> PciPath { - match self { - StorageDeviceV0::VirtioDisk(dev) => dev.pci_path, - StorageDeviceV0::NvmeDisk(dev) => dev.pci_path, - } - } -} - /// Clone of `InstanceSerialConsoleControlMessage` type defined in /// `propolis_api_types`, with which this must be kept in sync. /// diff --git a/openapi/propolis-server-falcon.json b/openapi/propolis-server-falcon.json deleted file mode 100644 index baf6dc95d..000000000 --- a/openapi/propolis-server-falcon.json +++ /dev/null @@ -1,2028 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "Oxide Propolis Server API", - "description": "API for interacting with the Propolis hypervisor frontend.", - "contact": { - "url": "https://oxide.computer", - "email": "api@oxide.computer" - }, - "version": "0.0.1" - }, - "paths": { - "/instance": { - "get": { - "operationId": "instance_get", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceGetResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "put": { - "operationId": "instance_ensure", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceEnsureRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceEnsureResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/disk/{id}/snapshot/{snapshot_id}": { - "post": { - "summary": "Issues a snapshot request to a crucible backend.", - "operationId": "instance_issue_crucible_snapshot_request", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "in": "path", - "name": "snapshot_id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "title": "Null", - "type": "string", - "enum": [ - null - ] - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/disk/{id}/status": { - "get": { - "summary": "Gets the status of a Crucible volume backing a disk", - "operationId": "disk_volume_status", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VolumeStatus" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/disk/{id}/vcr": { - "put": { - "summary": "Issues a volume_construction_request replace to a crucible backend.", - "operationId": "instance_issue_crucible_vcr_request", - "parameters": [ - { - "in": "path", - "name": "id", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceVCRReplace" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ReplaceResult" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/migration-status": { - "get": { - "operationId": "instance_migrate_status", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceMigrateStatusResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/nmi": { - "post": { - "summary": "Issues an NMI to the instance.", - "operationId": "instance_issue_nmi", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "title": "Null", - "type": "string", - "enum": [ - null - ] - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/serial": { - "get": { - "operationId": "instance_serial", - "parameters": [ - { - "in": "query", - "name": "from_start", - "description": "Character index in the serial buffer from which to read, counting the bytes output since instance start. If this is provided, `most_recent` must *not* be provided.", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - { - "in": "query", - "name": "most_recent", - "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance. (See note on `from_start` about mutual exclusivity)", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - } - } - ], - "responses": { - "default": { - "description": "", - "content": { - "*/*": { - "schema": {} - } - } - } - }, - "x-dropshot-websocket": {} - } - }, - "/instance/serial/history": { - "get": { - "operationId": "instance_serial_history_get", - "parameters": [ - { - "in": "query", - "name": "from_start", - "description": "Character index in the serial buffer from which to read, counting the bytes output since instance start. If this is not provided, `most_recent` must be provided, and if this *is* provided, `most_recent` must *not* be provided.", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - { - "in": "query", - "name": "max_bytes", - "description": "Maximum number of bytes of buffered serial console contents to return. If the requested range runs to the end of the available buffer, the data returned will be shorter than `max_bytes`.", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - { - "in": "query", - "name": "most_recent", - "description": "Character index in the serial buffer from which to read, counting *backward* from the most recently buffered data retrieved from the instance. (See note on `from_start` about mutual exclusivity)", - "schema": { - "nullable": true, - "type": "integer", - "format": "uint64", - "minimum": 0 - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceSerialConsoleHistoryResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/spec": { - "get": { - "operationId": "instance_spec_get", - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceSpecGetResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - }, - "put": { - "operationId": "instance_spec_ensure", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceSpecEnsureRequest" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "successful creation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceEnsureResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/state": { - "put": { - "operationId": "instance_state_put", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceStateRequested" - } - } - }, - "required": true - }, - "responses": { - "204": { - "description": "resource updated" - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - }, - "/instance/state-monitor": { - "get": { - "operationId": "instance_state_monitor", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceStateMonitorRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "successful operation", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InstanceStateMonitorResponse" - } - } - } - }, - "4XX": { - "$ref": "#/components/responses/Error" - }, - "5XX": { - "$ref": "#/components/responses/Error" - } - } - } - } - }, - "components": { - "schemas": { - "BackendSpecV0": { - "type": "object", - "properties": { - "network_backends": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/NetworkBackendV0" - } - }, - "storage_backends": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/StorageBackendV0" - } - } - }, - "required": [ - "network_backends", - "storage_backends" - ] - }, - "BlobStorageBackend": { - "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", - "type": "object", - "properties": { - "base64": { - "description": "The disk's initial contents, encoded as a base64 string.", - "type": "string" - }, - "readonly": { - "description": "Indicates whether the storage is read-only.", - "type": "boolean" - } - }, - "required": [ - "base64", - "readonly" - ], - "additionalProperties": false - }, - "Board": { - "description": "A VM's mainboard.", - "type": "object", - "properties": { - "chipset": { - "description": "The chipset to expose to guest software.", - "allOf": [ - { - "$ref": "#/components/schemas/Chipset" - } - ] - }, - "cpus": { - "description": "The number of virtual logical processors attached to this VM.", - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "memory_mb": { - "description": "The amount of guest RAM attached to this VM.", - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "chipset", - "cpus", - "memory_mb" - ], - "additionalProperties": false - }, - "BootOrderEntry": { - "description": "An entry in a list of boot options.", - "type": "object", - "properties": { - "name": { - "description": "The name of the device to attempt booting from.", - "type": "string" - } - }, - "required": [ - "name" - ] - }, - "BootSettings": { - "type": "object", - "properties": { - "order": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BootOrderEntry" - } - } - }, - "required": [ - "order" - ] - }, - "Chipset": { - "description": "A kind of virtual chipset.", - "oneOf": [ - { - "description": "An Intel 440FX-compatible chipset.", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "i440_fx" - ] - }, - "value": { - "$ref": "#/components/schemas/I440Fx" - } - }, - "required": [ - "type", - "value" - ], - "additionalProperties": false - } - ] - }, - "CrucibleOpts": { - "type": "object", - "properties": { - "cert_pem": { - "nullable": true, - "type": "string" - }, - "control": { - "nullable": true, - "type": "string" - }, - "flush_timeout": { - "nullable": true, - "type": "number", - "format": "float" - }, - "id": { - "type": "string", - "format": "uuid" - }, - "key": { - "nullable": true, - "type": "string" - }, - "key_pem": { - "nullable": true, - "type": "string" - }, - "lossy": { - "type": "boolean" - }, - "read_only": { - "type": "boolean" - }, - "root_cert_pem": { - "nullable": true, - "type": "string" - }, - "target": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "lossy", - "read_only", - "target" - ] - }, - "CrucibleStorageBackend": { - "description": "A Crucible storage backend.", - "type": "object", - "properties": { - "readonly": { - "description": "Indicates whether the storage is read-only.", - "type": "boolean" - }, - "request_json": { - "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", - "type": "string" - } - }, - "required": [ - "readonly", - "request_json" - ], - "additionalProperties": false - }, - "DeviceSpecV0": { - "type": "object", - "properties": { - "board": { - "$ref": "#/components/schemas/Board" - }, - "boot_settings": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/BootSettings" - } - ] - }, - "network_devices": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/NetworkDeviceV0" - } - }, - "p9fs": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/P9fs" - } - ] - }, - "pci_pci_bridges": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PciPciBridge" - } - }, - "qemu_pvpanic": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/QemuPvpanic" - } - ] - }, - "serial_ports": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SerialPort" - } - }, - "softnpu_p9": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/SoftNpuP9" - } - ] - }, - "softnpu_pci_port": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/SoftNpuPciPort" - } - ] - }, - "softnpu_ports": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SoftNpuPort" - } - }, - "storage_devices": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/StorageDeviceV0" - } - } - }, - "required": [ - "board", - "network_devices", - "pci_pci_bridges", - "serial_ports", - "softnpu_ports", - "storage_devices" - ], - "additionalProperties": false - }, - "DiskAttachment": { - "type": "object", - "properties": { - "disk_id": { - "type": "string", - "format": "uuid" - }, - "generation_id": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "state": { - "$ref": "#/components/schemas/DiskAttachmentState" - } - }, - "required": [ - "disk_id", - "generation_id", - "state" - ] - }, - "DiskAttachmentState": { - "oneOf": [ - { - "type": "string", - "enum": [ - "Detached", - "Destroyed", - "Faulted" - ] - }, - { - "type": "object", - "properties": { - "Attached": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "Attached" - ], - "additionalProperties": false - } - ] - }, - "DiskRequest": { - "type": "object", - "properties": { - "device": { - "type": "string" - }, - "name": { - "type": "string" - }, - "read_only": { - "type": "boolean" - }, - "slot": { - "$ref": "#/components/schemas/Slot" - }, - "volume_construction_request": { - "$ref": "#/components/schemas/VolumeConstructionRequest" - } - }, - "required": [ - "device", - "name", - "read_only", - "slot", - "volume_construction_request" - ] - }, - "DlpiNetworkBackend": { - "description": "A network backend associated with a DLPI VNIC on the host.", - "type": "object", - "properties": { - "vnic_name": { - "description": "The name of the VNIC to use as a backend.", - "type": "string" - } - }, - "required": [ - "vnic_name" - ], - "additionalProperties": false - }, - "Error": { - "description": "Error information from a response.", - "type": "object", - "properties": { - "error_code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "request_id": { - "type": "string" - } - }, - "required": [ - "message", - "request_id" - ] - }, - "FileStorageBackend": { - "description": "A storage backend backed by a file in the host system's file system.", - "type": "object", - "properties": { - "path": { - "description": "A path to a file that backs a disk.", - "type": "string" - }, - "readonly": { - "description": "Indicates whether the storage is read-only.", - "type": "boolean" - } - }, - "required": [ - "path", - "readonly" - ], - "additionalProperties": false - }, - "I440Fx": { - "description": "An Intel 440FX-compatible chipset.", - "type": "object", - "properties": { - "enable_pcie": { - "description": "Specifies whether the chipset should allow PCI configuration space to be accessed through the PCIe extended configuration mechanism.", - "type": "boolean" - } - }, - "required": [ - "enable_pcie" - ], - "additionalProperties": false - }, - "Instance": { - "type": "object", - "properties": { - "disks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DiskAttachment" - } - }, - "nics": { - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkInterface" - } - }, - "properties": { - "$ref": "#/components/schemas/InstanceProperties" - }, - "state": { - "$ref": "#/components/schemas/InstanceState" - } - }, - "required": [ - "disks", - "nics", - "properties", - "state" - ] - }, - "InstanceEnsureRequest": { - "type": "object", - "properties": { - "boot_settings": { - "nullable": true, - "default": null, - "allOf": [ - { - "$ref": "#/components/schemas/BootSettings" - } - ] - }, - "cloud_init_bytes": { - "nullable": true, - "type": "string" - }, - "disks": { - "default": [], - "type": "array", - "items": { - "$ref": "#/components/schemas/DiskRequest" - } - }, - "migrate": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMigrateInitiateRequest" - } - ] - }, - "nics": { - "default": [], - "type": "array", - "items": { - "$ref": "#/components/schemas/NetworkInterfaceRequest" - } - }, - "properties": { - "$ref": "#/components/schemas/InstanceProperties" - } - }, - "required": [ - "properties" - ] - }, - "InstanceEnsureResponse": { - "type": "object", - "properties": { - "migrate": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMigrateInitiateResponse" - } - ] - } - } - }, - "InstanceGetResponse": { - "type": "object", - "properties": { - "instance": { - "$ref": "#/components/schemas/Instance" - } - }, - "required": [ - "instance" - ] - }, - "InstanceMetadata": { - "type": "object", - "properties": { - "project_id": { - "type": "string", - "format": "uuid" - }, - "silo_id": { - "type": "string", - "format": "uuid" - }, - "sled_id": { - "type": "string", - "format": "uuid" - }, - "sled_model": { - "type": "string" - }, - "sled_revision": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "sled_serial": { - "type": "string" - } - }, - "required": [ - "project_id", - "silo_id", - "sled_id", - "sled_model", - "sled_revision", - "sled_serial" - ] - }, - "InstanceMigrateInitiateRequest": { - "type": "object", - "properties": { - "migration_id": { - "type": "string", - "format": "uuid" - }, - "src_addr": { - "type": "string" - }, - "src_uuid": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "migration_id", - "src_addr", - "src_uuid" - ] - }, - "InstanceMigrateInitiateResponse": { - "type": "object", - "properties": { - "migration_id": { - "type": "string", - "format": "uuid" - } - }, - "required": [ - "migration_id" - ] - }, - "InstanceMigrateStatusResponse": { - "description": "The statuses of the most recent attempts to live migrate into and out of this Propolis.\n\nIf a VM is initialized by migration in and then begins to migrate out, this structure will contain statuses for both migrations. This ensures that clients can always obtain the status of a successful migration in even after a migration out begins.\n\nThis structure only reports the status of the most recent migration in a single direction. That is, if a migration in or out fails, and a new migration attempt begins, the new migration's status replaces the old's.", - "type": "object", - "properties": { - "migration_in": { - "nullable": true, - "description": "The status of the most recent attempt to initialize the current instance via migration in, or `None` if the instance has never been a migration target.", - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMigrationStatus" - } - ] - }, - "migration_out": { - "nullable": true, - "description": "The status of the most recent attempt to migrate out of the current instance, or `None` if the instance has never been a migration source.", - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMigrationStatus" - } - ] - } - } - }, - "InstanceMigrationStatus": { - "description": "The status of an individual live migration.", - "type": "object", - "properties": { - "id": { - "description": "The ID of this migration, supplied either by the external migration requester (for targets) or the other side of the migration (for sources).", - "type": "string", - "format": "uuid" - }, - "state": { - "description": "The current phase the migration is in.", - "allOf": [ - { - "$ref": "#/components/schemas/MigrationState" - } - ] - } - }, - "required": [ - "id", - "state" - ] - }, - "InstanceProperties": { - "type": "object", - "properties": { - "bootrom_id": { - "description": "ID of the bootrom used to initialize this Instance.", - "type": "string", - "format": "uuid" - }, - "description": { - "description": "Free-form text description of an Instance.", - "type": "string" - }, - "id": { - "description": "Unique identifier for this Instance.", - "type": "string", - "format": "uuid" - }, - "image_id": { - "description": "ID of the image used to initialize this Instance.", - "type": "string", - "format": "uuid" - }, - "memory": { - "description": "Size of memory allocated to the Instance, in MiB.", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "metadata": { - "description": "Metadata used to track statistics for this Instance.", - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMetadata" - } - ] - }, - "name": { - "description": "Human-readable name of the Instance.", - "type": "string" - }, - "vcpus": { - "description": "Number of vCPUs to be allocated to the Instance.", - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "bootrom_id", - "description", - "id", - "image_id", - "memory", - "metadata", - "name", - "vcpus" - ] - }, - "InstanceSerialConsoleHistoryResponse": { - "description": "Contents of an Instance's serial console buffer.", - "type": "object", - "properties": { - "data": { - "description": "The bytes starting from the requested offset up to either the end of the buffer or the request's `max_bytes`. Provided as a u8 array rather than a string, as it may not be UTF-8.", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "last_byte_offset": { - "description": "The absolute offset since boot (suitable for use as `byte_offset` in a subsequent request) of the last byte returned in `data`.", - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "data", - "last_byte_offset" - ] - }, - "InstanceSpecEnsureRequest": { - "type": "object", - "properties": { - "instance_spec": { - "$ref": "#/components/schemas/VersionedInstanceSpec" - }, - "migrate": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/InstanceMigrateInitiateRequest" - } - ] - }, - "properties": { - "$ref": "#/components/schemas/InstanceProperties" - } - }, - "required": [ - "instance_spec", - "properties" - ] - }, - "InstanceSpecGetResponse": { - "type": "object", - "properties": { - "properties": { - "$ref": "#/components/schemas/InstanceProperties" - }, - "spec": { - "$ref": "#/components/schemas/VersionedInstanceSpec" - }, - "state": { - "$ref": "#/components/schemas/InstanceState" - } - }, - "required": [ - "properties", - "spec", - "state" - ] - }, - "InstanceSpecV0": { - "type": "object", - "properties": { - "backends": { - "$ref": "#/components/schemas/BackendSpecV0" - }, - "devices": { - "$ref": "#/components/schemas/DeviceSpecV0" - } - }, - "required": [ - "backends", - "devices" - ], - "additionalProperties": false - }, - "InstanceState": { - "description": "Current state of an Instance.", - "type": "string", - "enum": [ - "Creating", - "Starting", - "Running", - "Stopping", - "Stopped", - "Rebooting", - "Migrating", - "Repairing", - "Failed", - "Destroyed" - ] - }, - "InstanceStateMonitorRequest": { - "type": "object", - "properties": { - "gen": { - "type": "integer", - "format": "uint64", - "minimum": 0 - } - }, - "required": [ - "gen" - ] - }, - "InstanceStateMonitorResponse": { - "type": "object", - "properties": { - "gen": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "migration": { - "$ref": "#/components/schemas/InstanceMigrateStatusResponse" - }, - "state": { - "$ref": "#/components/schemas/InstanceState" - } - }, - "required": [ - "gen", - "migration", - "state" - ] - }, - "InstanceStateRequested": { - "type": "string", - "enum": [ - "Run", - "Stop", - "Reboot" - ] - }, - "InstanceVCRReplace": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "vcr_json": { - "type": "string" - } - }, - "required": [ - "name", - "vcr_json" - ] - }, - "MigrationState": { - "type": "string", - "enum": [ - "Sync", - "RamPush", - "Pause", - "RamPushDirty", - "Device", - "Resume", - "RamPull", - "Server", - "Finish", - "Error" - ] - }, - "NetworkBackendV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioNetworkBackend" - }, - "type": { - "type": "string", - "enum": [ - "Virtio" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/DlpiNetworkBackend" - }, - "type": { - "type": "string", - "enum": [ - "Dlpi" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, - "NetworkDeviceV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioNic" - }, - "type": { - "type": "string", - "enum": [ - "VirtioNic" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, - "NetworkInterface": { - "type": "object", - "properties": { - "attachment": { - "$ref": "#/components/schemas/NetworkInterfaceAttachmentState" - }, - "name": { - "type": "string" - } - }, - "required": [ - "attachment", - "name" - ] - }, - "NetworkInterfaceAttachmentState": { - "oneOf": [ - { - "type": "string", - "enum": [ - "Detached", - "Faulted" - ] - }, - { - "type": "object", - "properties": { - "Attached": { - "$ref": "#/components/schemas/Slot" - } - }, - "required": [ - "Attached" - ], - "additionalProperties": false - } - ] - }, - "NetworkInterfaceRequest": { - "type": "object", - "properties": { - "interface_id": { - "type": "string", - "format": "uuid" - }, - "name": { - "type": "string" - }, - "slot": { - "$ref": "#/components/schemas/Slot" - } - }, - "required": [ - "interface_id", - "name", - "slot" - ] - }, - "NvmeDisk": { - "description": "A disk that presents an NVMe interface to the guest.", - "type": "object", - "properties": { - "backend_name": { - "description": "The name of the disk's backend component.", - "type": "string" - }, - "pci_path": { - "description": "The PCI bus/device/function at which this disk should be attached.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "backend_name", - "pci_path" - ], - "additionalProperties": false - }, - "P9fs": { - "type": "object", - "properties": { - "chunk_size": { - "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "pci_path": { - "description": "The PCI path at which to attach the guest to this P9 filesystem.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - }, - "source": { - "description": "The host source path to mount into the guest.", - "type": "string" - }, - "target": { - "description": "The 9P target filesystem tag.", - "type": "string" - } - }, - "required": [ - "chunk_size", - "pci_path", - "source", - "target" - ], - "additionalProperties": false - }, - "PciPath": { - "description": "A PCI bus/device/function tuple.", - "type": "object", - "properties": { - "bus": { - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "device": { - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "function": { - "type": "integer", - "format": "uint8", - "minimum": 0 - } - }, - "required": [ - "bus", - "device", - "function" - ] - }, - "PciPciBridge": { - "description": "A PCI-PCI bridge.", - "type": "object", - "properties": { - "downstream_bus": { - "description": "The logical bus number of this bridge's downstream bus. Other devices may use this bus number in their PCI paths to indicate they should be attached to this bridge's bus.", - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "pci_path": { - "description": "The PCI path at which to attach this bridge.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "downstream_bus", - "pci_path" - ], - "additionalProperties": false - }, - "QemuPvpanic": { - "type": "object", - "properties": { - "enable_isa": { - "description": "Enable the QEMU PVPANIC ISA bus device (I/O port 0x505).", - "type": "boolean" - } - }, - "required": [ - "enable_isa" - ], - "additionalProperties": false - }, - "ReplaceResult": { - "type": "string", - "enum": [ - "started", - "started_already", - "completed_already", - "missing", - "vcr_matches" - ] - }, - "SerialPort": { - "description": "A serial port device.", - "type": "object", - "properties": { - "num": { - "description": "The serial port number for this port.", - "allOf": [ - { - "$ref": "#/components/schemas/SerialPortNumber" - } - ] - } - }, - "required": [ - "num" - ], - "additionalProperties": false - }, - "SerialPortNumber": { - "description": "A serial port identifier, which determines what I/O ports a guest can use to access a port.", - "type": "string", - "enum": [ - "com1", - "com2", - "com3", - "com4" - ] - }, - "Slot": { - "description": "A stable index which is translated by Propolis into a PCI BDF, visible to the guest.", - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "SoftNpuP9": { - "type": "object", - "properties": { - "pci_path": { - "description": "The PCI path at which to attach the guest to this port.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "pci_path" - ], - "additionalProperties": false - }, - "SoftNpuPciPort": { - "type": "object", - "properties": { - "pci_path": { - "description": "The PCI path at which to attach the guest to this port.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "pci_path" - ], - "additionalProperties": false - }, - "SoftNpuPort": { - "type": "object", - "properties": { - "backend_name": { - "description": "The name of the device's backend.", - "type": "string" - }, - "name": { - "description": "The name of the SoftNpu port.", - "type": "string" - } - }, - "required": [ - "backend_name", - "name" - ], - "additionalProperties": false - }, - "StorageBackendV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/CrucibleStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "Crucible" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/FileStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "File" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/BlobStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "Blob" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, - "StorageDeviceV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioDisk" - }, - "type": { - "type": "string", - "enum": [ - "VirtioDisk" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/NvmeDisk" - }, - "type": { - "type": "string", - "enum": [ - "NvmeDisk" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, - "VersionedInstanceSpec": { - "description": "A versioned instance spec.", - "oneOf": [ - { - "type": "object", - "properties": { - "spec": { - "$ref": "#/components/schemas/InstanceSpecV0" - }, - "version": { - "type": "string", - "enum": [ - "V0" - ] - } - }, - "required": [ - "spec", - "version" - ], - "additionalProperties": false - } - ] - }, - "VirtioDisk": { - "description": "A disk that presents a virtio-block interface to the guest.", - "type": "object", - "properties": { - "backend_name": { - "description": "The name of the disk's backend component.", - "type": "string" - }, - "pci_path": { - "description": "The PCI bus/device/function at which this disk should be attached.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "backend_name", - "pci_path" - ], - "additionalProperties": false - }, - "VirtioNetworkBackend": { - "description": "A network backend associated with a virtio-net (viona) VNIC on the host.", - "type": "object", - "properties": { - "vnic_name": { - "description": "The name of the viona VNIC to use as a backend.", - "type": "string" - } - }, - "required": [ - "vnic_name" - ], - "additionalProperties": false - }, - "VirtioNic": { - "description": "A network card that presents a virtio-net interface to the guest.", - "type": "object", - "properties": { - "backend_name": { - "description": "The name of the device's backend.", - "type": "string" - }, - "interface_id": { - "description": "A caller-defined correlation identifier for this interface. If Propolis is configured to collect network interface kstats in its Oximeter metrics, the metric series for this interface will be associated with this identifier.", - "type": "string", - "format": "uuid" - }, - "pci_path": { - "description": "The PCI path at which to attach this device.", - "allOf": [ - { - "$ref": "#/components/schemas/PciPath" - } - ] - } - }, - "required": [ - "backend_name", - "interface_id", - "pci_path" - ], - "additionalProperties": false - }, - "VolumeConstructionRequest": { - "oneOf": [ - { - "type": "object", - "properties": { - "block_size": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "id": { - "type": "string", - "format": "uuid" - }, - "read_only_parent": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/VolumeConstructionRequest" - } - ] - }, - "sub_volumes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/VolumeConstructionRequest" - } - }, - "type": { - "type": "string", - "enum": [ - "volume" - ] - } - }, - "required": [ - "block_size", - "id", - "sub_volumes", - "type" - ] - }, - { - "type": "object", - "properties": { - "block_size": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "id": { - "type": "string", - "format": "uuid" - }, - "type": { - "type": "string", - "enum": [ - "url" - ] - }, - "url": { - "type": "string" - } - }, - "required": [ - "block_size", - "id", - "type", - "url" - ] - }, - { - "type": "object", - "properties": { - "block_size": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "blocks_per_extent": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "extent_count": { - "type": "integer", - "format": "uint32", - "minimum": 0 - }, - "gen": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "opts": { - "$ref": "#/components/schemas/CrucibleOpts" - }, - "type": { - "type": "string", - "enum": [ - "region" - ] - } - }, - "required": [ - "block_size", - "blocks_per_extent", - "extent_count", - "gen", - "opts", - "type" - ] - }, - { - "type": "object", - "properties": { - "block_size": { - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "id": { - "type": "string", - "format": "uuid" - }, - "path": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "file" - ] - } - }, - "required": [ - "block_size", - "id", - "path", - "type" - ] - } - ] - }, - "VolumeStatus": { - "type": "object", - "properties": { - "active": { - "type": "boolean" - } - }, - "required": [ - "active" - ] - } - }, - "responses": { - "Error": { - "description": "Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - } - } - } - } - } - } -} diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 41f3fb5a2..0e7e1960a 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -457,27 +457,6 @@ }, "components": { "schemas": { - "BackendSpecV0": { - "type": "object", - "properties": { - "network_backends": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/NetworkBackendV0" - } - }, - "storage_backends": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/StorageBackendV0" - } - } - }, - "required": [ - "network_backends", - "storage_backends" - ] - }, "BlobStorageBackend": { "description": "A storage backend for a disk whose initial contents are given explicitly by the specification.", "type": "object", @@ -501,6 +480,14 @@ "description": "A VM's mainboard.", "type": "object", "properties": { + "boot_settings": { + "description": "The boot device order to supply to the guest.", + "allOf": [ + { + "$ref": "#/components/schemas/BootSettings" + } + ] + }, "chipset": { "description": "The chipset to expose to guest software.", "allOf": [ @@ -523,6 +510,7 @@ } }, "required": [ + "boot_settings", "chipset", "cpus", "memory_mb" @@ -581,6 +569,295 @@ } ] }, + "ComponentV0": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "VirtioDisk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "NvmeDisk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "VirtioNic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "SerialPort" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "PciPciBridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "QemuPvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "SoftNpuPciPort" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "SoftNpuPort" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "SoftNpuP9" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/P9fs" + }, + "type": { + "type": "string", + "enum": [ + "P9fs" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "CrucibleStorageBackend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "FileStorageBackend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "BlobStorageBackend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "VirtioNetworkBackend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "DlpiNetworkBackend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, "CrucibleOpts": { "type": "object", "properties": { @@ -615,96 +892,40 @@ "read_only": { "type": "boolean" }, - "root_cert_pem": { - "nullable": true, - "type": "string" - }, - "target": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "lossy", - "read_only", - "target" - ] - }, - "CrucibleStorageBackend": { - "description": "A Crucible storage backend.", - "type": "object", - "properties": { - "readonly": { - "description": "Indicates whether the storage is read-only.", - "type": "boolean" - }, - "request_json": { - "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", - "type": "string" - } - }, - "required": [ - "readonly", - "request_json" - ], - "additionalProperties": false - }, - "DeviceSpecV0": { - "type": "object", - "properties": { - "board": { - "$ref": "#/components/schemas/Board" - }, - "boot_settings": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/BootSettings" - } - ] - }, - "network_devices": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/NetworkDeviceV0" - } - }, - "pci_pci_bridges": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/PciPciBridge" - } - }, - "qemu_pvpanic": { - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/QemuPvpanic" - } - ] - }, - "serial_ports": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/SerialPort" - } + "root_cert_pem": { + "nullable": true, + "type": "string" }, - "storage_devices": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/StorageDeviceV0" + "target": { + "type": "array", + "items": { + "type": "string" } } }, "required": [ - "board", - "network_devices", - "pci_pci_bridges", - "serial_ports", - "storage_devices" + "id", + "lossy", + "read_only", + "target" + ] + }, + "CrucibleStorageBackend": { + "description": "A Crucible storage backend.", + "type": "object", + "properties": { + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "request_json": { + "description": "A serialized `[crucible_client_types::VolumeConstructionRequest]`. This is stored in serialized form so that breaking changes to the definition of a `VolumeConstructionRequest` do not inadvertently break instance spec deserialization.\n\nWhen using a spec to initialize a new instance, the spec author must ensure this request is well-formed and can be deserialized by the version of `crucible_client_types` used by the target Propolis.", + "type": "string" + } + }, + "required": [ + "readonly", + "request_json" ], "additionalProperties": false }, @@ -1191,16 +1412,19 @@ "InstanceSpecV0": { "type": "object", "properties": { - "backends": { - "$ref": "#/components/schemas/BackendSpecV0" + "board": { + "$ref": "#/components/schemas/Board" }, - "devices": { - "$ref": "#/components/schemas/DeviceSpecV0" + "components": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ComponentV0" + } } }, "required": [ - "backends", - "devices" + "board", + "components" ], "additionalProperties": false }, @@ -1292,71 +1516,6 @@ "Error" ] }, - "NetworkBackendV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioNetworkBackend" - }, - "type": { - "type": "string", - "enum": [ - "Virtio" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/DlpiNetworkBackend" - }, - "type": { - "type": "string", - "enum": [ - "Dlpi" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, - "NetworkDeviceV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioNic" - }, - "type": { - "type": "string", - "enum": [ - "VirtioNic" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - } - ] - }, "NetworkInterface": { "type": "object", "properties": { @@ -1438,6 +1597,40 @@ ], "additionalProperties": false }, + "P9fs": { + "type": "object", + "properties": { + "chunk_size": { + "description": "The chunk size to use in the 9P protocol. Vanilla Helios images should use 8192. Falcon Helios base images and Linux can use up to 65536.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "pci_path": { + "description": "The PCI path at which to attach the guest to this P9 filesystem.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "source": { + "description": "The host source path to mount into the guest.", + "type": "string" + }, + "target": { + "description": "The 9P target filesystem tag.", + "type": "string" + } + }, + "required": [ + "chunk_size", + "pci_path", + "source", + "target" + ], + "additionalProperties": false + }, "PciPath": { "description": "A PCI bus/device/function tuple.", "type": "object", @@ -1546,108 +1739,57 @@ "format": "uint8", "minimum": 0 }, - "StorageBackendV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/CrucibleStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "Crucible" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/FileStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "File" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/BlobStorageBackend" - }, - "type": { - "type": "string", - "enum": [ - "Blob" - ] + "SoftNpuP9": { + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false + ] } - ] + }, + "required": [ + "pci_path" + ], + "additionalProperties": false }, - "StorageDeviceV0": { - "oneOf": [ - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/VirtioDisk" - }, - "type": { - "type": "string", - "enum": [ - "VirtioDisk" - ] + "SoftNpuPciPort": { + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "type": "object", + "properties": { + "backend_name": { + "description": "The name of the device's backend.", + "type": "string" }, - { - "type": "object", - "properties": { - "component": { - "$ref": "#/components/schemas/NvmeDisk" - }, - "type": { - "type": "string", - "enum": [ - "NvmeDisk" - ] - } - }, - "required": [ - "component", - "type" - ], - "additionalProperties": false + "name": { + "description": "The name of the SoftNpu port.", + "type": "string" } - ] + }, + "required": [ + "backend_name", + "name" + ], + "additionalProperties": false }, "VersionedInstanceSpec": { "description": "A versioned instance spec.", diff --git a/phd-tests/framework/src/disk/crucible.rs b/phd-tests/framework/src/disk/crucible.rs index c8b543450..244f87309 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::types::{ - CrucibleOpts, CrucibleStorageBackend, StorageBackendV0, + ComponentV0, CrucibleOpts, CrucibleStorageBackend, VolumeConstructionRequest, }; use rand::{rngs::StdRng, RngCore, SeedableRng}; @@ -286,7 +286,7 @@ impl super::DiskConfig for CrucibleDisk { &self.device_name } - fn backend_spec(&self) -> StorageBackendV0 { + fn backend_spec(&self) -> ComponentV0 { let gen = self.generation.load(Ordering::Relaxed); let downstairs_addrs = self .downstairs_instances @@ -324,7 +324,7 @@ impl super::DiskConfig for CrucibleDisk { }), }; - StorageBackendV0::Crucible(CrucibleStorageBackend { + ComponentV0::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 3fbf05308..294c4c76c 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::types::{FileStorageBackend, StorageBackendV0}; +use propolis_client::types::{ComponentV0, FileStorageBackend}; use tracing::{debug, error, warn}; use uuid::Uuid; @@ -127,8 +127,8 @@ impl super::DiskConfig for FileBackedDisk { &self.device_name } - fn backend_spec(&self) -> StorageBackendV0 { - StorageBackendV0::File(FileStorageBackend { + fn backend_spec(&self) -> ComponentV0 { + ComponentV0::FileStorageBackend(FileStorageBackend { path: self.file.path().to_string(), readonly: false, }) diff --git a/phd-tests/framework/src/disk/in_memory.rs b/phd-tests/framework/src/disk/in_memory.rs index 5300c4f68..44bc6cd1f 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::types::{BlobStorageBackend, StorageBackendV0}; +use propolis_client::types::{BlobStorageBackend, ComponentV0}; use super::DiskConfig; use crate::disk::DeviceName; @@ -34,13 +34,13 @@ impl DiskConfig for InMemoryDisk { &self.device_name } - fn backend_spec(&self) -> StorageBackendV0 { + fn backend_spec(&self) -> ComponentV0 { let base64 = base64::Engine::encode( &base64::engine::general_purpose::STANDARD, self.contents.as_slice(), ); - StorageBackendV0::Blob(BlobStorageBackend { + ComponentV0::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 35ec84af4..a0ca5c506 100644 --- a/phd-tests/framework/src/disk/mod.rs +++ b/phd-tests/framework/src/disk/mod.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use in_memory::InMemoryDisk; -use propolis_client::types::StorageBackendV0; +use propolis_client::types::ComponentV0; use thiserror::Error; use crate::{ @@ -114,7 +114,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) -> StorageBackendV0; + fn backend_spec(&self) -> ComponentV0; /// 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 ddfdb42f8..22f29d8c6 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -6,12 +6,10 @@ use std::collections::BTreeMap; use std::sync::Arc; use anyhow::Context; -use propolis_client::{ - instance_spec::SpecBuilderV0, - types::{ - InstanceMetadata, NvmeDisk, PciPath, SerialPortNumber, StorageDeviceV0, - VirtioDisk, - }, +use propolis_client::types::{ + Board, BootOrderEntry, BootSettings, Chipset, ComponentV0, + InstanceMetadata, InstanceSpecV0, NvmeDisk, PciPath, SerialPort, + SerialPortNumber, VirtioDisk, }; use uuid::Uuid; @@ -258,8 +256,15 @@ impl<'dr> VmConfig<'dr> { ); } - let mut spec_builder = - SpecBuilderV0::new(self.cpus, self.memory_mib, false); + let mut spec = InstanceSpecV0 { + board: Board { + cpus: self.cpus, + memory_mb: self.memory_mib, + chipset: Chipset::default(), + boot_settings: BootSettings::default(), + }, + ..Default::default() + }; // Iterate over the collection of disks and handles and add spec // elements for all of them. This assumes the disk handles were created @@ -272,42 +277,40 @@ 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 => { - StorageDeviceV0::VirtioDisk(VirtioDisk { - backend_name: backend_name.clone().into_string(), - pci_path, - }) - } - DiskInterface::Nvme => StorageDeviceV0::NvmeDisk(NvmeDisk { + DiskInterface::Virtio => ComponentV0::VirtioDisk(VirtioDisk { + backend_name: backend_name.clone().into_string(), + pci_path, + }), + DiskInterface::Nvme => ComponentV0::NvmeDisk(NvmeDisk { backend_name: backend_name.clone().into_string(), pci_path, }), }; - spec_builder - .add_storage_device( - device_name.into_string(), - device_spec, - backend_name.into_string(), - backend_spec, - ) - .context("adding storage device to spec")?; + let _old = + spec.components.insert(device_name.into_string(), device_spec); + assert!(_old.is_none()); + let _old = spec + .components + .insert(backend_name.into_string(), backend_spec); + assert!(_old.is_none()); } - spec_builder - .add_serial_port(SerialPortNumber::Com1) - .context("adding serial port to spec")?; + let _old = spec.components.insert( + "com1".to_string(), + ComponentV0::SerialPort(SerialPort { num: SerialPortNumber::Com1 }), + ); + assert!(_old.is_none()); if let Some(boot_order) = self.boot_order.as_ref() { - spec_builder - .set_boot_order( - boot_order.iter().map(|x| x.to_string()).collect(), - ) - .context("adding boot order to spec")?; + spec.board.boot_settings = BootSettings { + order: boot_order + .iter() + .map(|item| BootOrderEntry { name: item.to_string() }) + .collect(), + }; } - let instance_spec = spec_builder.finish(); - // Generate random identifiers for this instance's timeseries metadata. let sled_id = Uuid::new_v4(); let metadata = InstanceMetadata { @@ -321,7 +324,7 @@ impl<'dr> VmConfig<'dr> { Ok(VmSpec { vm_name: self.vm_name.clone(), - instance_spec, + instance_spec: spec, disk_handles, guest_os_kind, config_toml_contents, diff --git a/phd-tests/framework/src/test_vm/mod.rs b/phd-tests/framework/src/test_vm/mod.rs index fccc439d6..062fc0c5b 100644 --- a/phd-tests/framework/src/test_vm/mod.rs +++ b/phd-tests/framework/src/test_vm/mod.rs @@ -296,8 +296,8 @@ impl TestVm { ) -> Result { let (vcpus, memory_mib) = match self.state { VmState::New => ( - self.spec.instance_spec.devices.board.cpus, - self.spec.instance_spec.devices.board.memory_mb, + self.spec.instance_spec.board.cpus, + self.spec.instance_spec.board.memory_mb, ), VmState::Ensured { .. } => { return Err(VmStateError::InstanceAlreadyEnsured.into()) @@ -555,7 +555,7 @@ impl TestVm { let timeout_duration = match Into::::into(timeout) { MigrationTimeout::Explicit(val) => val, MigrationTimeout::InferFromMemorySize => { - let mem_mib = self.spec.instance_spec.devices.board.memory_mb; + let mem_mib = self.spec.instance_spec.board.memory_mb; std::time::Duration::from_secs( (MIGRATION_SECS_PER_GUEST_GIB * mem_mib) / 1024, ) diff --git a/phd-tests/framework/src/test_vm/spec.rs b/phd-tests/framework/src/test_vm/spec.rs index 0d25dc271..92814f7f2 100644 --- a/phd-tests/framework/src/test_vm/spec.rs +++ b/phd-tests/framework/src/test_vm/spec.rs @@ -9,8 +9,7 @@ use crate::{ guest_os::GuestOsKind, }; use propolis_client::types::{ - DiskRequest, InstanceMetadata, InstanceSpecV0, PciPath, Slot, - StorageBackendV0, StorageDeviceV0, + ComponentV0, DiskRequest, InstanceMetadata, InstanceSpecV0, PciPath, Slot, }; use uuid::Uuid; @@ -49,16 +48,10 @@ impl VmSpec { let backend_spec = disk.backend_spec(); let backend_name = disk.device_name().clone().into_backend_name().into_string(); - match self - .instance_spec - .backends - .storage_backends - .get(&backend_name) - { - Some(StorageBackendV0::Crucible(_)) => { + match self.instance_spec.components.get(&backend_name) { + Some(ComponentV0::CrucibleStorageBackend(_)) => { self.instance_spec - .backends - .storage_backends + .components .insert(backend_name, backend_spec); } Some(_) | None => {} @@ -107,35 +100,41 @@ impl VmSpec { } } - fn get_device_info( - device: &StorageDeviceV0, - ) -> anyhow::Result { + fn get_device_info(device: &ComponentV0) -> anyhow::Result { match device { - StorageDeviceV0::VirtioDisk(d) => Ok(DeviceInfo { + ComponentV0::VirtioDisk(d) => Ok(DeviceInfo { backend_name: &d.backend_name, interface: "virtio", slot: convert_to_slot(d.pci_path)?, }), - StorageDeviceV0::NvmeDisk(d) => Ok(DeviceInfo { + ComponentV0::NvmeDisk(d) => Ok(DeviceInfo { backend_name: &d.backend_name, interface: "nvme", slot: convert_to_slot(d.pci_path)?, }), + _ => { + panic!("asked to get device info for a non-storage device") + } } } let mut reqs = vec![]; - for (name, device) in self.instance_spec.devices.storage_devices.iter() + for (name, device) in + self.instance_spec.components.iter().filter(|(_, c)| { + matches!( + c, + ComponentV0::VirtioDisk(_) | ComponentV0::NvmeDisk(_) + ) + }) { let info = get_device_info(device)?; let backend = self .instance_spec - .backends - .storage_backends + .components .get(info.backend_name) .expect("storage device should have a matching backend"); - let StorageBackendV0::Crucible(backend) = backend else { + let ComponentV0::CrucibleStorageBackend(backend) = backend else { anyhow::bail!("disk {name} does not have a Crucible backend"); }; diff --git a/phd-tests/tests/src/smoke.rs b/phd-tests/tests/src/smoke.rs index 9095e0c6d..5537e19eb 100644 --- a/phd-tests/tests/src/smoke.rs +++ b/phd-tests/tests/src/smoke.rs @@ -51,6 +51,6 @@ async fn instance_spec_get_test(ctx: &Framework) { let spec_get_response = vm.get_spec().await?; let propolis_client::types::VersionedInstanceSpec::V0(spec) = spec_get_response.spec; - assert_eq!(spec.devices.board.cpus, 4); - assert_eq!(spec.devices.board.memory_mb, 3072); + assert_eq!(spec.board.cpus, 4); + assert_eq!(spec.board.memory_mb, 3072); } From deb14a9cd64d20663f0c253c1e23eb813899e516 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Tue, 30 Jul 2024 23:55:06 +0000 Subject: [PATCH 3/7] remove unused falcon features --- bin/propolis-server/Cargo.toml | 2 +- crates/propolis-api-types/Cargo.toml | 3 --- lib/propolis-client/Cargo.toml | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/bin/propolis-server/Cargo.toml b/bin/propolis-server/Cargo.toml index b21c1e460..ebdb6ff7d 100644 --- a/bin/propolis-server/Cargo.toml +++ b/bin/propolis-server/Cargo.toml @@ -80,4 +80,4 @@ default = [] omicron-build = ["propolis/omicron-build"] # Falcon builds require corresponding bits turned on in the dependency libs -falcon = ["propolis/falcon", "propolis_api_types/falcon"] +falcon = ["propolis/falcon"] diff --git a/crates/propolis-api-types/Cargo.toml b/crates/propolis-api-types/Cargo.toml index e5ef2913f..5deedb3dd 100644 --- a/crates/propolis-api-types/Cargo.toml +++ b/crates/propolis-api-types/Cargo.toml @@ -14,6 +14,3 @@ schemars.workspace = true serde.workspace = true thiserror.workspace = true uuid.workspace = true - -[features] -falcon = [] diff --git a/lib/propolis-client/Cargo.toml b/lib/propolis-client/Cargo.toml index 313f13f45..93b3cec86 100644 --- a/lib/propolis-client/Cargo.toml +++ b/lib/propolis-client/Cargo.toml @@ -24,7 +24,3 @@ uuid = { workspace = true, features = ["serde", "v4"] } [dev-dependencies] tokio = { workspace = true, features = ["test-util", "macros"] } - -[features] -default = [] -falcon = [] From 0bdabd8ad67dc1a743de5332c8798861b5490fdf Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Wed, 18 Sep 2024 18:51:42 +0000 Subject: [PATCH 4/7] touch up some doc comments --- bin/propolis-server/src/lib/migrate/compat.rs | 3 ++- .../src/instance_spec/components/devices.rs | 17 +++++++++++++++++ openapi/propolis-server.json | 4 ++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bin/propolis-server/src/lib/migrate/compat.rs b/bin/propolis-server/src/lib/migrate/compat.rs index 2da008b5a..d180d91ac 100644 --- a/bin/propolis-server/src/lib/migrate/compat.rs +++ b/bin/propolis-server/src/lib/migrate/compat.rs @@ -2,7 +2,8 @@ // 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/. -//! Checks for compatibility of two instance specs. +//! Associated functions for the [`crate::spec::Spec`] type that determine +//! whether two specs describe migration-compatible VMs. use std::collections::HashMap; 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 40d1c007b..27defac39 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -108,6 +108,10 @@ pub struct QemuPvpanic { // Structs for Falcon devices. These devices don't support live migration. // +/// Describes a SoftNPU PCI device. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuPciPort { @@ -115,6 +119,10 @@ pub struct SoftNpuPciPort { pub pci_path: PciPath, } +/// Describes a SoftNPU network port. +/// +/// 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 { @@ -125,6 +133,11 @@ pub struct SoftNpuPort { pub backend_name: String, } +/// Describes a PCI device that shares host files with the guest using the P9 +/// protocol. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SoftNpuP9 { @@ -132,6 +145,10 @@ pub struct SoftNpuP9 { pub pci_path: PciPath, } +/// Describes a filesystem to expose through a P9 device. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] #[serde(deny_unknown_fields)] pub struct P9fs { diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 0e7e1960a..3650c4b35 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -1598,6 +1598,7 @@ "additionalProperties": false }, "P9fs": { + "description": "Describes a filesystem to expose through a P9 device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", "type": "object", "properties": { "chunk_size": { @@ -1740,6 +1741,7 @@ "minimum": 0 }, "SoftNpuP9": { + "description": "Describes a PCI device that shares host files with the guest using the P9 protocol.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", "type": "object", "properties": { "pci_path": { @@ -1757,6 +1759,7 @@ "additionalProperties": false }, "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", "type": "object", "properties": { "pci_path": { @@ -1774,6 +1777,7 @@ "additionalProperties": false }, "SoftNpuPort": { + "description": "Describes a SoftNPU network port.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", "type": "object", "properties": { "backend_name": { From 8b40d530d2f42468fe5a22b2e10cbac36a208614 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Mon, 30 Sep 2024 20:54:51 +0000 Subject: [PATCH 5/7] try making boot settings a component instead --- bin/propolis-server/src/lib/server.rs | 8 +- .../src/lib/spec/api_spec_v0.rs | 75 +++++++++++++------ bin/propolis-server/src/lib/spec/builder.rs | 58 +++++++++++--- bin/propolis-server/src/lib/spec/mod.rs | 61 ++++----------- .../src/instance_spec/components/board.rs | 6 -- .../src/instance_spec/components/devices.rs | 19 +++++ .../src/instance_spec/v0.rs | 1 + crates/propolis-api-types/src/lib.rs | 20 ++--- openapi/propolis-server.json | 37 ++++++--- phd-tests/framework/src/test_vm/config.rs | 17 +++-- 10 files changed, 178 insertions(+), 124 deletions(-) diff --git a/bin/propolis-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index d4561d824..ce0ea860a 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -130,9 +130,11 @@ fn instance_spec_from_request( } if let Some(boot_settings) = request.boot_settings.as_ref() { - for item in boot_settings.order.iter() { - spec_builder.add_boot_option(item.clone())?; - } + let order = boot_settings.order.clone(); + spec_builder.add_boot_order( + "boot-settings".to_string(), + order.into_iter().map(Into::into), + )?; } if let Some(base64) = &request.cloud_init_bytes { 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 0ea2a360d..5ae9af5ee 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -10,8 +10,7 @@ use std::collections::HashMap; use propolis_api_types::instance_spec::{ components::{ backends::{DlpiNetworkBackend, VirtioNetworkBackend}, - board::Board as ApiBoard, - devices::SerialPort as SerialPortDesc, + devices::{BootSettings, SerialPort as SerialPortDesc}, }, v0::{ComponentV0, InstanceSpecV0}, }; @@ -50,6 +49,20 @@ pub(crate) enum ApiSpecError { impl From for InstanceSpecV0 { fn from(val: Spec) -> Self { + // Exhaustively destructure the input spec so that adding a new field + // without considering it here will break the build. + let Spec { + board, + disks, + nics, + boot_settings, + serial, + pci_pci_bridges, + pvpanic, + #[cfg(feature = "falcon")] + softnpu, + } = val; + // Inserts a component entry into the supplied map, asserting first that // the supplied key is not present in that map. // @@ -70,24 +83,16 @@ impl From for InstanceSpecV0 { spec.components.insert(key, val); } - let board = ApiBoard { - cpus: val.board.cpus, - memory_mb: val.board.memory_mb, - chipset: val.board.chipset, - boot_settings: val.boot_settings.map(Into::into).unwrap_or( - propolis_api_types::BootSettings { order: Vec::new() }, - ), - }; let mut spec = InstanceSpecV0 { board, ..Default::default() }; - for (disk_name, disk) in val.disks { + for (disk_name, disk) in disks { let backend_name = disk.device_spec.backend_name().to_owned(); insert_component(&mut spec, disk_name, disk.device_spec.into()); insert_component(&mut spec, backend_name, disk.backend_spec.into()); } - for (nic_name, nic) in val.nics { + for (nic_name, nic) in nics { let backend_name = nic.device_spec.backend_name.clone(); insert_component( &mut spec, @@ -102,7 +107,7 @@ impl From for InstanceSpecV0 { ); } - for (name, desc) in val.serial { + for (name, desc) in serial { if desc.device == SerialPortDevice::Uart { insert_component( &mut spec, @@ -112,7 +117,7 @@ impl From for InstanceSpecV0 { } } - for (bridge_name, bridge) in val.pci_pci_bridges { + for (bridge_name, bridge) in pci_pci_bridges { insert_component( &mut spec, bridge_name, @@ -120,17 +125,27 @@ impl From for InstanceSpecV0 { ); } - if let Some(pvpanic) = val.pvpanic { + if let Some(pvpanic) = pvpanic { insert_component( &mut spec, - pvpanic.name.clone(), + pvpanic.name, ComponentV0::QemuPvpanic(pvpanic.spec), ); } + if let Some(settings) = boot_settings { + insert_component( + &mut spec, + settings.name, + ComponentV0::BootSettings(BootSettings { + order: settings.order.into_iter().map(Into::into).collect(), + }), + ); + } + #[cfg(feature = "falcon")] { - if let Some(softnpu_pci) = val.softnpu.pci_port { + if let Some(softnpu_pci) = softnpu.pci_port { insert_component( &mut spec, format!("softnpu-pci-{}", softnpu_pci.pci_path), @@ -138,7 +153,7 @@ impl From for InstanceSpecV0 { ); } - if let Some(p9) = val.softnpu.p9_device { + if let Some(p9) = softnpu.p9_device { insert_component( &mut spec, format!("softnpu-p9-{}", p9.pci_path), @@ -146,7 +161,7 @@ impl From for InstanceSpecV0 { ); } - if let Some(p9fs) = val.softnpu.p9fs { + if let Some(p9fs) = softnpu.p9fs { insert_component( &mut spec, format!("p9fs-{}", p9fs.pci_path), @@ -154,7 +169,7 @@ impl From for InstanceSpecV0 { ); } - for (port_name, port) in val.softnpu.ports { + for (port_name, port) in softnpu.ports { insert_component( &mut spec, port_name.clone(), @@ -180,8 +195,9 @@ impl TryFrom for Spec { type Error = ApiSpecError; fn try_from(value: InstanceSpecV0) -> Result { - let mut builder = SpecBuilder::with_board(&value.board); + let mut builder = SpecBuilder::with_board(value.board); let mut devices: Vec<(String, ComponentV0)> = vec![]; + let mut boot_settings = None; let mut storage_backends: HashMap = HashMap::new(); let mut viona_backends: HashMap = @@ -260,6 +276,14 @@ impl TryFrom for Spec { spec: pvpanic, })?; } + ComponentV0::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_name, settings)); + } #[cfg(not(feature = "falcon"))] ComponentV0::SoftNpuPciPort(_) | ComponentV0::SoftNpuPort(_) @@ -307,8 +331,13 @@ impl TryFrom for Spec { } } - for item in value.board.boot_settings.order.into_iter() { - builder.add_boot_option(item)?; + // 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) = storage_backends.into_keys().next() { diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index acbfe90e5..227d001ce 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -9,12 +9,12 @@ use std::collections::{BTreeSet, HashSet}; use propolis_api_types::{ instance_spec::{ components::{ - board::{Board as ApiBoard, Chipset, I440Fx}, + board::{Board, Chipset, I440Fx}, devices::{PciPciBridge, SerialPortNumber}, }, PciPath, }, - BootOrderEntry, DiskRequest, InstanceProperties, NetworkInterfaceRequest, + DiskRequest, InstanceProperties, NetworkInterfaceRequest, }; use thiserror::Error; @@ -28,7 +28,7 @@ use crate::{config, spec::SerialPortDevice}; use super::{ api_request::{self, DeviceRequestError}, config_toml::{ConfigTomlError, ParsedConfig}, - Board, BootSettings, Disk, Nic, QemuPvpanic, SerialPort, + BootOrderEntry, BootSettings, Disk, Nic, QemuPvpanic, SerialPort, }; #[cfg(feature = "falcon")] @@ -46,19 +46,22 @@ pub(crate) enum SpecBuilderError { #[error("device {0} has the same name as its backend")] DeviceAndBackendNamesIdentical(String), - #[error("A component with name {0} already exists")] + #[error("a component with name {0} already exists")] ComponentNameInUse(String), - #[error("A PCI device is already attached at {0:?}")] + #[error("a PCI device is already attached at {0:?}")] PciPathInUse(PciPath), - #[error("Serial port {0:?} is already specified")] + #[error("serial port {0:?} is already specified")] SerialPortInUse(SerialPortNumber), #[error("pvpanic device already specified")] PvpanicInUse, - #[error("Boot option {0} is not an attached device")] + #[error("boot settings were already specified")] + BootSettingsInUse, + + #[error("boot option {0} is not an attached device")] BootOptionMissing(String), } @@ -84,9 +87,9 @@ impl SpecBuilder { } } - pub(super) fn with_board(api_board: &ApiBoard) -> Self { + pub(super) fn with_board(board: Board) -> Self { Self { - spec: super::Spec { board: api_board.into(), ..Default::default() }, + spec: super::Spec { board, ..Default::default() }, ..Default::default() } } @@ -113,8 +116,36 @@ impl SpecBuilder { Ok(()) } + /// Sets the spec's boot order to the list of disk devices specified in + /// `boot_options`. + /// + /// All of the items in the supplied `boot_options` must already be present + /// in the spec's disk map. + pub fn add_boot_order( + &mut self, + component_name: String, + boot_options: impl Iterator, + ) -> Result<(), SpecBuilderError> { + if self.component_names.contains(&component_name) { + return Err(SpecBuilderError::ComponentNameInUse(component_name)); + } + + if self.spec.boot_settings.is_some() { + return Err(SpecBuilderError::BootSettingsInUse); + } + + self.spec.boot_settings = + Some(BootSettings { name: component_name, order: vec![] }); + + for item in boot_options { + self.add_boot_option(item)?; + } + + Ok(()) + } + /// Add a boot option to the boot option list of the spec under construction. - pub fn add_boot_option( + fn add_boot_option( &mut self, item: BootOrderEntry, ) -> Result<(), SpecBuilderError> { @@ -125,9 +156,12 @@ impl SpecBuilder { let boot_settings = self .spec .boot_settings - .get_or_insert(BootSettings { order: Vec::new() }); + .as_mut() + .expect("boot settings must already exist"); - boot_settings.order.push(item.into()); + boot_settings + .order + .push(crate::spec::BootOrderEntry { name: item.name.clone() }); Ok(()) } diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 0245afb16..4efe20488 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -22,7 +22,7 @@ use propolis_api_types::instance_spec::{ BlobStorageBackend, CrucibleStorageBackend, FileStorageBackend, VirtioNetworkBackend, }, - board::{Chipset, I440Fx}, + board::Board, devices::{ NvmeDisk, PciPciBridge, QemuPvpanic as QemuPvpanicDesc, SerialPortNumber, VirtioDisk, VirtioNic, @@ -74,65 +74,30 @@ pub(crate) struct Spec { } #[derive(Clone, Debug)] -pub(crate) struct Board { - pub cpus: u8, - pub memory_mb: u64, - pub chipset: Chipset, -} - -impl Default for Board { - fn default() -> Self { - Self { - cpus: 0, - memory_mb: 0, - chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), - } - } -} - -impl From<&propolis_api_types::instance_spec::components::board::Board> - for Board -{ - fn from( - value: &propolis_api_types::instance_spec::components::board::Board, - ) -> Self { - Board { - cpus: value.cpus, - memory_mb: value.memory_mb, - chipset: value.chipset, - } - } -} - -#[derive(Clone, Debug, Default)] pub(crate) struct BootSettings { + pub name: String, pub order: Vec, } -impl From for BootSettings { - fn from(value: propolis_api_types::BootSettings) -> Self { - Self { order: value.order.into_iter().map(Into::into).collect() } - } -} - -impl From for propolis_api_types::BootSettings { - fn from(value: BootSettings) -> Self { - Self { order: value.order.into_iter().map(Into::into).collect() } - } -} - #[derive(Clone, Debug, Default)] pub(crate) struct BootOrderEntry { pub name: String, } -impl From for BootOrderEntry { - fn from(value: propolis_api_types::BootOrderEntry) -> Self { - Self { name: value.name } +impl + From + for BootOrderEntry +{ + fn from( + value: propolis_api_types::instance_spec::components::devices::BootOrderEntry, + ) -> Self { + Self { name: value.name.clone() } } } -impl From for propolis_api_types::BootOrderEntry { +impl From + for propolis_api_types::instance_spec::components::devices::BootOrderEntry +{ fn from(value: BootOrderEntry) -> Self { Self { name: value.name } } diff --git a/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index f8713949b..df8d9e24c 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -8,8 +8,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::BootSettings; - /// An Intel 440FX-compatible chipset. #[derive( Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, @@ -48,9 +46,6 @@ pub struct Board { /// The chipset to expose to guest software. pub chipset: Chipset, - - /// The boot device order to supply to the guest. - pub boot_settings: BootSettings, // TODO: Guest platform and CPU feature identification. // TODO: NUMA topology. } @@ -61,7 +56,6 @@ impl Default for Board { cpus: 0, memory_mb: 0, chipset: Chipset::I440Fx(I440Fx { enable_pcie: false }), - boot_settings: BootSettings { order: vec![] }, } } } 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 27defac39..f40a595d6 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -104,6 +104,25 @@ pub struct QemuPvpanic { // TODO(eliza): add support for the PCI PVPANIC device... } +/// A set of settings that determines what Propolis tells guest firmware about +/// how to boot a guest operating system. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] +#[serde(deny_unknown_fields)] +pub struct BootSettings { + /// An ordered list of components to attempt to boot from. + pub order: Vec, +} + +/// An entry in the boot order stored in a [`BootSettings`] component. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] +pub struct BootOrderEntry { + /// The name of another component in the spec that Propolis should try to + /// boot from. + /// + /// Currently, only disk device components are supported. + pub name: String, +} + // // Structs for Falcon devices. These devices don't support live migration. // diff --git a/crates/propolis-api-types/src/instance_spec/v0.rs b/crates/propolis-api-types/src/instance_spec/v0.rs index 93b8e545d..4fcdd7f54 100644 --- a/crates/propolis-api-types/src/instance_spec/v0.rs +++ b/crates/propolis-api-types/src/instance_spec/v0.rs @@ -17,6 +17,7 @@ pub enum ComponentV0 { SerialPort(components::devices::SerialPort), PciPciBridge(components::devices::PciPciBridge), QemuPvpanic(components::devices::QemuPvpanic), + BootSettings(components::devices::BootSettings), SoftNpuPciPort(components::devices::SoftNpuPciPort), SoftNpuPort(components::devices::SoftNpuPort), SoftNpuP9(components::devices::SoftNpuP9), diff --git a/crates/propolis-api-types/src/lib.rs b/crates/propolis-api-types/src/lib.rs index 593a2ec4b..a6de2d547 100644 --- a/crates/propolis-api-types/src/lib.rs +++ b/crates/propolis-api-types/src/lib.rs @@ -10,8 +10,14 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use uuid::Uuid; -// Re-export types that are of a public struct +// Re-export the instance spec boot settings types so they can also be used in +// legacy instance ensure requests. +pub use crate::instance_spec::components::devices::{ + BootOrderEntry, BootSettings, +}; use crate::instance_spec::VersionedInstanceSpec; + +// Re-export volume construction requests since they're part of a disk request. pub use crucible_client_types::VolumeConstructionRequest; pub mod instance_spec; @@ -388,18 +394,6 @@ pub struct DiskAttachment { pub state: DiskAttachmentState, } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct BootSettings { - pub order: Vec, -} - -/// An entry in a list of boot options. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct BootOrderEntry { - /// The name of the device to attempt booting from. - pub name: String, -} - /// A stable index which is translated by Propolis /// into a PCI BDF, visible to the guest. #[derive(Copy, Clone, Debug, Deserialize, Serialize, JsonSchema)] diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 3650c4b35..6f834a6c5 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -480,14 +480,6 @@ "description": "A VM's mainboard.", "type": "object", "properties": { - "boot_settings": { - "description": "The boot device order to supply to the guest.", - "allOf": [ - { - "$ref": "#/components/schemas/BootSettings" - } - ] - }, "chipset": { "description": "The chipset to expose to guest software.", "allOf": [ @@ -510,7 +502,6 @@ } }, "required": [ - "boot_settings", "chipset", "cpus", "memory_mb" @@ -518,11 +509,11 @@ "additionalProperties": false }, "BootOrderEntry": { - "description": "An entry in a list of boot options.", + "description": "An entry in the boot order stored in a [`BootSettings`] component.", "type": "object", "properties": { "name": { - "description": "The name of the device to attempt booting from.", + "description": "The name of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", "type": "string" } }, @@ -531,9 +522,11 @@ ] }, "BootSettings": { + "description": "A set of settings that determines what Propolis tells guest firmware about how to boot a guest operating system.", "type": "object", "properties": { "order": { + "description": "An ordered list of components to attempt to boot from.", "type": "array", "items": { "$ref": "#/components/schemas/BootOrderEntry" @@ -542,7 +535,8 @@ }, "required": [ "order" - ] + ], + "additionalProperties": false }, "Chipset": { "description": "A kind of virtual chipset.", @@ -685,6 +679,25 @@ ], "additionalProperties": false }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "BootSettings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, { "type": "object", "properties": { diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index 22f29d8c6..241180665 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -261,7 +261,6 @@ impl<'dr> VmConfig<'dr> { cpus: self.cpus, memory_mb: self.memory_mib, chipset: Chipset::default(), - boot_settings: BootSettings::default(), }, ..Default::default() }; @@ -303,12 +302,16 @@ impl<'dr> VmConfig<'dr> { assert!(_old.is_none()); if let Some(boot_order) = self.boot_order.as_ref() { - spec.board.boot_settings = BootSettings { - order: boot_order - .iter() - .map(|item| BootOrderEntry { name: item.to_string() }) - .collect(), - }; + let _old = spec.components.insert( + "boot-settings".to_string(), + ComponentV0::BootSettings(BootSettings { + order: boot_order + .iter() + .map(|item| BootOrderEntry { name: item.to_string() }) + .collect(), + }), + ); + assert!(_old.is_none()); } // Generate random identifiers for this instance's timeseries metadata. From 900b5c6b52ed18bbb1dc274ff76fd4e0beee98e9 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Mon, 30 Sep 2024 22:35:20 +0000 Subject: [PATCH 6/7] combine boot option routines --- bin/propolis-server/src/lib/spec/builder.rs | 34 ++++++--------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/bin/propolis-server/src/lib/spec/builder.rs b/bin/propolis-server/src/lib/spec/builder.rs index 227d001ce..c19dbfb2a 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -134,35 +134,19 @@ impl SpecBuilder { return Err(SpecBuilderError::BootSettingsInUse); } - self.spec.boot_settings = - Some(BootSettings { name: component_name, order: vec![] }); - + let mut order = vec![]; for item in boot_options { - self.add_boot_option(item)?; - } - - Ok(()) - } + if !self.spec.disks.contains_key(item.name.as_str()) { + return Err(SpecBuilderError::BootOptionMissing( + item.name.clone(), + )); + } - /// Add a boot option to the boot option list of the spec under construction. - fn add_boot_option( - &mut self, - item: BootOrderEntry, - ) -> Result<(), SpecBuilderError> { - if !self.spec.disks.contains_key(item.name.as_str()) { - return Err(SpecBuilderError::BootOptionMissing(item.name.clone())); + order.push(crate::spec::BootOrderEntry { name: item.name.clone() }); } - let boot_settings = self - .spec - .boot_settings - .as_mut() - .expect("boot settings must already exist"); - - boot_settings - .order - .push(crate::spec::BootOrderEntry { name: item.name.clone() }); - + self.spec.boot_settings = + Some(BootSettings { name: component_name, order }); Ok(()) } From 04fb94329a2a7f13b10ac93fdc002aca7b50a117 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Tue, 1 Oct 2024 00:01:48 +0000 Subject: [PATCH 7/7] improve doc comment --- .../src/instance_spec/components/devices.rs | 5 +++-- openapi/propolis-server.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) 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 f40a595d6..06ced9844 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -104,8 +104,9 @@ pub struct QemuPvpanic { // TODO(eliza): add support for the PCI PVPANIC device... } -/// A set of settings that determines what Propolis tells guest firmware about -/// how to boot a guest operating system. +/// Settings supplied to the guest's firmware image that specify the order in +/// which it should consider its options when selecting a device to try to boot +/// from. #[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] #[serde(deny_unknown_fields)] pub struct BootSettings { diff --git a/openapi/propolis-server.json b/openapi/propolis-server.json index 6f834a6c5..f63f1ceee 100644 --- a/openapi/propolis-server.json +++ b/openapi/propolis-server.json @@ -522,7 +522,7 @@ ] }, "BootSettings": { - "description": "A set of settings that determines what Propolis tells guest firmware about how to boot a guest operating system.", + "description": "Settings supplied to the guest's firmware image that specify the order in which it should consider its options when selecting a device to try to boot from.", "type": "object", "properties": { "order": {