diff --git a/Cargo.lock b/Cargo.lock index 479568431..c466d5f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2402,9 +2402,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" @@ -5389,6 +5386,7 @@ dependencies = [ "propolis_types", "schemars", "serde", + "serde_human_bytes", "serde_json", "thiserror 1.0.64", "uuid", @@ -6524,10 +6522,12 @@ 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", + "schemars", + "serde_core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 755507a9a..77b0d25c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -158,6 +158,7 @@ schemars = "0.8.10" semver = "1.0" serde = "1.0" serde_arrays = "0.1" +serde_human_bytes = { git = "https://github.com/oxidecomputer/serde_human_bytes", branch = "main", features = ["schemars08"] } serde_derive = "1.0" serde_json = "1.0" serde_test = "1.0.138" 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 86f7dcd4e..83c64f726 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 8ea49325d..f88964b6a 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 v0 = v1::instance_spec::InstanceSpec { board, components }; let mut spec: Spec = v0.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..f95202ada 100644 --- a/crates/propolis-api-types-versions/Cargo.toml +++ b/crates/propolis-api-types-versions/Cargo.toml @@ -12,6 +12,7 @@ crucible-client-types.workspace = true propolis_types.workspace = true schemars.workspace = true serde.workspace = true +serde_human_bytes.workspace = true thiserror.workspace = true uuid.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..cd96d78b5 --- /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 = "serde_human_bytes::HexArray::<20>")] + pub serial_number: [u8; 20], + + /// The model number to return in response to an NVMe Identify Controller + /// command. + #[serde(with = "serde_human_bytes::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..7380ee283 --- /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 `serde_human_bytes` 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-3.0.0-948573.json b/openapi/propolis-server/propolis-server-3.0.0-948573.json new file mode 100644 index 000000000..67feb3189 --- /dev/null +++ b/openapi/propolis-server/propolis-server-3.0.0-948573.json @@ -0,0 +1,2064 @@ +{ + "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": "3.0.0" + }, + "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" + } + }, + { + "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" + } + } + ], + "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" + } + } + ], + "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/migrate/{migration_id}/start": { + "get": { + "summary": "DO NOT USE THIS IF YOU'RE NOT PROPOLIS-SERVER.", + "description": "Internal API called during a migration from a destination instance to the source instance as part of the HTTP connection upgrade used to establish the migration link. This API is exported via OpenAPI purely to verify that its shape hasn't changed.", + "operationId": "instance_migrate_start", + "parameters": [ + { + "in": "path", + "name": "migration_id", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "default": { + "description": "", + "content": { + "*/*": { + "schema": {} + } + } + } + }, + "x-dropshot-websocket": {} + } + }, + "/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" + } + } + } + }, + "/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": { + "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" + } + ] + }, + "cpuid": { + "nullable": true, + "description": "The CPUID values to expose to the guest. If `None`, bhyve will derive default values from the host's CPUID values.", + "allOf": [ + { + "$ref": "#/components/schemas/Cpuid" + } + ] + }, + "cpus": { + "description": "The number of virtual logical processors attached to this VM.", + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "guest_hv_interface": { + "description": "The hypervisor platform to expose to the guest. The default is a bhyve-compatible interface with no additional features.\n\nFor compatibility with older versions of Propolis, this field is only serialized if it specifies a non-default interface.", + "allOf": [ + { + "$ref": "#/components/schemas/GuestHypervisorInterface" + } + ] + }, + "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 the boot order stored in a [`BootSettings`] component.", + "type": "object", + "properties": { + "id": { + "description": "The ID of another component in the spec that Propolis should try to boot from.\n\nCurrently, only disk device components are supported.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + } + }, + "required": [ + "id" + ] + }, + "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" + } + } + }, + "required": [ + "order" + ], + "additionalProperties": false + }, + "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 + } + ] + }, + "Component": { + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioDisk" + }, + "type": { + "type": "string", + "enum": [ + "virtio_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/NvmeDisk" + }, + "type": { + "type": "string", + "enum": [ + "nvme_disk" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNic" + }, + "type": { + "type": "string", + "enum": [ + "virtio_nic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SerialPort" + }, + "type": { + "type": "string", + "enum": [ + "serial_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/PciPciBridge" + }, + "type": { + "type": "string", + "enum": [ + "pci_pci_bridge" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/QemuPvpanic" + }, + "type": { + "type": "string", + "enum": [ + "qemu_pvpanic" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BootSettings" + }, + "type": { + "type": "string", + "enum": [ + "boot_settings" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPciPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_pci_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuPort" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_port" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/SoftNpuP9" + }, + "type": { + "type": "string", + "enum": [ + "soft_npu_p9" + ] + } + }, + "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/MigrationFailureInjector" + }, + "type": { + "type": "string", + "enum": [ + "migration_failure_injector" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "crucible_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/FileStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "file_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/BlobStorageBackend" + }, + "type": { + "type": "string", + "enum": [ + "blob_storage_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "virtio_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "$ref": "#/components/schemas/DlpiNetworkBackend" + }, + "type": { + "type": "string", + "enum": [ + "dlpi_network_backend" + ] + } + }, + "required": [ + "component", + "type" + ], + "additionalProperties": false + } + ] + }, + "Cpuid": { + "description": "A set of CPUID values to expose to a guest.", + "type": "object", + "properties": { + "entries": { + "description": "A list of CPUID leaves/subleaves and their associated values.\n\nPropolis servers require that each entry's `leaf` be unique and that it falls in either the \"standard\" (0 to 0xFFFF) or \"extended\" (0x8000_0000 to 0x8000_FFFF) function ranges, since these are the only valid input ranges currently defined by Intel and AMD. See the Intel 64 and IA-32 Architectures Software Developer's Manual (June 2024) Table 3-17 and the AMD64 Architecture Programmer's Manual (March 2024) Volume 3's documentation of the CPUID instruction.", + "type": "array", + "items": { + "$ref": "#/components/schemas/CpuidEntry" + } + }, + "vendor": { + "description": "The CPU vendor to emulate.\n\nCPUID leaves in the extended range (0x8000_0000 to 0x8000_FFFF) have vendor-defined semantics. Propolis uses this value to determine these semantics when deciding whether it needs to specialize the supplied template values for these leaves.", + "allOf": [ + { + "$ref": "#/components/schemas/CpuidVendor" + } + ] + } + }, + "required": [ + "entries", + "vendor" + ], + "additionalProperties": false + }, + "CpuidEntry": { + "description": "A full description of a CPUID leaf/subleaf and the values it produces.", + "type": "object", + "properties": { + "eax": { + "description": "The value to return in eax.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ebx": { + "description": "The value to return in ebx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "ecx": { + "description": "The value to return in ecx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "edx": { + "description": "The value to return in edx.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "leaf": { + "description": "The leaf (function) number for this entry.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "subleaf": { + "nullable": true, + "description": "The subleaf (index) number for this entry, if it uses subleaves.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "eax", + "ebx", + "ecx", + "edx", + "leaf" + ], + "additionalProperties": false + }, + "CpuidVendor": { + "description": "A CPU vendor to use when interpreting the meanings of CPUID leaves in the extended ID range (0x80000000 to 0x8000FFFF).", + "type": "string", + "enum": [ + "amd", + "intel" + ] + }, + "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 + }, + "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": { + "block_size": { + "description": "Block size of the backend", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "path": { + "description": "A path to a file that backs a disk.", + "type": "string" + }, + "readonly": { + "description": "Indicates whether the storage is read-only.", + "type": "boolean" + }, + "workers": { + "nullable": true, + "description": "Optional worker threads for the file backend, exposed for testing only.", + "type": "integer", + "format": "uint", + "minimum": 1 + } + }, + "required": [ + "block_size", + "path", + "readonly" + ], + "additionalProperties": false + }, + "GuestHypervisorInterface": { + "description": "A hypervisor interface to expose to the guest.", + "oneOf": [ + { + "description": "Expose a bhyve-like interface (\"bhyve bhyve \" as the hypervisor ID in leaf 0x4000_0000 and no additional leaves or features).", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "bhyve" + ] + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + { + "description": "Expose a Hyper-V-compatible hypervisor interface with the supplied features enabled.", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "hyper_v" + ] + }, + "value": { + "type": "object", + "properties": { + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HyperVFeatureFlag" + }, + "uniqueItems": true + } + }, + "required": [ + "features" + ], + "additionalProperties": false + } + }, + "required": [ + "type", + "value" + ], + "additionalProperties": false + } + ] + }, + "HyperVFeatureFlag": { + "description": "Flags that enable \"simple\" Hyper-V enlightenments that require no feature-specific configuration.", + "type": "string", + "enum": [ + "reference_tsc" + ] + }, + "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": { + "properties": { + "$ref": "#/components/schemas/InstanceProperties" + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "properties", + "state" + ] + }, + "InstanceEnsureRequest": { + "type": "object", + "properties": { + "init": { + "$ref": "#/components/schemas/InstanceInitializationMethod" + }, + "properties": { + "$ref": "#/components/schemas/InstanceProperties" + } + }, + "required": [ + "init", + "properties" + ] + }, + "InstanceEnsureResponse": { + "type": "object", + "properties": { + "migrate": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/InstanceMigrateInitiateResponse" + } + ] + } + } + }, + "InstanceGetResponse": { + "type": "object", + "properties": { + "instance": { + "$ref": "#/components/schemas/Instance" + } + }, + "required": [ + "instance" + ] + }, + "InstanceInitializationMethod": { + "oneOf": [ + { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "Spec" + ] + }, + "value": { + "type": "object", + "properties": { + "spec": { + "$ref": "#/components/schemas/InstanceSpec" + } + }, + "required": [ + "spec" + ] + } + }, + "required": [ + "method", + "value" + ] + }, + { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "MigrationTarget" + ] + }, + "value": { + "type": "object", + "properties": { + "migration_id": { + "type": "string", + "format": "uuid" + }, + "replace_components": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ReplacementComponent" + } + }, + "src_addr": { + "type": "string" + } + }, + "required": [ + "migration_id", + "replace_components", + "src_addr" + ] + } + }, + "required": [ + "method", + "value" + ] + } + ] + }, + "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" + ] + }, + "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": { + "description": { + "description": "Free-form text description of an Instance.", + "type": "string" + }, + "id": { + "description": "Unique identifier for this Instance.", + "type": "string", + "format": "uuid" + }, + "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" + } + }, + "required": [ + "description", + "id", + "metadata", + "name" + ] + }, + "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" + ] + }, + "InstanceSpec": { + "type": "object", + "properties": { + "board": { + "$ref": "#/components/schemas/Board" + }, + "components": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Component" + } + }, + "smbios": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/SmbiosType1Input" + } + ] + } + }, + "required": [ + "board", + "components" + ] + }, + "InstanceSpecGetResponse": { + "type": "object", + "properties": { + "properties": { + "$ref": "#/components/schemas/InstanceProperties" + }, + "spec": { + "$ref": "#/components/schemas/InstanceSpecStatus" + }, + "state": { + "$ref": "#/components/schemas/InstanceState" + } + }, + "required": [ + "properties", + "spec", + "state" + ] + }, + "InstanceSpecStatus": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "WaitingForMigrationSource" + ] + } + }, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "Present" + ] + }, + "value": { + "$ref": "#/components/schemas/InstanceSpec" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, + "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": { + "vcr_json": { + "type": "string" + } + }, + "required": [ + "vcr_json" + ] + }, + "MigrationFailureInjector": { + "description": "Describes a synthetic device that registers for VM lifecycle notifications and returns errors during attempts to migrate.\n\nThis is only supported by Propolis servers compiled with the `failure-injection` feature.", + "type": "object", + "properties": { + "fail_exports": { + "description": "The number of times this device should fail requests to export state.", + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "fail_imports": { + "description": "The number of times this device should fail requests to import state.", + "type": "integer", + "format": "uint32", + "minimum": 0 + } + }, + "required": [ + "fail_exports", + "fail_imports" + ], + "additionalProperties": false + }, + "MigrationState": { + "type": "string", + "enum": [ + "Sync", + "RamPush", + "Pause", + "RamPushDirty", + "Device", + "Resume", + "RamPull", + "Server", + "Finish", + "Error" + ] + }, + "NvmeDisk": { + "description": "A disk that presents an NVMe interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "model_number": { + "description": "The model number to return in response to an NVMe Identify Controller command.", + "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": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + }, + "serial_number": { + "description": "The serial number to return in response to an NVMe Identify Controller command.", + "type": "string", + "pattern": "^[0-9a-fA-F]{40}$", + "minLength": 40, + "maxLength": 40 + } + }, + "required": [ + "backend_id", + "model_number", + "pci_path", + "serial_number" + ], + "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", + "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" + ] + }, + "ReplacementComponent": { + "description": "An instance spec component that should be replaced during a live migration.", + "oneOf": [ + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "MigrationFailureInjector" + ] + }, + "spec": { + "$ref": "#/components/schemas/MigrationFailureInjector" + } + }, + "required": [ + "component", + "spec" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "CrucibleStorageBackend" + ] + }, + "spec": { + "$ref": "#/components/schemas/CrucibleStorageBackend" + } + }, + "required": [ + "component", + "spec" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "component": { + "type": "string", + "enum": [ + "VirtioNetworkBackend" + ] + }, + "spec": { + "$ref": "#/components/schemas/VirtioNetworkBackend" + } + }, + "required": [ + "component", + "spec" + ], + "additionalProperties": false + } + ] + }, + "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" + ] + }, + "SmbiosType1Input": { + "type": "object", + "properties": { + "manufacturer": { + "type": "string" + }, + "product_name": { + "type": "string" + }, + "serial_number": { + "type": "string" + }, + "version": { + "type": "integer", + "format": "uint64", + "minimum": 0 + } + }, + "required": [ + "manufacturer", + "product_name", + "serial_number", + "version" + ], + "additionalProperties": false + }, + "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": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPciPort": { + "description": "Describes a SoftNPU PCI device.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "pci_path": { + "description": "The PCI path at which to attach the guest to this port.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "pci_path" + ], + "additionalProperties": false + }, + "SoftNpuPort": { + "description": "Describes a port in a SoftNPU emulated ASIC.\n\nThis is only supported by Propolis servers compiled with the `falcon` feature.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the port's associated DLPI backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "link_name": { + "description": "The data link name for this port.", + "type": "string" + } + }, + "required": [ + "backend_id", + "link_name" + ], + "additionalProperties": false + }, + "SpecKey": { + "description": "A key identifying a component in an instance spec.", + "oneOf": [ + { + "title": "uuid", + "allOf": [ + { + "type": "string", + "format": "uuid" + } + ] + }, + { + "title": "name", + "allOf": [ + { + "type": "string" + } + ] + } + ] + }, + "VirtioDisk": { + "description": "A disk that presents a virtio-block interface to the guest.", + "type": "object", + "properties": { + "backend_id": { + "description": "The name of the disk's backend component.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "pci_path": { + "description": "The PCI bus/device/function at which this disk should be attached.", + "allOf": [ + { + "$ref": "#/components/schemas/PciPath" + } + ] + } + }, + "required": [ + "backend_id", + "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_id": { + "description": "The name of the device's backend.", + "allOf": [ + { + "$ref": "#/components/schemas/SpecKey" + } + ] + }, + "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_id", + "interface_id", + "pci_path" + ], + "additionalProperties": false + }, + "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/propolis-server-latest.json b/openapi/propolis-server/propolis-server-latest.json index a28007449..05a142adf 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-948573.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 3be06080c..4f4336ef5 100644 --- a/phd-tests/framework/src/test_vm/config.rs +++ b/phd-tests/framework/src/test_vm/config.rs @@ -324,36 +324,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()); @@ -367,7 +373,8 @@ impl<'dr> VmConfig<'dr> { id: SpecKey::Name(item.to_string()), }) .collect(), - }), + }) + .into(), ); assert!(_old.is_none()); } @@ -375,7 +382,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 132483077..a866e2f12 100644 --- a/phd-tests/framework/src/test_vm/mod.rs +++ b/phd-tests/framework/src/test_vm/mod.rs @@ -28,7 +28,7 @@ use camino::Utf8PathBuf; use core::result::Result as StdResult; use propolis_client::{ instance_spec::{ - ComponentV0, InstanceProperties, InstanceSpecGetResponse, + Component, InstanceProperties, InstanceSpecGetResponse, ReplacementComponent, }, support::{InstanceSerialConsoleHelper, WSClientOffset}, @@ -602,7 +602,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( @@ -610,7 +610,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()); } } }