From f255595c642967c0d399baca73cc20b1e9b20f75 Mon Sep 17 00:00:00 2001 From: Greg Colombo Date: Mon, 30 Sep 2024 20:35:17 -0700 Subject: [PATCH] instance spec rework: flatten InstanceSpecV0 (#767) Remove the complex hierarchy of component types from InstanceSpecV0 and instead turn V0 specs into a Board and a bag of Components. This makes it much easier to define new component types flexibly: - New components all go into the same collection: no more deciding if you've defined a device or a backend or deciding what to do if you have something in between. - New components always appear in a map and so always have names (their keys). A bonus is that having only one component map prevents keys from being reused in multiple maps. - Because new components don't need fields, there's no need to remember to tag component fields with serde's default or skip_serializing_if attributes (though this may still be needed for component fields). - It's also much harder to get painted into a backwards-compatibility corner. Suppose you define a new component, add a field for it, and make the field an Option. If you later want to support having more than one of that component, you need to add a new spec field with higher maximum cardinality to avoid breaking old specs. Now you have two fields for the same kind of component. Yuck. The downside of all this is that there are now more wire specs that type check but that the server should reject; for example, you can now have a wire spec with multiple panic devices. The previous changes in this series mitigate this by converting wire specs into more rigorously organized internal specs (which may enforce e.g. cardinality limits) before propolis-server will actually use them. Now that wire specs have a much simpler structure, the spec builder in the propolis-client lib pulls very little weight, so remove it. Incoming wire specs need to be validated on the server in any case, and two different server versions may disagree on whether a particular wire spec is acceptable (if one has features the other does not), so it's hard to have a single builder that checks invariants for all relevant server versions. Clients who want to have a mistake-catching/invariant-protecting builder are, of course, still free to define their own (and those who might not want one, like PHD, don't need to pull one in that they won't use). Finally, remove the #[cfg(feature = "falcon")] guards from the api-types crate. Servers built without Falcon support will reject specs that contain Falcon components. The downside to this is that users of generated clients who never intend to access a Falcon server (e.g. sled agent) will start to see these types. The upside is that this means that propolis-server's OpenAPI definition no longer varies with its feature set, which means we no longer need propolis-server-falcon.json (which is easy to forget to update) or the Falcon variant of the generated client (whose Progenitor directives had to be manually kept in sync with the non-Falcon client). This is (once again) a migration protocol-breaking change, so the migrate-from-base tests are (once again) expected to fail. Tests: cargo test, PHD. --- bin/propolis-server/Cargo.toml | 2 +- bin/propolis-server/src/lib/initializer.rs | 6 +- bin/propolis-server/src/lib/migrate/compat.rs | 3 +- bin/propolis-server/src/lib/server.rs | 8 +- .../src/lib/spec/api_spec_v0.rs | 406 ++-- bin/propolis-server/src/lib/spec/builder.rs | 48 +- bin/propolis-server/src/lib/spec/mod.rs | 68 +- crates/propolis-api-types/Cargo.toml | 3 - .../src/instance_spec/components/board.rs | 2 +- .../src/instance_spec/components/devices.rs | 41 +- .../src/instance_spec/v0.rs | 100 +- crates/propolis-api-types/src/lib.rs | 20 +- lib/propolis-client/Cargo.toml | 4 - 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 | 711 +++--- 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 | 74 +- 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 +- 26 files changed, 915 insertions(+), 2998 deletions(-) delete mode 100644 lib/propolis-client/src/instance_spec.rs delete mode 100644 openapi/propolis-server-falcon.json 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/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/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/bin/propolis-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index 711648d6f..5bd585d76 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)?; - } + 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 804b4749e..5ae9af5ee 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -8,43 +8,61 @@ use std::collections::HashMap; use propolis_api_types::instance_spec::{ - components::devices::SerialPort as SerialPortDesc, - v0::{InstanceSpecV0, NetworkBackendV0, NetworkDeviceV0, StorageDeviceV0}, + components::{ + backends::{DlpiNetworkBackend, VirtioNetworkBackend}, + devices::{BootSettings, 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 { 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. // @@ -52,90 +70,119 @@ 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; - for (disk_name, disk) in val.disks { + let mut spec = InstanceSpecV0 { board, ..Default::default() }; + + for (disk_name, disk) in 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 { + for (nic_name, nic) in 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 { + for (name, desc) in 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 { + for (bridge_name, bridge) in 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) = pvpanic { + insert_component( + &mut spec, + 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")] { - spec.devices.softnpu_pci_port = val.softnpu.pci_port; - spec.devices.softnpu_p9 = val.softnpu.p9_device; - spec.devices.p9fs = val.softnpu.p9fs; - for (port_name, port) in val.softnpu.ports { + if let Some(softnpu_pci) = softnpu.pci_port { insert_component( - &mut spec.devices.softnpu_ports, + &mut spec, + format!("softnpu-pci-{}", softnpu_pci.pci_path), + ComponentV0::SoftNpuPciPort(softnpu_pci), + ); + } + + if let Some(p9) = softnpu.p9_device { + insert_component( + &mut spec, + format!("softnpu-p9-{}", p9.pci_path), + ComponentV0::SoftNpuP9(p9), + ); + } + + if let Some(p9fs) = softnpu.p9fs { + insert_component( + &mut spec, + format!("p9fs-{}", p9fs.pci_path), + ComponentV0::P9fs(p9fs), + ); + } + + for (port_name, port) in softnpu.ports { + insert_component( + &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 +194,162 @@ 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 boot_settings = None; + 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 }, - )?; + } + 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(_) + | 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)?; - } + // 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), + )?; } - 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..c19dbfb2a 100644 --- a/bin/propolis-server/src/lib/spec/builder.rs +++ b/bin/propolis-server/src/lib/spec/builder.rs @@ -14,7 +14,7 @@ use propolis_api_types::{ }, 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}, - 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), } @@ -113,20 +116,37 @@ impl SpecBuilder { Ok(()) } - /// Add a boot option to the boot option list of the spec under construction. - pub fn add_boot_option( + /// 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, - item: &BootOrderEntry, + component_name: String, + boot_options: impl Iterator, ) -> Result<(), SpecBuilderError> { - if !self.spec.disks.contains_key(item.name.as_str()) { - return Err(SpecBuilderError::BootOptionMissing(item.name.clone())); + if self.component_names.contains(&component_name) { + return Err(SpecBuilderError::ComponentNameInUse(component_name)); + } + + if self.spec.boot_settings.is_some() { + return Err(SpecBuilderError::BootSettingsInUse); } - let boot_order = self.spec.boot_order.get_or_insert(Vec::new()); + let mut order = vec![]; + for item in boot_options { + if !self.spec.disks.contains_key(item.name.as_str()) { + return Err(SpecBuilderError::BootOptionMissing( + item.name.clone(), + )); + } - boot_order - .push(crate::spec::BootOrderEntry { name: item.name.clone() }); + order.push(crate::spec::BootOrderEntry { name: item.name.clone() }); + } + self.spec.boot_settings = + Some(BootSettings { name: component_name, order }); Ok(()) } diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 230943537..4efe20488 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -28,9 +28,10 @@ use propolis_api_types::instance_spec::{ 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,36 @@ pub(crate) struct Spec { pub softnpu: SoftNpu, } +#[derive(Clone, Debug)] +pub(crate) struct BootSettings { + pub name: String, + pub order: Vec, +} + #[derive(Clone, Debug, Default)] pub(crate) struct BootOrderEntry { pub name: String, } +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::instance_spec::components::devices::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 +133,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 +142,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 +180,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/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/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index e9c3f59de..df8d9e24c 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -35,7 +35,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. 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..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,11 +104,34 @@ pub struct QemuPvpanic { // TODO(eliza): add support for the PCI PVPANIC device... } +/// 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 { + /// 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. // -#[cfg(feature = "falcon")] +/// 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 { @@ -116,7 +139,10 @@ pub struct SoftNpuPciPort { pub pci_path: PciPath, } -#[cfg(feature = "falcon")] +/// 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 { @@ -127,7 +153,11 @@ pub struct SoftNpuPort { pub backend_name: String, } -#[cfg(feature = "falcon")] +/// 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 { @@ -135,7 +165,10 @@ pub struct SoftNpuP9 { pub pci_path: PciPath, } -#[cfg(feature = "falcon")] +/// 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/crates/propolis-api-types/src/instance_spec/v0.rs b/crates/propolis-api-types/src/instance_spec/v0.rs index 31451dcdc..4fcdd7f54 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,28 @@ 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), + BootSettings(components::devices::BootSettings), + 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, } 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/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 = [] 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..f63f1ceee 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", @@ -530,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" } }, @@ -543,9 +522,11 @@ ] }, "BootSettings": { + "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": { + "description": "An ordered list of components to attempt to boot from.", "type": "array", "items": { "$ref": "#/components/schemas/BootOrderEntry" @@ -554,7 +535,8 @@ }, "required": [ "order" - ] + ], + "additionalProperties": false }, "Chipset": { "description": "A kind of virtual chipset.", @@ -581,6 +563,314 @@ } ] }, + "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/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "BootSettings" + ] + } + }, + "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 +905,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 +1425,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 +1529,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 +1610,41 @@ ], "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": { + "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 +1753,60 @@ "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": { + "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": { + "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": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "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": { + "description": "Describes a SoftNPU network port.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_name": { + "description": "The name of the device's backend.", + "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..241180665 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,14 @@ 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(), + }, + ..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 +276,44 @@ 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")?; + 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()); } - 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 +327,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); }