diff --git a/Cargo.lock b/Cargo.lock index f8e04801b..1b605f422 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,6 +548,19 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byte-wrapper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b041d2328ab2041b4e9d8f21e3b0b5b8bb61e9a23bb9171387771c314ea340" +dependencies = [ + "base64 0.22.1", + "hex", + "schemars", + "serde_core", + "serde_json", +] + [[package]] name = "bytecount" version = "0.6.9" @@ -2459,9 +2472,6 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] [[package]] name = "hickory-proto" @@ -5542,6 +5552,7 @@ dependencies = [ name = "propolis-api-types-versions" version = "0.0.0" dependencies = [ + "byte-wrapper", "crucible-client-types", "propolis_types", "schemars", @@ -6794,10 +6805,11 @@ dependencies = [ [[package]] name = "serde_human_bytes" version = "0.1.0" -source = "git+http://github.com/oxidecomputer/serde_human_bytes?branch=main#0a09794501b6208120528c3b457d5f3a8cb17424" +source = "git+https://github.com/oxidecomputer/serde_human_bytes?branch=main#8f60acdfe7c6d9e2a01f59be920c1c2b19129322" dependencies = [ + "base64 0.22.1", "hex", - "serde", + "serde_core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8843a13af..bfcd34583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ bitstruct = "0.1" bitvec = "1.0" byteorder = "1" bytes = "1.7.1" +byte-wrapper = { version = "0.1.0", features = ["serde", "schemars08"] } camino = "1.1.6" cargo_metadata = "0.18.1" cc = "1.0.73" diff --git a/bin/mock-server/src/lib/api_types.rs b/bin/mock-server/src/lib/api_types.rs index 870ffc01b..ad9268ec6 100644 --- a/bin/mock-server/src/lib/api_types.rs +++ b/bin/mock-server/src/lib/api_types.rs @@ -9,6 +9,7 @@ progenitor::generate_api!( spec = "../../openapi/propolis-server/propolis-server-latest.json", derives = [schemars::JsonSchema], replace = { + NvmeDisk = propolis_api_types_versions::latest::components::devices::NvmeDisk, SpecKey = propolis_api_types_versions::latest::instance_spec::SpecKey, }, patch = { diff --git a/bin/propolis-cli/src/main.rs b/bin/propolis-cli/src/main.rs index 49fd3980c..ba5e15c33 100644 --- a/bin/propolis-cli/src/main.rs +++ b/bin/propolis-cli/src/main.rs @@ -212,7 +212,7 @@ fn add_component_to_spec( use std::collections::btree_map::Entry; match spec.components.entry(id) { Entry::Vacant(vacant_entry) => { - vacant_entry.insert(component); + vacant_entry.insert(component.into()); Ok(()) } Entry::Occupied(occupied_entry) => Err(anyhow::anyhow!( @@ -259,11 +259,16 @@ impl DiskRequest { backend_id: backend_id.clone(), pci_path, }), - "nvme" => ComponentV0::NvmeDisk(NvmeDisk { - backend_id: backend_id.clone(), - pci_path, - serial_number: nvme_serial_from_str(&self.name, b' '), - }), + "nvme" => { + let nvme = NvmeDisk { + backend_id: backend_id.clone(), + pci_path, + serial_number: nvme_serial_from_str(&self.name, b' '), + // TODO: populate model_number + model_number: [0u8; 40], + }; + ComponentV0::NvmeDisk(nvme.into()) + } _ => anyhow::bail!( "invalid device type in disk request: {:?}", self.device @@ -420,10 +425,11 @@ impl VmConfig { } // If there are no SoftNPU devices, also enable COM4. + use propolis_client::instance_spec::Component; if !spec .components .iter() - .any(|(_, c)| matches!(c, ComponentV0::SoftNpuPort(_))) + .any(|(_, c)| matches!(c, Component::SoftNpuPort(_))) { add_component_to_spec( &mut spec, diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 353ffd4cf..ab4fd9fc5 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -55,6 +55,8 @@ impl From for InstanceSpec { let smbios = val.smbios_type1_input.clone(); let v1::instance_spec::InstanceSpec { board, components } = v1::instance_spec::InstanceSpec::from(val); + let components = + components.into_iter().map(|(k, v)| (k, v.into())).collect(); InstanceSpec { board, components, smbios } } } @@ -65,6 +67,8 @@ impl TryFrom for Spec { fn try_from(value: InstanceSpec) -> Result { let InstanceSpec { board, components, smbios } = value; + let components = + components.into_iter().map(|(k, v)| (k, v.into())).collect(); let v1 = v1::instance_spec::InstanceSpec { board, components }; let mut spec: Spec = v1.try_into()?; spec.smbios_type1_input = smbios; @@ -207,7 +211,7 @@ impl From for v1::instance_spec::Component { fn from(value: StorageDevice) -> Self { match value { StorageDevice::Virtio(d) => Self::VirtioDisk(d), - StorageDevice::Nvme(d) => Self::NvmeDisk(d), + StorageDevice::Nvme(d) => Self::NvmeDisk(d.into()), } } } @@ -220,7 +224,9 @@ impl TryFrom for StorageDevice { ) -> Result { match value { v1::instance_spec::Component::VirtioDisk(d) => Ok(Self::Virtio(d)), - v1::instance_spec::Component::NvmeDisk(d) => Ok(Self::Nvme(d)), + v1::instance_spec::Component::NvmeDisk(d) => { + Ok(Self::Nvme(d.into())) + } _ => Err(ComponentTypeMismatch), } } diff --git a/crates/propolis-api-types-versions/Cargo.toml b/crates/propolis-api-types-versions/Cargo.toml index 8fefbe843..9b24784a9 100644 --- a/crates/propolis-api-types-versions/Cargo.toml +++ b/crates/propolis-api-types-versions/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" doctest = false [dependencies] +byte-wrapper.workspace = true crucible-client-types.workspace = true propolis_types.workspace = true schemars.workspace = true diff --git a/crates/propolis-api-types-versions/src/latest.rs b/crates/propolis-api-types-versions/src/latest.rs index 802663468..999793b87 100644 --- a/crates/propolis-api-types-versions/src/latest.rs +++ b/crates/propolis-api-types-versions/src/latest.rs @@ -30,7 +30,6 @@ pub mod components { pub use crate::v1::components::devices::BootOrderEntry; pub use crate::v1::components::devices::BootSettings; pub use crate::v1::components::devices::MigrationFailureInjector; - pub use crate::v1::components::devices::NvmeDisk; pub use crate::v1::components::devices::P9fs; pub use crate::v1::components::devices::PciPciBridge; pub use crate::v1::components::devices::QemuPvpanic; @@ -41,6 +40,8 @@ pub mod components { pub use crate::v1::components::devices::SoftNpuPort; pub use crate::v1::components::devices::VirtioDisk; pub use crate::v1::components::devices::VirtioNic; + + pub use crate::v3::components::devices::NvmeDisk; } } @@ -68,12 +69,11 @@ pub mod instance { pub use crate::v1::instance::InstanceStateRequested; pub use crate::v1::instance::ReplacementComponent; - pub use crate::v2::api::InstanceEnsureRequest; - pub use crate::v2::api::InstanceInitializationMethod; + pub use crate::v3::api::InstanceEnsureRequest; + pub use crate::v3::api::InstanceInitializationMethod; } pub mod instance_spec { - pub use crate::v1::instance_spec::Component; pub use crate::v1::instance_spec::CpuidIdent; pub use crate::v1::instance_spec::CpuidValues; pub use crate::v1::instance_spec::CpuidVendor; @@ -81,10 +81,12 @@ pub mod instance_spec { pub use crate::v1::instance_spec::SpecKey; pub use crate::v1::instance_spec::VersionedInstanceSpec; - pub use crate::v2::instance_spec::InstanceSpec; - pub use crate::v2::instance_spec::InstanceSpecGetResponse; - pub use crate::v2::instance_spec::InstanceSpecStatus; pub use crate::v2::instance_spec::SmbiosType1Input; + + pub use crate::v3::instance_spec::Component; + pub use crate::v3::instance_spec::InstanceSpec; + pub use crate::v3::instance_spec::InstanceSpecGetResponse; + pub use crate::v3::instance_spec::InstanceSpecStatus; } pub mod migration { diff --git a/crates/propolis-api-types-versions/src/lib.rs b/crates/propolis-api-types-versions/src/lib.rs index d745931f0..029009311 100644 --- a/crates/propolis-api-types-versions/src/lib.rs +++ b/crates/propolis-api-types-versions/src/lib.rs @@ -35,3 +35,5 @@ pub mod latest; pub mod v1; #[path = "programmable_smbios/mod.rs"] pub mod v2; +#[path = "nvme_model_number/mod.rs"] +pub mod v3; diff --git a/crates/propolis-api-types-versions/src/nvme_model_number/api.rs b/crates/propolis-api-types-versions/src/nvme_model_number/api.rs new file mode 100644 index 000000000..1617a8cb3 --- /dev/null +++ b/crates/propolis-api-types-versions/src/nvme_model_number/api.rs @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! API request and response types for the NVME_MODEL_NUMBER API version. + +use std::{collections::BTreeMap, net::SocketAddr}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::instance_spec::InstanceSpec; +use crate::v1::instance::{InstanceProperties, ReplacementComponent}; +use crate::v1::instance_spec::SpecKey; +use crate::v2; + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "method", content = "value")] +pub enum InstanceInitializationMethod { + Spec { + spec: InstanceSpec, + }, + MigrationTarget { + migration_id: Uuid, + src_addr: SocketAddr, + replace_components: BTreeMap, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceEnsureRequest { + pub properties: InstanceProperties, + pub init: InstanceInitializationMethod, +} + +impl From + for InstanceInitializationMethod +{ + fn from(old: v2::api::InstanceInitializationMethod) -> Self { + match old { + v2::api::InstanceInitializationMethod::Spec { spec } => { + Self::Spec { spec: spec.into() } + } + v2::api::InstanceInitializationMethod::MigrationTarget { + migration_id, + src_addr, + replace_components, + } => Self::MigrationTarget { + migration_id, + src_addr, + replace_components, + }, + } + } +} + +impl From for InstanceEnsureRequest { + fn from(old: v2::api::InstanceEnsureRequest) -> Self { + Self { properties: old.properties, init: old.init.into() } + } +} diff --git a/crates/propolis-api-types-versions/src/nvme_model_number/components/devices.rs b/crates/propolis-api-types-versions/src/nvme_model_number/components/devices.rs new file mode 100644 index 000000000..ed1f018c1 --- /dev/null +++ b/crates/propolis-api-types-versions/src/nvme_model_number/components/devices.rs @@ -0,0 +1,53 @@ +// 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/. + +//! Device configuration data for the NVME_MODEL_NUMBER API version. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v1; +use crate::v1::instance_spec::{PciPath, SpecKey}; + +/// A disk that presents an NVMe interface to the guest. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct NvmeDisk { + /// The name of the disk's backend component. + pub backend_id: SpecKey, + + /// The PCI bus/device/function at which this disk should be attached. + pub pci_path: PciPath, + + /// The serial number to return in response to an NVMe Identify Controller + /// command. + #[serde(with = "byte_wrapper::HexArray::<20>")] + pub serial_number: [u8; 20], + + /// The model number to return in response to an NVMe Identify Controller + /// command. + #[serde(with = "byte_wrapper::HexArray::<40>")] + pub model_number: [u8; 40], +} + +impl From for NvmeDisk { + fn from(old: v1::components::devices::NvmeDisk) -> Self { + Self { + backend_id: old.backend_id, + pci_path: old.pci_path, + serial_number: old.serial_number, + model_number: [0u8; 40], + } + } +} + +impl From for v1::components::devices::NvmeDisk { + fn from(new: NvmeDisk) -> Self { + Self { + backend_id: new.backend_id, + pci_path: new.pci_path, + serial_number: new.serial_number, + } + } +} diff --git a/crates/propolis-api-types-versions/src/nvme_model_number/components/mod.rs b/crates/propolis-api-types-versions/src/nvme_model_number/components/mod.rs new file mode 100644 index 000000000..c9c65c918 --- /dev/null +++ b/crates/propolis-api-types-versions/src/nvme_model_number/components/mod.rs @@ -0,0 +1,7 @@ +// 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/. + +//! Component types for the NVME_MODEL_NUMBER API version. + +pub mod devices; diff --git a/crates/propolis-api-types-versions/src/nvme_model_number/instance_spec.rs b/crates/propolis-api-types-versions/src/nvme_model_number/instance_spec.rs new file mode 100644 index 000000000..6b1afae2c --- /dev/null +++ b/crates/propolis-api-types-versions/src/nvme_model_number/instance_spec.rs @@ -0,0 +1,224 @@ +// 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/. + +//! Instance specification types for the NVME_MODEL_NUMBER API version. + +use std::collections::BTreeMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use super::components::devices; +use crate::v1; +use crate::v1::components::{backends, board}; +use crate::v1::instance::{InstanceProperties, InstanceState}; +use crate::v1::instance_spec::SpecKey; +use crate::v2; +use crate::v2::instance_spec::SmbiosType1Input; + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde( + deny_unknown_fields, + tag = "type", + content = "component", + rename_all = "snake_case" +)] +pub enum Component { + VirtioDisk(v1::components::devices::VirtioDisk), + NvmeDisk(devices::NvmeDisk), + VirtioNic(v1::components::devices::VirtioNic), + SerialPort(v1::components::devices::SerialPort), + PciPciBridge(v1::components::devices::PciPciBridge), + QemuPvpanic(v1::components::devices::QemuPvpanic), + BootSettings(v1::components::devices::BootSettings), + SoftNpuPciPort(v1::components::devices::SoftNpuPciPort), + SoftNpuPort(v1::components::devices::SoftNpuPort), + SoftNpuP9(v1::components::devices::SoftNpuP9), + P9fs(v1::components::devices::P9fs), + MigrationFailureInjector(v1::components::devices::MigrationFailureInjector), + CrucibleStorageBackend(backends::CrucibleStorageBackend), + FileStorageBackend(backends::FileStorageBackend), + BlobStorageBackend(backends::BlobStorageBackend), + VirtioNetworkBackend(backends::VirtioNetworkBackend), + DlpiNetworkBackend(backends::DlpiNetworkBackend), +} + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +pub struct InstanceSpec { + pub board: board::Board, + pub components: BTreeMap, + pub smbios: Option, +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "value")] +pub enum InstanceSpecStatus { + WaitingForMigrationSource, + Present(InstanceSpec), +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceSpecGetResponse { + pub properties: InstanceProperties, + pub state: InstanceState, + pub spec: InstanceSpecStatus, +} + +// Conversions from v1 Component to v3 Component. +impl From for Component { + fn from(old: v1::instance_spec::Component) -> Self { + match old { + v1::instance_spec::Component::VirtioDisk(d) => Self::VirtioDisk(d), + v1::instance_spec::Component::NvmeDisk(d) => { + Self::NvmeDisk(d.into()) + } + v1::instance_spec::Component::VirtioNic(n) => Self::VirtioNic(n), + v1::instance_spec::Component::SerialPort(s) => Self::SerialPort(s), + v1::instance_spec::Component::PciPciBridge(b) => { + Self::PciPciBridge(b) + } + v1::instance_spec::Component::QemuPvpanic(p) => { + Self::QemuPvpanic(p) + } + v1::instance_spec::Component::BootSettings(b) => { + Self::BootSettings(b) + } + v1::instance_spec::Component::SoftNpuPciPort(p) => { + Self::SoftNpuPciPort(p) + } + v1::instance_spec::Component::SoftNpuPort(p) => { + Self::SoftNpuPort(p) + } + v1::instance_spec::Component::SoftNpuP9(p) => Self::SoftNpuP9(p), + v1::instance_spec::Component::P9fs(p) => Self::P9fs(p), + v1::instance_spec::Component::MigrationFailureInjector(m) => { + Self::MigrationFailureInjector(m) + } + v1::instance_spec::Component::CrucibleStorageBackend(c) => { + Self::CrucibleStorageBackend(c) + } + v1::instance_spec::Component::FileStorageBackend(f) => { + Self::FileStorageBackend(f) + } + v1::instance_spec::Component::BlobStorageBackend(b) => { + Self::BlobStorageBackend(b) + } + v1::instance_spec::Component::VirtioNetworkBackend(v) => { + Self::VirtioNetworkBackend(v) + } + v1::instance_spec::Component::DlpiNetworkBackend(d) => { + Self::DlpiNetworkBackend(d) + } + } + } +} + +// Conversions from v3 Component to v1 Component. +impl From for v1::instance_spec::Component { + fn from(new: Component) -> Self { + match new { + Component::VirtioDisk(d) => Self::VirtioDisk(d), + Component::NvmeDisk(d) => Self::NvmeDisk(d.into()), + Component::VirtioNic(n) => Self::VirtioNic(n), + Component::SerialPort(s) => Self::SerialPort(s), + Component::PciPciBridge(b) => Self::PciPciBridge(b), + Component::QemuPvpanic(p) => Self::QemuPvpanic(p), + Component::BootSettings(b) => Self::BootSettings(b), + Component::SoftNpuPciPort(p) => Self::SoftNpuPciPort(p), + Component::SoftNpuPort(p) => Self::SoftNpuPort(p), + Component::SoftNpuP9(p) => Self::SoftNpuP9(p), + Component::P9fs(p) => Self::P9fs(p), + Component::MigrationFailureInjector(m) => { + Self::MigrationFailureInjector(m) + } + Component::CrucibleStorageBackend(c) => { + Self::CrucibleStorageBackend(c) + } + Component::FileStorageBackend(f) => Self::FileStorageBackend(f), + Component::BlobStorageBackend(b) => Self::BlobStorageBackend(b), + Component::VirtioNetworkBackend(v) => Self::VirtioNetworkBackend(v), + Component::DlpiNetworkBackend(d) => Self::DlpiNetworkBackend(d), + } + } +} + +// Conversions from v2 InstanceSpec to v3 InstanceSpec. +impl From for InstanceSpec { + fn from(old: v2::instance_spec::InstanceSpec) -> Self { + Self { + board: old.board, + components: old + .components + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + smbios: old.smbios, + } + } +} + +// Conversions from v3 InstanceSpec to v2 InstanceSpec. +impl From for v2::instance_spec::InstanceSpec { + fn from(new: InstanceSpec) -> Self { + Self { + board: new.board, + components: new + .components + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + smbios: new.smbios, + } + } +} + +// Conversions for InstanceSpecStatus. +impl From for v2::instance_spec::InstanceSpecStatus { + fn from(new: InstanceSpecStatus) -> Self { + match new { + InstanceSpecStatus::WaitingForMigrationSource => { + Self::WaitingForMigrationSource + } + InstanceSpecStatus::Present(spec) => Self::Present(spec.into()), + } + } +} + +impl From for InstanceSpecStatus { + fn from(old: v2::instance_spec::InstanceSpecStatus) -> Self { + match old { + v2::instance_spec::InstanceSpecStatus::WaitingForMigrationSource => { + Self::WaitingForMigrationSource + } + v2::instance_spec::InstanceSpecStatus::Present(spec) => { + Self::Present(spec.into()) + } + } + } +} + +// Conversions for InstanceSpecGetResponse. +impl From + for v2::instance_spec::InstanceSpecGetResponse +{ + fn from(new: InstanceSpecGetResponse) -> Self { + Self { + properties: new.properties, + state: new.state, + spec: new.spec.into(), + } + } +} + +impl From + for InstanceSpecGetResponse +{ + fn from(old: v2::instance_spec::InstanceSpecGetResponse) -> Self { + Self { + properties: old.properties, + state: old.state, + spec: old.spec.into(), + } + } +} diff --git a/crates/propolis-api-types-versions/src/nvme_model_number/mod.rs b/crates/propolis-api-types-versions/src/nvme_model_number/mod.rs new file mode 100644 index 000000000..f6fba6149 --- /dev/null +++ b/crates/propolis-api-types-versions/src/nvme_model_number/mod.rs @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Version `NVME_MODEL_NUMBER` of the Propolis Server API. +//! +//! This version adds a `model_number` field to `NvmeDisk` and updates +//! `serial_number` to use the `byte_wrapper` hex serialization. + +pub mod api; +pub mod components; +pub mod instance_spec; diff --git a/crates/propolis-config-toml/src/spec.rs b/crates/propolis-config-toml/src/spec.rs index ab86fb719..943d900ea 100644 --- a/crates/propolis-config-toml/src/spec.rs +++ b/crates/propolis-config-toml/src/spec.rs @@ -324,11 +324,16 @@ fn parse_storage_device_from_config( Interface::Virtio => { ComponentV0::VirtioDisk(VirtioDisk { backend_id, pci_path }) } - Interface::Nvme => ComponentV0::NvmeDisk(NvmeDisk { - backend_id, - pci_path, - serial_number: nvme_serial_from_str(name, b' '), - }), + Interface::Nvme => { + let nvme = NvmeDisk { + backend_id, + pci_path, + serial_number: nvme_serial_from_str(name, b' '), + // TODO: populate model_number from config + model_number: [0u8; 40], + }; + ComponentV0::NvmeDisk(nvme.into()) + } }, id_to_return, )) diff --git a/crates/propolis-server-api/src/lib.rs b/crates/propolis-server-api/src/lib.rs index 5d20b55bf..c9c26affa 100644 --- a/crates/propolis-server-api/src/lib.rs +++ b/crates/propolis-server-api/src/lib.rs @@ -8,7 +8,7 @@ use dropshot::{ WebsocketChannelResult, WebsocketConnection, }; use dropshot_api_manager_types::api_versions; -use propolis_api_types_versions::{latest, v1}; +use propolis_api_types_versions::{latest, v1, v2}; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -22,6 +22,7 @@ api_versions!([ // | example for the next person. // v // (next_int, IDENT), + (3, NVME_MODEL_NUMBER), (2, PROGRAMMABLE_SMBIOS), (1, INITIAL), ]); @@ -45,7 +46,7 @@ pub trait PropolisServerApi { #[endpoint { method = PUT, path = "/instance", - versions = VERSION_PROGRAMMABLE_SMBIOS.. + versions = VERSION_NVME_MODEL_NUMBER.. }] async fn instance_ensure( rqctx: RequestContext, @@ -55,6 +56,26 @@ pub trait PropolisServerApi { HttpError, >; + #[endpoint { + operation_id = "instance_ensure", + method = PUT, + path = "/instance", + versions = VERSION_PROGRAMMABLE_SMBIOS..VERSION_NVME_MODEL_NUMBER + }] + async fn instance_ensure_v2( + rqctx: RequestContext, + request: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + Self::instance_ensure( + rqctx, + request.map(latest::instance::InstanceEnsureRequest::from), + ) + .await + } + #[endpoint { operation_id = "instance_ensure", method = PUT, @@ -68,9 +89,9 @@ pub trait PropolisServerApi { HttpResponseCreated, HttpError, > { - Self::instance_ensure( + Self::instance_ensure_v2( rqctx, - request.map(latest::instance::InstanceEnsureRequest::from), + request.map(v2::api::InstanceEnsureRequest::from), ) .await } @@ -78,7 +99,7 @@ pub trait PropolisServerApi { #[endpoint { method = GET, path = "/instance/spec", - versions = VERSION_PROGRAMMABLE_SMBIOS.. + versions = VERSION_NVME_MODEL_NUMBER.. }] async fn instance_spec_get( rqctx: RequestContext, @@ -87,6 +108,23 @@ pub trait PropolisServerApi { HttpError, >; + #[endpoint { + operation_id = "instance_spec_get", + method = GET, + path = "/instance/spec", + versions = VERSION_PROGRAMMABLE_SMBIOS..VERSION_NVME_MODEL_NUMBER + }] + async fn instance_spec_get_v2( + rqctx: RequestContext, + ) -> Result< + HttpResponseOk, + HttpError, + > { + Ok(Self::instance_spec_get(rqctx) + .await? + .map(v2::instance_spec::InstanceSpecGetResponse::from)) + } + #[endpoint { operation_id = "instance_spec_get", method = GET, @@ -99,7 +137,7 @@ pub trait PropolisServerApi { HttpResponseOk, HttpError, > { - Ok(Self::instance_spec_get(rqctx) + Ok(Self::instance_spec_get_v2(rqctx) .await? .map(v1::instance_spec::InstanceSpecGetResponse::from)) } diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index f64085bc2..1afe08f2c 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -53,6 +53,7 @@ progenitor::generate_api!( InstanceProperties = propolis_api_types_versions::latest::instance::InstanceProperties, InstanceMetadata = propolis_api_types_versions::latest::instance::InstanceMetadata, InstanceSpecGetResponse = propolis_api_types_versions::latest::instance_spec::InstanceSpecGetResponse, + NvmeDisk = propolis_api_types_versions::latest::components::devices::NvmeDisk, SmbiosType1Input = propolis_api_types_versions::latest::instance_spec::SmbiosType1Input, VersionedInstanceSpec = propolis_api_types_versions::latest::instance_spec::VersionedInstanceSpec, CpuidEntry = propolis_api_types_versions::latest::components::board::CpuidEntry, diff --git a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub b/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub new file mode 100644 index 000000000..faa9b4d85 --- /dev/null +++ b/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json.gitstub @@ -0,0 +1 @@ +fd3636877061da7e951cb1fbce365f7cbf40933c:openapi/propolis-server/propolis-server-2.0.0-d68a9f.json diff --git a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json b/openapi/propolis-server/propolis-server-3.0.0-2837dd.json similarity index 98% rename from openapi/propolis-server/propolis-server-2.0.0-d68a9f.json rename to openapi/propolis-server/propolis-server-3.0.0-2837dd.json index 6c49482e1..6e97c80f7 100644 --- a/openapi/propolis-server/propolis-server-2.0.0-d68a9f.json +++ b/openapi/propolis-server/propolis-server-3.0.0-2837dd.json @@ -7,7 +7,7 @@ "url": "https://oxide.computer", "email": "api@oxide.computer" }, - "version": "2.0.0" + "version": "3.0.0" }, "paths": { "/instance": { @@ -579,7 +579,7 @@ } ] }, - "ComponentV0": { + "Component": { "oneOf": [ { "type": "object", @@ -1434,7 +1434,7 @@ "components": { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/ComponentV0" + "$ref": "#/components/schemas/Component" } }, "smbios": { @@ -1625,6 +1625,18 @@ } ] }, + "model_number": { + "description": "The model number to return in response to an NVMe Identify Controller command.", + "x-rust-type": { + "crate": "byte-wrapper", + "path": "byte_wrapper::HexArray::<40>", + "version": "0.1.0" + }, + "type": "string", + "pattern": "^[0-9a-fA-F]{80}$", + "minLength": 80, + "maxLength": 80 + }, "pci_path": { "description": "The PCI bus/device/function at which this disk should be attached.", "allOf": [ @@ -1635,18 +1647,20 @@ }, "serial_number": { "description": "The serial number to return in response to an NVMe Identify Controller command.", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0 + "x-rust-type": { + "crate": "byte-wrapper", + "path": "byte_wrapper::HexArray::<20>", + "version": "0.1.0" }, - "minItems": 20, - "maxItems": 20 + "type": "string", + "pattern": "^[0-9a-fA-F]{40}$", + "minLength": 40, + "maxLength": 40 } }, "required": [ "backend_id", + "model_number", "pci_path", "serial_number" ], diff --git a/openapi/propolis-server/propolis-server-latest.json b/openapi/propolis-server/propolis-server-latest.json index a28007449..8177cacab 120000 --- a/openapi/propolis-server/propolis-server-latest.json +++ b/openapi/propolis-server/propolis-server-latest.json @@ -1 +1 @@ -propolis-server-2.0.0-d68a9f.json \ No newline at end of file +propolis-server-3.0.0-2837dd.json \ No newline at end of file diff --git a/phd-tests/framework/src/test_vm/config.rs b/phd-tests/framework/src/test_vm/config.rs index 6e17c84e3..c816df0e2 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -321,36 +321,42 @@ impl<'dr> VmConfig<'dr> { ), pci_path, }), - DiskInterface::Nvme => ComponentV0::NvmeDisk(NvmeDisk { - backend_id: SpecKey::Name( - backend_name.clone().into_string(), - ), - pci_path, - serial_number: nvme_serial_from_str( - device_name.as_str(), - // Omicron supplies (or will supply, as of this writing) - // 0 as the padding byte to maintain compatibility for - // existing disks. Match that behavior here so that PHD - // and Omicron VM configurations are as similar as - // possible. - 0, - ), - }), + DiskInterface::Nvme => { + let nvme = NvmeDisk { + backend_id: SpecKey::Name( + backend_name.clone().into_string(), + ), + pci_path, + serial_number: nvme_serial_from_str( + device_name.as_str(), + // Omicron supplies (or will supply, as of this writing) + // 0 as the padding byte to maintain compatibility for + // existing disks. Match that behavior here so that PHD + // and Omicron VM configurations are as similar as + // possible. + 0, + ), + // TODO: populate model_number + model_number: [0u8; 40], + }; + ComponentV0::NvmeDisk(nvme.into()) + } }; let _old = spec .components - .insert(device_name.into_string().into(), device_spec); + .insert(device_name.into_string().into(), device_spec.into()); assert!(_old.is_none()); let _old = spec .components - .insert(backend_name.into_string().into(), backend_spec); + .insert(backend_name.into_string().into(), backend_spec.into()); assert!(_old.is_none()); } let _old = spec.components.insert( "com1".into(), - ComponentV0::SerialPort(SerialPort { num: SerialPortNumber::Com1 }), + ComponentV0::SerialPort(SerialPort { num: SerialPortNumber::Com1 }) + .into(), ); assert!(_old.is_none()); @@ -364,7 +370,8 @@ impl<'dr> VmConfig<'dr> { id: SpecKey::Name(item.to_string()), }) .collect(), - }), + }) + .into(), ); assert!(_old.is_none()); } @@ -372,7 +379,7 @@ impl<'dr> VmConfig<'dr> { if let Some(mig) = migration_failure.as_ref() { let _old = spec.components.insert( "migration-failure".into(), - ComponentV0::MigrationFailureInjector(mig.clone()), + ComponentV0::MigrationFailureInjector(mig.clone()).into(), ); assert!(_old.is_none()); } diff --git a/phd-tests/framework/src/test_vm/mod.rs b/phd-tests/framework/src/test_vm/mod.rs index 98686861b..5450810c8 100644 --- a/phd-tests/framework/src/test_vm/mod.rs +++ b/phd-tests/framework/src/test_vm/mod.rs @@ -29,7 +29,7 @@ use core::result::Result as StdResult; use futures::FutureExt; use propolis_client::{ instance_spec::{ - ComponentV0, InstanceProperties, InstanceSpecGetResponse, + Component, InstanceProperties, InstanceSpecGetResponse, ReplacementComponent, }, support::{InstanceSerialConsoleHelper, WSClientOffset}, @@ -707,7 +707,7 @@ impl TestVm { let mut map = ReplacementComponents::new(); for (id, comp) in &self.spec.instance_spec().components { match comp { - ComponentV0::MigrationFailureInjector(inj) => { + Component::MigrationFailureInjector(inj) => { map.insert( id.to_string(), ReplacementComponent::MigrationFailureInjector( @@ -715,7 +715,7 @@ impl TestVm { ), ); } - ComponentV0::CrucibleStorageBackend(be) => { + Component::CrucibleStorageBackend(be) => { map.insert( id.to_string(), ReplacementComponent::CrucibleStorageBackend( diff --git a/phd-tests/framework/src/test_vm/spec.rs b/phd-tests/framework/src/test_vm/spec.rs index b755156f8..4cf0091e0 100644 --- a/phd-tests/framework/src/test_vm/spec.rs +++ b/phd-tests/framework/src/test_vm/spec.rs @@ -10,7 +10,7 @@ use crate::{ }; use camino::Utf8PathBuf; use propolis_client::instance_spec::{ - ComponentV0, InstanceMetadata, InstanceSpec, + Component, InstanceMetadata, InstanceSpec, }; use uuid::Uuid; @@ -90,10 +90,10 @@ impl VmSpec { .into_backend_name() .into_string() .into(); - if let Some(ComponentV0::CrucibleStorageBackend(_)) = + if let Some(Component::CrucibleStorageBackend(_)) = spec.components.get(&backend_name) { - spec.components.insert(backend_name, backend_spec); + spec.components.insert(backend_name, backend_spec.into()); } } }