From c41b396bb1459dc4159d57b9f6b9a7e4591487c2 Mon Sep 17 00:00:00 2001 From: Rain Date: Mon, 22 Dec 2025 22:08:02 +0000 Subject: [PATCH 1/3] [spr] initial version Created using spr 1.3.6-beta.1 --- Cargo.lock | 24 +- Cargo.toml | 1 + bin/propolis-server/Cargo.toml | 1 + bin/propolis-server/src/lib/initializer.rs | 2 +- .../src/lib/migrate/destination.rs | 2 +- bin/propolis-server/src/lib/migrate/mod.rs | 2 +- .../src/lib/migrate/preamble.rs | 36 +- bin/propolis-server/src/lib/migrate/source.rs | 9 +- .../src/lib/serial/history_buffer.rs | 2 +- bin/propolis-server/src/lib/serial/mod.rs | 2 +- bin/propolis-server/src/lib/server.rs | 187 ++------ .../src/lib/spec/api_spec_v0.rs | 116 ++--- bin/propolis-server/src/lib/spec/mod.rs | 45 +- .../src/lib/stats/virtual_machine.rs | 2 +- bin/propolis-server/src/lib/vm/active.rs | 5 +- bin/propolis-server/src/lib/vm/ensure.rs | 12 +- bin/propolis-server/src/lib/vm/mod.rs | 73 +-- bin/propolis-server/src/lib/vm/services.rs | 2 +- .../src/lib/vm/state_driver.rs | 7 +- .../src/lib/vm/state_publisher.rs | 21 +- crates/propolis-api-types-versions/Cargo.toml | 19 + .../src/impls/instance.rs | 41 ++ .../src/impls/instance_spec.rs | 105 +++++ .../src/impls/mod.rs | 8 + .../src/initial/components/backends.rs | 94 ++++ .../src/initial/components/board.rs | 182 ++++++++ .../src/initial/components/devices.rs | 206 +++++++++ .../src/initial/components/mod.rs | 12 + .../src/initial/disk.rs | 38 ++ .../src/initial/instance.rs | 179 ++++++++ .../src/initial/instance_spec.rs | 192 ++++++++ .../src/initial/migration.rs | 88 ++++ .../src/initial/mod.rs | 15 + .../src/initial/serial.rs | 69 +++ .../propolis-api-types-versions/src/latest.rs | 104 +++++ crates/propolis-api-types-versions/src/lib.rs | 36 ++ .../src/programmable_smbios/api.rs | 62 +++ .../src/programmable_smbios/instance_spec.rs | 82 ++++ .../src/programmable_smbios/mod.rs | 12 + crates/propolis-api-types/Cargo.toml | 9 +- crates/propolis-api-types/src/disk.rs | 7 + crates/propolis-api-types/src/instance.rs | 7 + .../src/instance_spec/components/backends.rs | 91 +--- .../src/instance_spec/components/board.rs | 179 +------- .../src/instance_spec/components/devices.rs | 203 +-------- .../src/instance_spec/components/mod.rs | 13 +- .../src/instance_spec/mod.rs | 380 +--------------- .../src/instance_spec/v0.rs | 58 --- crates/propolis-api-types/src/lib.rs | 425 +----------------- crates/propolis-api-types/src/migration.rs | 7 + crates/propolis-api-types/src/serial.rs | 7 + crates/propolis-api-types/src/v0.rs | 40 -- crates/propolis-server-api/Cargo.toml | 2 +- crates/propolis-server-api/src/lib.rs | 88 ++-- lib/propolis-client/Cargo.toml | 1 + lib/propolis-client/src/lib.rs | 18 +- 56 files changed, 1888 insertions(+), 1742 deletions(-) create mode 100644 crates/propolis-api-types-versions/Cargo.toml create mode 100644 crates/propolis-api-types-versions/src/impls/instance.rs create mode 100644 crates/propolis-api-types-versions/src/impls/instance_spec.rs create mode 100644 crates/propolis-api-types-versions/src/impls/mod.rs create mode 100644 crates/propolis-api-types-versions/src/initial/components/backends.rs create mode 100644 crates/propolis-api-types-versions/src/initial/components/board.rs create mode 100644 crates/propolis-api-types-versions/src/initial/components/devices.rs create mode 100644 crates/propolis-api-types-versions/src/initial/components/mod.rs create mode 100644 crates/propolis-api-types-versions/src/initial/disk.rs create mode 100644 crates/propolis-api-types-versions/src/initial/instance.rs create mode 100644 crates/propolis-api-types-versions/src/initial/instance_spec.rs create mode 100644 crates/propolis-api-types-versions/src/initial/migration.rs create mode 100644 crates/propolis-api-types-versions/src/initial/mod.rs create mode 100644 crates/propolis-api-types-versions/src/initial/serial.rs create mode 100644 crates/propolis-api-types-versions/src/latest.rs create mode 100644 crates/propolis-api-types-versions/src/lib.rs create mode 100644 crates/propolis-api-types-versions/src/programmable_smbios/api.rs create mode 100644 crates/propolis-api-types-versions/src/programmable_smbios/instance_spec.rs create mode 100644 crates/propolis-api-types-versions/src/programmable_smbios/mod.rs create mode 100644 crates/propolis-api-types/src/disk.rs create mode 100644 crates/propolis-api-types/src/instance.rs delete mode 100644 crates/propolis-api-types/src/instance_spec/v0.rs create mode 100644 crates/propolis-api-types/src/migration.rs create mode 100644 crates/propolis-api-types/src/serial.rs delete mode 100644 crates/propolis-api-types/src/v0.rs diff --git a/Cargo.lock b/Cargo.lock index aeaeee354..78c42debc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5393,6 +5393,19 @@ dependencies = [ "zerocopy 0.8.27", ] +[[package]] +name = "propolis-api-types-versions" +version = "0.0.0" +dependencies = [ + "crucible-client-types", + "propolis_types", + "schemars", + "serde", + "serde_json", + "thiserror 1.0.64", + "uuid", +] + [[package]] name = "propolis-cli" version = "0.1.0" @@ -5427,6 +5440,7 @@ dependencies = [ "futures", "progenitor 0.10.0", "progenitor-client 0.10.0", + "propolis-api-types-versions", "propolis_api_types", "rand 0.9.2", "reqwest", @@ -5544,6 +5558,7 @@ dependencies = [ "oximeter-instruments", "oximeter-producer", "propolis", + "propolis-api-types-versions", "propolis-server-api", "propolis_api_types", "propolis_types", @@ -5580,7 +5595,7 @@ dependencies = [ "crucible-client-types", "dropshot", "dropshot-api-manager-types", - "propolis_api_types", + "propolis-api-types-versions", ] [[package]] @@ -5636,12 +5651,7 @@ name = "propolis_api_types" version = "0.0.0" dependencies = [ "crucible-client-types", - "propolis_types", - "schemars", - "serde", - "serde_json", - "thiserror 1.0.64", - "uuid", + "propolis-api-types-versions", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f438d79ae..77ddc1233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ nvpair = { path = "crates/nvpair" } nvpair_sys = { path = "crates/nvpair/sys" } propolis-config-toml = { path = "crates/propolis-config-toml" } propolis_api_types = { path = "crates/propolis-api-types" } +propolis-api-types-versions = { path = "crates/propolis-api-types-versions" } propolis-server-api = { path = "crates/propolis-server-api" } propolis_types = { path = "crates/propolis-types" } rfb = { path = "crates/rfb" } diff --git a/bin/propolis-server/Cargo.toml b/bin/propolis-server/Cargo.toml index 7c8f4a5d1..bd21e599e 100644 --- a/bin/propolis-server/Cargo.toml +++ b/bin/propolis-server/Cargo.toml @@ -61,6 +61,7 @@ slog-term.workspace = true strum = { workspace = true, features = ["derive"] } propolis = { workspace = true, features = ["crucible-full", "oximeter"] } propolis_api_types = { workspace = true } +propolis-api-types-versions.workspace = true propolis-server-api.workspace = true propolis_types.workspace = true rgb_frame.workspace = true diff --git a/bin/propolis-server/src/lib/initializer.rs b/bin/propolis-server/src/lib/initializer.rs index ee7ebdac6..814a1bb55 100644 --- a/bin/propolis-server/src/lib/initializer.rs +++ b/bin/propolis-server/src/lib/initializer.rs @@ -46,9 +46,9 @@ use propolis::hw::uart::LpcUart; use propolis::hw::{nvme, virtio}; use propolis::intr_pins; use propolis::vmm::{self, Builder, Machine}; +use propolis_api_types::instance::InstanceProperties; use propolis_api_types::instance_spec::components::devices::SerialPortNumber; use propolis_api_types::instance_spec::{self, SpecKey}; -use propolis_api_types::InstanceProperties; use propolis_types::{CpuidIdent, CpuidVendor}; use slog::info; use strum::IntoEnumIterator; diff --git a/bin/propolis-server/src/lib/migrate/destination.rs b/bin/propolis-server/src/lib/migrate/destination.rs index 4985ffbf2..26a297d04 100644 --- a/bin/propolis-server/src/lib/migrate/destination.rs +++ b/bin/propolis-server/src/lib/migrate/destination.rs @@ -10,8 +10,8 @@ use propolis::migrate::{ MigrateCtx, MigrateStateError, Migrator, PayloadOffer, PayloadOffers, }; use propolis::vmm; +use propolis_api_types::instance::ReplacementComponent; use propolis_api_types::instance_spec::SpecKey; -use propolis_api_types::ReplacementComponent; use slog::{error, info, trace, warn}; use std::collections::BTreeMap; use std::convert::TryInto; diff --git a/bin/propolis-server/src/lib/migrate/mod.rs b/bin/propolis-server/src/lib/migrate/mod.rs index 47128e0e4..2145dd3c3 100644 --- a/bin/propolis-server/src/lib/migrate/mod.rs +++ b/bin/propolis-server/src/lib/migrate/mod.rs @@ -5,7 +5,7 @@ use bit_field::BitField; use dropshot::HttpError; use propolis::migrate::MigrateStateError; -use propolis_api_types::MigrationState; +use propolis_api_types::migration::MigrationState; use serde::{Deserialize, Serialize}; use slog::error; use thiserror::Error; diff --git a/bin/propolis-server/src/lib/migrate/preamble.rs b/bin/propolis-server/src/lib/migrate/preamble.rs index 0201665d2..3c205f8ef 100644 --- a/bin/propolis-server/src/lib/migrate/preamble.rs +++ b/bin/propolis-server/src/lib/migrate/preamble.rs @@ -4,10 +4,8 @@ use std::collections::BTreeMap; -use propolis_api_types::{ - instance_spec::{v0::ComponentV0, SpecKey, VersionedInstanceSpec}, - ReplacementComponent, -}; +use propolis_api_types::instance::ReplacementComponent; +use propolis_api_types_versions::v1; use serde::{Deserialize, Serialize}; use crate::spec::{api_spec_v0::ApiSpecError, Spec}; @@ -16,12 +14,14 @@ use super::MigrateError; #[derive(Deserialize, Serialize, Debug)] pub(crate) struct Preamble { - pub instance_spec: VersionedInstanceSpec, + pub instance_spec: v1::instance_spec::VersionedInstanceSpec, pub blobs: Vec>, } impl Preamble { - pub fn new(instance_spec: VersionedInstanceSpec) -> Preamble { + pub fn new( + instance_spec: v1::instance_spec::VersionedInstanceSpec, + ) -> Preamble { Preamble { instance_spec, blobs: Vec::new() } } @@ -35,15 +35,22 @@ impl Preamble { /// not present in the source spec, this routine fails. pub fn amend_spec( self, - replacements: &BTreeMap, + replacements: &BTreeMap< + v1::instance_spec::SpecKey, + ReplacementComponent, + >, ) -> Result { - fn wrong_type_error(id: &SpecKey, kind: &str) -> MigrateError { + fn wrong_type_error( + id: &v1::instance_spec::SpecKey, + kind: &str, + ) -> MigrateError { let msg = format!("component {id} is not a {kind} in the source spec"); MigrateError::InstanceSpecsIncompatible(msg) } - let VersionedInstanceSpec::V0(mut source_spec) = self.instance_spec; + let v1::instance_spec::VersionedInstanceSpec::V0(mut source_spec) = + self.instance_spec; for (id, comp) in replacements { let Some(to_amend) = source_spec.components.get_mut(id) else { return Err(MigrateError::InstanceSpecsIncompatible(format!( @@ -64,7 +71,9 @@ impl Preamble { #[cfg(feature = "failure-injection")] ReplacementComponent::MigrationFailureInjector(comp) => { - let ComponentV0::MigrationFailureInjector(src) = to_amend + let v1::instance_spec::Component::MigrationFailureInjector( + src, + ) = to_amend else { return Err(wrong_type_error( id, @@ -75,7 +84,9 @@ impl Preamble { *src = comp.clone(); } ReplacementComponent::CrucibleStorageBackend(comp) => { - let ComponentV0::CrucibleStorageBackend(src) = to_amend + let v1::instance_spec::Component::CrucibleStorageBackend( + src, + ) = to_amend else { return Err(wrong_type_error(id, "crucible backend")); }; @@ -83,7 +94,8 @@ impl Preamble { *src = comp.clone(); } ReplacementComponent::VirtioNetworkBackend(comp) => { - let ComponentV0::VirtioNetworkBackend(src) = to_amend + let v1::instance_spec::Component::VirtioNetworkBackend(src) = + to_amend else { return Err(wrong_type_error(id, "viona backend")); }; diff --git a/bin/propolis-server/src/lib/migrate/source.rs b/bin/propolis-server/src/lib/migrate/source.rs index f4694dc45..df9545753 100644 --- a/bin/propolis-server/src/lib/migrate/source.rs +++ b/bin/propolis-server/src/lib/migrate/source.rs @@ -9,7 +9,7 @@ use propolis::migrate::{ MigrateCtx, MigrateStateError, Migrator, PayloadOutputs, }; use propolis::vmm; -use propolis_api_types::instance_spec::VersionedInstanceSpec; +use propolis_api_types_versions::v1; use slog::{debug, error, info, trace, warn}; use std::collections::HashMap; use std::convert::TryInto; @@ -467,9 +467,10 @@ impl RonV0Runner<'_, T> { async fn sync(&mut self) -> Result<(), MigrateError> { self.update_state(MigrationState::Sync); - let preamble = Preamble::new(VersionedInstanceSpec::V0( - self.vm.lock_shared().await.instance_spec().clone().into(), - )); + let preamble = + Preamble::new(v1::instance_spec::VersionedInstanceSpec::V0( + self.vm.lock_shared().await.instance_spec().clone().into(), + )); let s = ron::ser::to_string(&preamble) .map_err(codec::ProtocolError::from)?; self.send_msg(codec::Message::Serialized(s)).await?; diff --git a/bin/propolis-server/src/lib/serial/history_buffer.rs b/bin/propolis-server/src/lib/serial/history_buffer.rs index f688e71ed..6d300a7b6 100644 --- a/bin/propolis-server/src/lib/serial/history_buffer.rs +++ b/bin/propolis-server/src/lib/serial/history_buffer.rs @@ -6,7 +6,7 @@ //! first mebibyte and the most recent mebibyte of console output. use dropshot::HttpError; -use propolis_api_types as api; +use propolis_api_types::serial as api; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::convert::TryFrom; diff --git a/bin/propolis-server/src/lib/serial/mod.rs b/bin/propolis-server/src/lib/serial/mod.rs index 82062af73..4230293fc 100644 --- a/bin/propolis-server/src/lib/serial/mod.rs +++ b/bin/propolis-server/src/lib/serial/mod.rs @@ -18,7 +18,7 @@ use futures::future::Fuse; use futures::stream::SplitSink; use futures::{FutureExt, SinkExt, StreamExt}; use propolis::chardev::{pollers, Sink, Source}; -use propolis_api_types::InstanceSerialConsoleControlMessage; +use propolis_api_types::serial::InstanceSerialConsoleControlMessage; use slog::{info, warn, Logger}; use thiserror::Error; use tokio::sync::{mpsc, oneshot, Mutex, RwLock as AsyncRwLock}; diff --git a/bin/propolis-server/src/lib/server.rs b/bin/propolis-server/src/lib/server.rs index 9664f3abc..bd70c1c1b 100644 --- a/bin/propolis-server/src/lib/server.rs +++ b/bin/propolis-server/src/lib/server.rs @@ -36,12 +36,24 @@ use internal_dns_resolver::{ResolveError, Resolver}; use internal_dns_types::names::ServiceName; pub use nexus_client::Client as NexusClient; use oximeter::types::ProducerRegistry; -use propolis_api_types as api; -use propolis_api_types::instance_spec::{ - v0::InstanceSpecGetResponseV0, SpecKey, +use propolis_api_types::disk::{ + InstanceVCRReplace, SnapshotRequestPathParams, VCRRequestPathParams, + VolumeStatus, VolumeStatusPathParams, +}; +use propolis_api_types::instance::{ + ErrorCode, Instance, InstanceEnsureRequest, InstanceEnsureResponse, + InstanceGetResponse, InstanceInitializationMethod, + InstanceStateMonitorRequest, InstanceStateMonitorResponse, + InstanceStateRequested, +}; +use propolis_api_types::instance_spec::{InstanceSpecGetResponse, SpecKey}; +use propolis_api_types::migration::{ + InstanceMigrateStartRequest, InstanceMigrateStatusResponse, +}; +use propolis_api_types::serial::{ + InstanceSerialConsoleHistoryRequest, InstanceSerialConsoleHistoryResponse, + InstanceSerialConsoleStreamRequest, }; -use propolis_api_types::v0::InstanceInitializationMethodV0; -use propolis_api_types::InstanceInitializationMethod; use propolis_server_api::PropolisServerApi; use rfb::tungstenite::BinaryWs; use slog::{error, warn, Logger}; @@ -193,17 +205,9 @@ async fn find_local_nexus_client( } } -// DEPRECATED -async fn v0_instance_get( - rqctx: &RequestContext>, -) -> Result { - let ctx = rqctx.context(); - ctx.vm.v0_get().await.ok_or_else(not_created_error) -} - async fn instance_get( rqctx: &RequestContext>, -) -> Result { +) -> Result { let ctx = rqctx.context(); ctx.vm.get().await.ok_or_else(not_created_error) } @@ -215,12 +219,10 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_ensure( rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError> - { + request: TypedBody, + ) -> Result, HttpError> { let server_context = rqctx.context(); - let api::InstanceEnsureRequest { properties, init } = - request.into_inner(); + let InstanceEnsureRequest { properties, init } = request.into_inner(); let oximeter_registry = server_context .static_config .metrics @@ -287,96 +289,7 @@ impl PropolisServerApi for PropolisServerImpl { VmError::WaitingToInitialize | VmError::AlreadyInitialized | VmError::RundownInProgress => HttpError::for_client_error( - Some(api::ErrorCode::AlreadyInitialized.to_string()), - ClientErrorStatusCode::CONFLICT, - "instance already initialized".to_string(), - ), - VmError::InitializationFailed(e) => { - HttpError::for_internal_error(format!( - "VM initialization failed: {e}" - )) - } - _ => HttpError::for_internal_error(format!( - "unexpected error from VM controller: {e}" - )), - }) - } - - async fn v0_instance_ensure( - rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError> - { - let server_context = rqctx.context(); - let api::v0::InstanceEnsureRequestV0 { properties, init } = - request.into_inner(); - let oximeter_registry = server_context - .static_config - .metrics - .as_ref() - .map(|_| ProducerRegistry::with_id(properties.id)); - - let nexus_client = - find_local_nexus_client(rqctx.server.local_addr, rqctx.log.clone()) - .await; - - let ensure_options = crate::vm::EnsureOptions { - bootrom_path: server_context.static_config.bootrom_path.clone(), - bootrom_version: server_context - .static_config - .bootrom_version - .clone(), - use_reservoir: server_context.static_config.use_reservoir, - metrics_config: server_context.static_config.metrics.clone(), - oximeter_registry, - nexus_client, - vnc_server: server_context.vnc_server.clone(), - local_server_addr: rqctx.server.local_addr, - }; - - let vm_init = match init { - InstanceInitializationMethodV0::Spec { spec } => spec - .try_into() - .map(|s| VmInitializationMethod::Spec(Box::new(s))) - .map_err(|e| { - if let Some(s) = e.source() { - format!("{e}: {s}") - } else { - e.to_string() - } - }), - InstanceInitializationMethodV0::MigrationTarget { - migration_id, - src_addr, - replace_components, - } => Ok(VmInitializationMethod::Migration(MigrationTargetInfo { - migration_id, - src_addr, - replace_components, - })), - } - .map_err(|e| { - HttpError::for_bad_request( - None, - format!("failed to generate internal instance spec: {e}"), - ) - })?; - - let request = VmEnsureRequest { properties, init: vm_init }; - server_context - .vm - .ensure(&server_context.log, request, ensure_options) - .await - .map(HttpResponseCreated) - .map_err(|e| match e { - VmError::ResultChannelClosed => HttpError::for_internal_error( - "state driver unexpectedly dropped result channel" - .to_string(), - ), - VmError::WaitingToInitialize - | VmError::AlreadyInitialized - | VmError::RundownInProgress => HttpError::for_client_error( - Some(api::ErrorCode::AlreadyInitialized.to_string()), + Some(ErrorCode::AlreadyInitialized.to_string()), ClientErrorStatusCode::CONFLICT, "instance already initialized".to_string(), ), @@ -393,22 +306,16 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_spec_get( rqctx: RequestContext, - ) -> Result, HttpError> { + ) -> Result, HttpError> { Ok(HttpResponseOk(instance_get(&rqctx).await?)) } - async fn v0_instance_spec_get( - rqctx: RequestContext, - ) -> Result, HttpError> { - Ok(HttpResponseOk(v0_instance_get(&rqctx).await?)) - } - async fn instance_get( rqctx: RequestContext, - ) -> Result, HttpError> { + ) -> Result, HttpError> { instance_get(&rqctx).await.map(|full| { - HttpResponseOk(api::InstanceGetResponse { - instance: api::Instance { + HttpResponseOk(InstanceGetResponse { + instance: Instance { properties: full.properties, state: full.state, }, @@ -418,9 +325,8 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_state_monitor( rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError> - { + request: TypedBody, + ) -> Result, HttpError> { let ctx = rqctx.context(); let gen = request.into_inner().gen; let mut state_watcher = @@ -438,7 +344,7 @@ impl PropolisServerApi for PropolisServerImpl { // Inform the client of this condition so it doesn't wait forever. state_watcher.changed().await.map_err(|_| { HttpError::for_client_error( - Some(api::ErrorCode::NoInstance.to_string()), + Some(ErrorCode::NoInstance.to_string()), ClientErrorStatusCode::GONE, format!( "No instance present; will never reach generation {gen}", @@ -450,7 +356,7 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_state_put( rqctx: RequestContext, - request: TypedBody, + request: TypedBody, ) -> Result { let ctx = rqctx.context(); let requested_state = request.into_inner(); @@ -477,7 +383,7 @@ impl PropolisServerApi for PropolisServerImpl { }); if result.is_ok() { - if let api::InstanceStateRequested::Reboot = requested_state { + if let InstanceStateRequested::Reboot = requested_state { let stats = MutexGuard::map( vm.services().oximeter.lock().await, |state| &mut state.stats, @@ -493,11 +399,9 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_serial_history_get( rqctx: RequestContext, - query: Query, - ) -> Result< - HttpResponseOk, - HttpError, - > { + query: Query, + ) -> Result, HttpError> + { let ctx = rqctx.context(); let vm = ctx.vm.active_vm().await.ok_or_else(not_created_error)?; let serial = vm.objects().lock_shared().await.com1().clone(); @@ -511,7 +415,7 @@ impl PropolisServerApi for PropolisServerImpl { .await .map_err(|e| HttpError::for_bad_request(None, e.to_string()))?; - Ok(HttpResponseOk(api::InstanceSerialConsoleHistoryResponse { + Ok(HttpResponseOk(InstanceSerialConsoleHistoryResponse { data, last_byte_offset: end as u64, })) @@ -519,7 +423,7 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_serial( rqctx: RequestContext, - query: Query, + query: Query, websock: WebsocketConnection, ) -> dropshot::WebsocketChannelResult { let ctx = rqctx.context(); @@ -599,7 +503,7 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_migrate_start( rqctx: RequestContext, - path_params: Path, + path_params: Path, websock: WebsocketConnection, ) -> dropshot::WebsocketChannelResult { let ctx = rqctx.context(); @@ -610,8 +514,7 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_migrate_status( rqctx: RequestContext, - ) -> Result, HttpError> - { + ) -> Result, HttpError> { let ctx = rqctx.context(); ctx.vm .state_watcher() @@ -622,7 +525,7 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_issue_crucible_snapshot_request( rqctx: RequestContext, - path_params: Path, + path_params: Path, ) -> Result, HttpError> { let vm = rqctx .context() @@ -649,8 +552,8 @@ impl PropolisServerApi for PropolisServerImpl { async fn disk_volume_status( rqctx: RequestContext, - path_params: Path, - ) -> Result, HttpError> { + path_params: Path, + ) -> Result, HttpError> { let path_params = path_params.into_inner(); let vm = rqctx .context() @@ -668,7 +571,7 @@ impl PropolisServerApi for PropolisServerImpl { HttpError::for_not_found(Some(s.clone()), s) })?; - Ok(HttpResponseOk(api::VolumeStatus { + Ok(HttpResponseOk(VolumeStatus { active: backend.volume_is_active().await.map_err(|e| { HttpError::for_bad_request(Some(e.to_string()), e.to_string()) })?, @@ -677,8 +580,8 @@ impl PropolisServerApi for PropolisServerImpl { async fn instance_issue_crucible_vcr_request( rqctx: RequestContext, - path_params: Path, - request: TypedBody, + path_params: Path, + request: TypedBody, ) -> Result, HttpError> { let path_params = path_params.into_inner(); @@ -747,7 +650,7 @@ pub fn api() -> ApiDescription> { fn not_created_error() -> HttpError { HttpError::for_client_error( - Some(api::ErrorCode::NoInstance.to_string()), + Some(ErrorCode::NoInstance.to_string()), ClientErrorStatusCode::FAILED_DEPENDENCY, "Server not initialized (no instance)".to_string(), ) diff --git a/bin/propolis-server/src/lib/spec/api_spec_v0.rs b/bin/propolis-server/src/lib/spec/api_spec_v0.rs index 2b52c16c7..704084ca8 100644 --- a/bin/propolis-server/src/lib/spec/api_spec_v0.rs +++ b/bin/propolis-server/src/lib/spec/api_spec_v0.rs @@ -13,9 +13,9 @@ use propolis_api_types::instance_spec::{ board::Board as InstanceSpecBoard, devices::{BootSettings, SerialPort as SerialPortDesc}, }, - v0::{ComponentV0, InstanceSpecV0}, SpecKey, }; +use propolis_api_types_versions::v1; use thiserror::Error; #[cfg(feature = "falcon")] @@ -52,7 +52,7 @@ pub(crate) enum ApiSpecError { BackendNotUsed(SpecKey), } -impl From for InstanceSpecV0 { +impl From for v1::instance_spec::InstanceSpec { fn from(val: Spec) -> Self { // Exhaustively destructure the input spec so that adding a new field // without considering it here will break the build. @@ -70,7 +70,7 @@ impl From for InstanceSpecV0 { #[cfg(feature = "falcon")] softnpu, - // Not part of `InstanceSpecV0`. Added in `InstanceSpec` in API + // Not part of `v1::instance_spec::InstanceSpec`. Added in `InstanceSpec` in API // Version 2.0.0. smbios_type1_input: _, } = val; @@ -83,9 +83,9 @@ impl From for InstanceSpecV0 { // upholds this invariant at spec creation time. #[track_caller] fn insert_component( - spec: &mut InstanceSpecV0, + spec: &mut v1::instance_spec::InstanceSpec, key: SpecKey, - val: ComponentV0, + val: v1::instance_spec::Component, ) { assert!( !spec.components.contains_key(&key), @@ -102,7 +102,10 @@ impl From for InstanceSpecV0 { guest_hv_interface: board.guest_hv_interface, cpuid: Some(cpuid.into_instance_spec_cpuid()), }; - let mut spec = InstanceSpecV0 { board, components: Default::default() }; + let mut spec = v1::instance_spec::InstanceSpec { + board, + components: Default::default(), + }; for (disk_id, disk) in disks { let backend_id = disk.device_spec.backend_id().to_owned(); @@ -115,13 +118,15 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, nic_id, - ComponentV0::VirtioNic(nic.device_spec), + v1::instance_spec::Component::VirtioNic(nic.device_spec), ); insert_component( &mut spec, backend_id, - ComponentV0::VirtioNetworkBackend(nic.backend_spec), + v1::instance_spec::Component::VirtioNetworkBackend( + nic.backend_spec, + ), ); } @@ -130,7 +135,9 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, name, - ComponentV0::SerialPort(SerialPortDesc { num: desc.num }), + v1::instance_spec::Component::SerialPort(SerialPortDesc { + num: desc.num, + }), ); } } @@ -139,7 +146,7 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, bridge_name, - ComponentV0::PciPciBridge(bridge), + v1::instance_spec::Component::PciPciBridge(bridge), ); } @@ -147,7 +154,7 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, pvpanic.id, - ComponentV0::QemuPvpanic(pvpanic.spec), + v1::instance_spec::Component::QemuPvpanic(pvpanic.spec), ); } @@ -155,7 +162,7 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, settings.name, - ComponentV0::BootSettings(BootSettings { + v1::instance_spec::Component::BootSettings(BootSettings { order: settings.order.into_iter().map(Into::into).collect(), }), ); @@ -166,7 +173,9 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, mig.id, - ComponentV0::MigrationFailureInjector(mig.spec), + v1::instance_spec::Component::MigrationFailureInjector( + mig.spec, + ), ); } @@ -179,7 +188,7 @@ impl From for InstanceSpecV0 { "softnpu-pci-{}", softnpu_pci.pci_path )), - ComponentV0::SoftNpuPciPort(softnpu_pci), + v1::instance_spec::Component::SoftNpuPciPort(softnpu_pci), ); } @@ -187,7 +196,7 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, SpecKey::Name(format!("softnpu-p9-{}", p9.pci_path)), - ComponentV0::SoftNpuP9(p9), + v1::instance_spec::Component::SoftNpuP9(p9), ); } @@ -195,7 +204,7 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, SpecKey::Name(format!("p9fs-{}", p9fs.pci_path)), - ComponentV0::P9fs(p9fs), + v1::instance_spec::Component::P9fs(p9fs), ); } @@ -203,16 +212,20 @@ impl From for InstanceSpecV0 { insert_component( &mut spec, port_name.clone(), - ComponentV0::SoftNpuPort(SoftNpuPortSpec { - link_name: port.link_name, - backend_id: port.backend_name.clone(), - }), + v1::instance_spec::Component::SoftNpuPort( + SoftNpuPortSpec { + link_name: port.link_name, + backend_id: port.backend_name.clone(), + }, + ), ); insert_component( &mut spec, port.backend_name, - ComponentV0::DlpiNetworkBackend(port.backend_spec), + v1::instance_spec::Component::DlpiNetworkBackend( + port.backend_spec, + ), ); } } @@ -221,12 +234,14 @@ impl From for InstanceSpecV0 { } } -impl TryFrom for Spec { +impl TryFrom for Spec { type Error = ApiSpecError; - fn try_from(value: InstanceSpecV0) -> Result { + fn try_from( + value: v1::instance_spec::InstanceSpec, + ) -> Result { let mut builder = SpecBuilder::with_instance_spec_board(value.board)?; - let mut devices: Vec<(SpecKey, ComponentV0)> = vec![]; + let mut devices: Vec<(SpecKey, v1::instance_spec::Component)> = vec![]; let mut boot_settings = None; let mut storage_backends: BTreeMap = BTreeMap::new(); @@ -237,9 +252,9 @@ impl TryFrom for Spec { for (id, component) in value.components.into_iter() { match component { - ComponentV0::CrucibleStorageBackend(_) - | ComponentV0::FileStorageBackend(_) - | ComponentV0::BlobStorageBackend(_) => { + v1::instance_spec::Component::CrucibleStorageBackend(_) + | v1::instance_spec::Component::FileStorageBackend(_) + | v1::instance_spec::Component::BlobStorageBackend(_) => { storage_backends.insert( id, component.try_into().expect( @@ -247,10 +262,10 @@ impl TryFrom for Spec { ), ); } - ComponentV0::VirtioNetworkBackend(viona) => { + v1::instance_spec::Component::VirtioNetworkBackend(viona) => { viona_backends.insert(id, viona); } - ComponentV0::DlpiNetworkBackend(dlpi) => { + v1::instance_spec::Component::DlpiNetworkBackend(dlpi) => { dlpi_backends.insert(id, dlpi); } device => { @@ -261,7 +276,8 @@ impl TryFrom for Spec { for (device_id, device_spec) in devices { match device_spec { - ComponentV0::VirtioDisk(_) | ComponentV0::NvmeDisk(_) => { + v1::instance_spec::Component::VirtioDisk(_) + | v1::instance_spec::Component::NvmeDisk(_) => { let device_spec = StorageDevice::try_from(device_spec) .expect("component is known to be a disk"); @@ -279,7 +295,7 @@ impl TryFrom for Spec { Disk { device_spec, backend_spec }, )?; } - ComponentV0::VirtioNic(nic) => { + v1::instance_spec::Component::VirtioNic(nic) => { let (_, backend_spec) = viona_backends .remove_entry(&nic.backend_id) .ok_or_else(|| { @@ -294,19 +310,19 @@ impl TryFrom for Spec { Nic { device_spec: nic, backend_spec }, )?; } - ComponentV0::SerialPort(port) => { + v1::instance_spec::Component::SerialPort(port) => { builder.add_serial_port(device_id, port.num)?; } - ComponentV0::PciPciBridge(bridge) => { + v1::instance_spec::Component::PciPciBridge(bridge) => { builder.add_pci_bridge(device_id, bridge)?; } - ComponentV0::QemuPvpanic(pvpanic) => { + v1::instance_spec::Component::QemuPvpanic(pvpanic) => { builder.add_pvpanic_device(QemuPvpanic { id: device_id, spec: pvpanic, })?; } - ComponentV0::BootSettings(settings) => { + v1::instance_spec::Component::BootSettings(settings) => { // The builder returns an error if its caller tries to add // a boot option that isn't in the set of attached disks. // Since there may be more disk devices left in the @@ -315,35 +331,35 @@ impl TryFrom for Spec { boot_settings = Some((device_id, settings)); } #[cfg(not(feature = "failure-injection"))] - ComponentV0::MigrationFailureInjector(_) => { + v1::instance_spec::Component::MigrationFailureInjector(_) => { return Err(ApiSpecError::FeatureCompiledOut { component: device_id, feature: "failure-injection", }); } #[cfg(feature = "failure-injection")] - ComponentV0::MigrationFailureInjector(mig) => { + v1::instance_spec::Component::MigrationFailureInjector(mig) => { builder.add_migration_failure_device(MigrationFailure { id: device_id, spec: mig, })?; } #[cfg(not(feature = "falcon"))] - ComponentV0::SoftNpuPciPort(_) - | ComponentV0::SoftNpuPort(_) - | ComponentV0::SoftNpuP9(_) - | ComponentV0::P9fs(_) => { + v1::instance_spec::Component::SoftNpuPciPort(_) + | v1::instance_spec::Component::SoftNpuPort(_) + | v1::instance_spec::Component::SoftNpuP9(_) + | v1::instance_spec::Component::P9fs(_) => { return Err(ApiSpecError::FeatureCompiledOut { component: device_id, feature: "falcon", }); } #[cfg(feature = "falcon")] - ComponentV0::SoftNpuPciPort(port) => { + v1::instance_spec::Component::SoftNpuPciPort(port) => { builder.set_softnpu_pci_port(port)?; } #[cfg(feature = "falcon")] - ComponentV0::SoftNpuPort(port) => { + v1::instance_spec::Component::SoftNpuPort(port) => { let (_, backend_spec) = dlpi_backends .remove_entry(&port.backend_id) .ok_or_else(|| { @@ -362,18 +378,18 @@ impl TryFrom for Spec { builder.add_softnpu_port(device_id, port)?; } #[cfg(feature = "falcon")] - ComponentV0::SoftNpuP9(p9) => { + v1::instance_spec::Component::SoftNpuP9(p9) => { builder.set_softnpu_p9(p9)?; } #[cfg(feature = "falcon")] - ComponentV0::P9fs(p9fs) => { + v1::instance_spec::Component::P9fs(p9fs) => { builder.set_p9fs(p9fs)?; } - ComponentV0::CrucibleStorageBackend(_) - | ComponentV0::FileStorageBackend(_) - | ComponentV0::BlobStorageBackend(_) - | ComponentV0::VirtioNetworkBackend(_) - | ComponentV0::DlpiNetworkBackend(_) => { + v1::instance_spec::Component::CrucibleStorageBackend(_) + | v1::instance_spec::Component::FileStorageBackend(_) + | v1::instance_spec::Component::BlobStorageBackend(_) + | v1::instance_spec::Component::VirtioNetworkBackend(_) + | v1::instance_spec::Component::DlpiNetworkBackend(_) => { unreachable!("already filtered out backends") } } diff --git a/bin/propolis-server/src/lib/spec/mod.rs b/bin/propolis-server/src/lib/spec/mod.rs index 236730c87..8ea49325d 100644 --- a/bin/propolis-server/src/lib/spec/mod.rs +++ b/bin/propolis-server/src/lib/spec/mod.rs @@ -30,10 +30,10 @@ use propolis_api_types::instance_spec::{ SerialPortNumber, VirtioDisk, VirtioNic, }, }, - v0::{ComponentV0, InstanceSpecV0}, PciPath, SpecKey, }; -use propolis_api_types::{InstanceSpec, SmbiosType1Input}; +use propolis_api_types::instance_spec::{InstanceSpec, SmbiosType1Input}; +use propolis_api_types_versions::v1; use thiserror::Error; #[cfg(feature = "failure-injection")] @@ -53,7 +53,8 @@ pub(crate) mod builder; impl From for InstanceSpec { fn from(val: Spec) -> Self { let smbios = val.smbios_type1_input.clone(); - let InstanceSpecV0 { board, components } = InstanceSpecV0::from(val); + let v1::instance_spec::InstanceSpec { board, components } = + v1::instance_spec::InstanceSpec::from(val); InstanceSpec { board, components, smbios } } } @@ -64,7 +65,7 @@ impl TryFrom for Spec { fn try_from(value: InstanceSpec) -> Result { let InstanceSpec { board, components, smbios } = value; - let v0 = InstanceSpecV0 { board, components }; + let v0 = v1::instance_spec::InstanceSpec { board, components }; let mut spec: Spec = v0.try_into()?; spec.smbios_type1_input = smbios; Ok(spec) @@ -109,8 +110,8 @@ pub(crate) struct Spec { // backwards compatibility, but that isn't currently possible. // // One way to fix this would be to remove the `Builder` and directly - // construct `Spec` from a function that takes an `InstanceSpecV0` and the - // VM UUID. This would replace `impl TryFrom for Spec`, and + // construct `Spec` from a function that takes an `v1::instance_spec::InstanceSpec` and the + // VM UUID. This would replace `impl TryFrom for Spec`, and // would allow removing the `Default` derive on `Spec`, and the `Option` // from the `smbios_type1_input` field. pub smbios_type1_input: Option, @@ -202,7 +203,7 @@ impl StorageDevice { } } -impl From for ComponentV0 { +impl From for v1::instance_spec::Component { fn from(value: StorageDevice) -> Self { match value { StorageDevice::Virtio(d) => Self::VirtioDisk(d), @@ -211,13 +212,15 @@ impl From for ComponentV0 { } } -impl TryFrom for StorageDevice { +impl TryFrom for StorageDevice { type Error = ComponentTypeMismatch; - fn try_from(value: ComponentV0) -> Result { + fn try_from( + value: v1::instance_spec::Component, + ) -> Result { match value { - ComponentV0::VirtioDisk(d) => Ok(Self::Virtio(d)), - ComponentV0::NvmeDisk(d) => Ok(Self::Nvme(d)), + v1::instance_spec::Component::VirtioDisk(d) => Ok(Self::Virtio(d)), + v1::instance_spec::Component::NvmeDisk(d) => Ok(Self::Nvme(d)), _ => Err(ComponentTypeMismatch), } } @@ -249,7 +252,7 @@ impl StorageBackend { } } -impl From for ComponentV0 { +impl From for v1::instance_spec::Component { fn from(value: StorageBackend) -> Self { match value { StorageBackend::Crucible(be) => Self::CrucibleStorageBackend(be), @@ -259,14 +262,22 @@ impl From for ComponentV0 { } } -impl TryFrom for StorageBackend { +impl TryFrom for StorageBackend { type Error = ComponentTypeMismatch; - fn try_from(value: ComponentV0) -> Result { + fn try_from( + value: v1::instance_spec::Component, + ) -> Result { match value { - ComponentV0::CrucibleStorageBackend(be) => Ok(Self::Crucible(be)), - ComponentV0::FileStorageBackend(be) => Ok(Self::File(be)), - ComponentV0::BlobStorageBackend(be) => Ok(Self::Blob(be)), + v1::instance_spec::Component::CrucibleStorageBackend(be) => { + Ok(Self::Crucible(be)) + } + v1::instance_spec::Component::FileStorageBackend(be) => { + Ok(Self::File(be)) + } + v1::instance_spec::Component::BlobStorageBackend(be) => { + Ok(Self::Blob(be)) + } _ => Err(ComponentTypeMismatch), } } diff --git a/bin/propolis-server/src/lib/stats/virtual_machine.rs b/bin/propolis-server/src/lib/stats/virtual_machine.rs index bfcf59537..976c11031 100644 --- a/bin/propolis-server/src/lib/stats/virtual_machine.rs +++ b/bin/propolis-server/src/lib/stats/virtual_machine.rs @@ -64,7 +64,7 @@ pub struct VirtualMachine { impl VirtualMachine { pub fn new( n_vcpus: u8, - properties: &propolis_api_types::InstanceProperties, + properties: &propolis_api_types::instance::InstanceProperties, ) -> Self { Self { target: VirtualMachineTarget { diff --git a/bin/propolis-server/src/lib/vm/active.rs b/bin/propolis-server/src/lib/vm/active.rs index 1da8f7ea4..2abb93a0b 100644 --- a/bin/propolis-server/src/lib/vm/active.rs +++ b/bin/propolis-server/src/lib/vm/active.rs @@ -6,9 +6,10 @@ use std::sync::Arc; -use propolis_api_types::{ - instance_spec::SpecKey, InstanceProperties, InstanceStateRequested, +use propolis_api_types::instance::{ + InstanceProperties, InstanceStateRequested, }; +use propolis_api_types::instance_spec::SpecKey; use slog::info; use uuid::Uuid; diff --git a/bin/propolis-server/src/lib/vm/ensure.rs b/bin/propolis-server/src/lib/vm/ensure.rs index 20bc39ee5..dc5de2fb5 100644 --- a/bin/propolis-server/src/lib/vm/ensure.rs +++ b/bin/propolis-server/src/lib/vm/ensure.rs @@ -98,13 +98,13 @@ use propolis::enlightenment::{ hyperv::{Features as HyperVFeatures, HyperV}, Enlightenment, }; -use propolis_api_types::{ - instance_spec::components::board::{ - GuestHypervisorInterface, HyperVFeatureFlag, - }, - InstanceEnsureResponse, InstanceMigrateInitiateResponse, - InstanceProperties, InstanceState, +use propolis_api_types::instance::{ + InstanceEnsureResponse, InstanceProperties, InstanceState, +}; +use propolis_api_types::instance_spec::components::board::{ + GuestHypervisorInterface, HyperVFeatureFlag, }; +use propolis_api_types::migration::InstanceMigrateInitiateResponse; use slog::{debug, info}; use crate::{ diff --git a/bin/propolis-server/src/lib/vm/mod.rs b/bin/propolis-server/src/lib/vm/mod.rs index 1aad36126..e40d9e4f7 100644 --- a/bin/propolis-server/src/lib/vm/mod.rs +++ b/bin/propolis-server/src/lib/vm/mod.rs @@ -84,15 +84,15 @@ use std::{collections::BTreeMap, net::SocketAddr, path::PathBuf, sync::Arc}; use active::ActiveVm; use ensure::VmEnsureRequest; use oximeter::types::ProducerRegistry; -use propolis_api_types::instance_spec::v0::{ - InstanceSpecGetResponseV0, InstanceSpecStatusV0, +use propolis_api_types::instance::{ + InstanceEnsureResponse, InstanceProperties, InstanceState, + InstanceStateMonitorResponse, }; -use propolis_api_types::{ - instance_spec::{SpecKey, VersionedInstanceSpec}, - InstanceEnsureResponse, InstanceMigrateStatusResponse, - InstanceMigrationStatus, InstanceProperties, InstanceSpecGetResponse, - InstanceSpecStatus, InstanceState, InstanceStateMonitorResponse, - MigrationState, +use propolis_api_types::instance_spec::{ + InstanceSpecGetResponse, InstanceSpecStatus, SpecKey, +}; +use propolis_api_types::migration::{ + InstanceMigrateStatusResponse, InstanceMigrationStatus, MigrationState, }; use slog::info; use state_driver::StateDriverOutput; @@ -229,19 +229,6 @@ impl From for InstanceSpecStatus { } } -impl From for InstanceSpecStatusV0 { - fn from(value: MaybeSpec) -> Self { - match value { - MaybeSpec::WaitingForMigrationSource => { - Self::WaitingForMigrationSource - } - MaybeSpec::Present(spec) => { - Self::Present(VersionedInstanceSpec::V0((*spec).into())) - } - } - } -} - /// Describes a past or future VM and its properties. struct VmDescription { /// Records the VM's last externally-visible state. @@ -347,50 +334,6 @@ impl Vm { .ok() } - /// Returns the state, properties, and instance spec for the instance most - /// recently wrapped by this `Vm`. - /// - /// # Returns - /// - /// - `Some` if the VM has been created. - /// - `None` if no VM has ever been created. - pub(super) async fn v0_get(&self) -> Option { - let guard = self.inner.read().await; - match &guard.state { - // If no VM has ever been created, there's nothing to get. - VmState::NoVm => None, - - // If the VM is active, pull the required data out of its objects. - VmState::Active(vm) => { - let spec = - vm.objects().lock_shared().await.instance_spec().clone(); - let state = vm.external_state_rx.borrow().clone(); - Some(InstanceSpecGetResponseV0 { - properties: vm.properties.clone(), - spec: InstanceSpecStatusV0::Present( - VersionedInstanceSpec::V0(spec.into()), - ), - state: state.state, - }) - } - VmState::WaitingForInit { vm, spec } - | VmState::RundownComplete { vm, spec } => { - Some(InstanceSpecGetResponseV0 { - properties: vm.properties.clone(), - state: vm.external_state_rx.borrow().state, - spec: spec.clone().into(), - }) - } - VmState::Rundown { vm, spec } => Some(InstanceSpecGetResponseV0 { - properties: vm.properties.clone(), - state: vm.external_state_rx.borrow().state, - spec: InstanceSpecStatusV0::Present(VersionedInstanceSpec::V0( - spec.as_ref().to_owned().into(), - )), - }), - } - } - /// Returns the state, properties, and instance spec for the instance most /// recently wrapped by this `Vm`. /// diff --git a/bin/propolis-server/src/lib/vm/services.rs b/bin/propolis-server/src/lib/vm/services.rs index da73fa702..7281b159d 100644 --- a/bin/propolis-server/src/lib/vm/services.rs +++ b/bin/propolis-server/src/lib/vm/services.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use oximeter::types::ProducerRegistry; -use propolis_api_types::InstanceProperties; +use propolis_api_types::instance::InstanceProperties; use slog::{error, info, Logger}; use crate::{ diff --git a/bin/propolis-server/src/lib/vm/state_driver.rs b/bin/propolis-server/src/lib/vm/state_driver.rs index d4ae5f053..556bc70a6 100644 --- a/bin/propolis-server/src/lib/vm/state_driver.rs +++ b/bin/propolis-server/src/lib/vm/state_driver.rs @@ -96,10 +96,11 @@ use std::{ use anyhow::Context; use dropshot::HttpError; -use propolis_api_types::{ - instance_spec::{components::backends::CrucibleStorageBackend, SpecKey}, - InstanceState, MigrationState, +use propolis_api_types::instance::InstanceState; +use propolis_api_types::instance_spec::{ + components::backends::CrucibleStorageBackend, SpecKey, }; +use propolis_api_types::migration::MigrationState; use slog::{error, info}; use tokio::sync::Notify; use uuid::Uuid; diff --git a/bin/propolis-server/src/lib/vm/state_publisher.rs b/bin/propolis-server/src/lib/vm/state_publisher.rs index 63a4f93a1..fa9e559a3 100644 --- a/bin/propolis-server/src/lib/vm/state_publisher.rs +++ b/bin/propolis-server/src/lib/vm/state_publisher.rs @@ -5,9 +5,11 @@ //! Helper types for publishing instance states as made visible through the //! external API. -use propolis_api_types::{ - InstanceMigrateStatusResponse, InstanceMigrationStatus, InstanceState, - InstanceStateMonitorResponse, +use propolis_api_types::instance::{ + InstanceState, InstanceStateMonitorResponse, +}; +use propolis_api_types::migration::{ + InstanceMigrateStatusResponse, InstanceMigrationStatus, }; use slog::info; use uuid::Uuid; @@ -19,7 +21,7 @@ use super::{InstanceStateRx, InstanceStateTx}; /// An update to an instance's migration's state. pub(crate) struct MigrationStateUpdate { /// The migration's new state. - pub state: propolis_api_types::MigrationState, + pub state: propolis_api_types::migration::MigrationState, /// The migration's ID. pub id: Uuid, @@ -104,11 +106,10 @@ impl StatePublisher { "state" => ?state, "migration" => ?migration); - let _ = - self.tx.send(propolis_api_types::InstanceStateMonitorResponse { - gen, - state, - migration, - }); + let _ = self.tx.send(InstanceStateMonitorResponse { + gen, + state, + migration, + }); } } diff --git a/crates/propolis-api-types-versions/Cargo.toml b/crates/propolis-api-types-versions/Cargo.toml new file mode 100644 index 000000000..8fefbe843 --- /dev/null +++ b/crates/propolis-api-types-versions/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "propolis-api-types-versions" +version = "0.0.0" +license = "MPL-2.0" +edition = "2021" + +[lib] +doctest = false + +[dependencies] +crucible-client-types.workspace = true +propolis_types.workspace = true +schemars.workspace = true +serde.workspace = true +thiserror.workspace = true +uuid.workspace = true + +[dev-dependencies] +serde_json.workspace = true diff --git a/crates/propolis-api-types-versions/src/impls/instance.rs b/crates/propolis-api-types-versions/src/impls/instance.rs new file mode 100644 index 000000000..01cad5860 --- /dev/null +++ b/crates/propolis-api-types-versions/src/impls/instance.rs @@ -0,0 +1,41 @@ +// 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/. + +//! Functional code for instance types. + +use crate::latest::instance::{ErrorCode, InstanceProperties}; + +impl InstanceProperties { + /// Return the name of the VMM resource backing this VM. + pub fn vm_name(&self) -> String { + self.id.to_string() + } +} + +impl std::fmt::Display for ErrorCode { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} + +impl std::str::FromStr for ErrorCode { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s.trim() { + s if s.eq_ignore_ascii_case("NoInstance") => Ok(Self::NoInstance), + s if s.eq_ignore_ascii_case("AlreadyInitialized") => { + Ok(ErrorCode::AlreadyInitialized) + } + s if s.eq_ignore_ascii_case("AlreadyRunning") => { + Ok(ErrorCode::AlreadyRunning) + } + s if s.eq_ignore_ascii_case("CreateFailed") => { + Ok(ErrorCode::CreateFailed) + } + _ => Err("unknown error code, expected one of: \ + 'NoInstance', 'AlreadyInitialized', 'AlreadyRunning', \ + 'CreateFailed'"), + } + } +} diff --git a/crates/propolis-api-types-versions/src/impls/instance_spec.rs b/crates/propolis-api-types-versions/src/impls/instance_spec.rs new file mode 100644 index 000000000..92100610f --- /dev/null +++ b/crates/propolis-api-types-versions/src/impls/instance_spec.rs @@ -0,0 +1,105 @@ +// 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/. + +//! Functional code for instance spec types. + +use uuid::Uuid; + +use crate::latest::instance_spec::SpecKey; + +impl std::fmt::Display for SpecKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Uuid(uuid) => write!(f, "{uuid}"), + Self::Name(name) => write!(f, "{name}"), + } + } +} + +impl std::str::FromStr for SpecKey { + type Err = core::convert::Infallible; + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + +impl From<&str> for SpecKey { + fn from(s: &str) -> Self { + match Uuid::parse_str(s) { + Ok(uuid) => Self::Uuid(uuid), + Err(_) => Self::Name(s.to_owned()), + } + } +} + +impl From for SpecKey { + fn from(value: String) -> Self { + match Uuid::parse_str(value.as_str()) { + Ok(uuid) => Self::Uuid(uuid), + Err(_) => Self::Name(value), + } + } +} + +impl From for SpecKey { + fn from(value: Uuid) -> Self { + Self::Uuid(value) + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use uuid::Uuid; + + use super::SpecKey; + use crate::latest::components::devices::QemuPvpanic; + use crate::latest::instance_spec::Component; + + type TestMap = BTreeMap; + + // Verifies that UUID-type spec keys that are serialized and deserialized + // continue to be interpreted as UUID-type spec keys. + #[test] + fn spec_key_uuid_roundtrip() { + let id = Uuid::new_v4(); + let mut map = TestMap::new(); + map.insert( + SpecKey::Uuid(id), + Component::QemuPvpanic(QemuPvpanic { enable_isa: true }), + ); + + let ser = serde_json::to_string(&map).unwrap(); + let unser: TestMap = serde_json::from_str(&ser).unwrap(); + let key = unser.keys().next().expect("one key in the map"); + let SpecKey::Uuid(got_id) = key else { + panic!("expected SpecKey::Uuid, got {key}"); + }; + + assert_eq!(*got_id, id); + } + + // Verifies that serializing a name-type spec key that happens to be the + // string representation of a UUID causes the key to deserialize as a + // UUID-type key. + #[test] + fn spec_key_uuid_string_deserializes_as_uuid_variant() { + let id = Uuid::new_v4(); + let mut map = TestMap::new(); + map.insert( + SpecKey::Name(id.to_string()), + Component::QemuPvpanic(QemuPvpanic { enable_isa: true }), + ); + + let ser = serde_json::to_string(&map).unwrap(); + let unser: TestMap = serde_json::from_str(&ser).unwrap(); + let key = unser.keys().next().expect("one key in the map"); + let SpecKey::Uuid(got_id) = key else { + panic!("expected SpecKey::Uuid, got {key}"); + }; + + assert_eq!(*got_id, id); + } +} diff --git a/crates/propolis-api-types-versions/src/impls/mod.rs b/crates/propolis-api-types-versions/src/impls/mod.rs new file mode 100644 index 000000000..5bb760c00 --- /dev/null +++ b/crates/propolis-api-types-versions/src/impls/mod.rs @@ -0,0 +1,8 @@ +// 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/. + +//! Functional code for the latest versions of types. + +mod instance; +mod instance_spec; diff --git a/crates/propolis-api-types-versions/src/initial/components/backends.rs b/crates/propolis-api-types-versions/src/initial/components/backends.rs new file mode 100644 index 000000000..0a711faab --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/components/backends.rs @@ -0,0 +1,94 @@ +// 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/. + +//! Backend configuration data: the structs that tell Propolis how to configure +//! its components to talk to other services supplied by the host OS or the +//! larger rack. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::num::NonZeroUsize; + +/// A Crucible storage backend. +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct CrucibleStorageBackend { + /// 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. + /// + /// When 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. + pub request_json: String, + + /// Indicates whether the storage is read-only. + pub readonly: bool, +} + +impl std::fmt::Debug for CrucibleStorageBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Redact the contents of the VCR since they may contain volume + // encryption keys. + f.debug_struct("CrucibleStorageBackend") + .field("request_json", &"".to_string()) + .field("readonly", &self.readonly) + .finish() + } +} + +/// A storage backend backed by a file in the host system's file system. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct FileStorageBackend { + /// A path to a file that backs a disk. + pub path: String, + + /// Indicates whether the storage is read-only. + pub readonly: bool, + + /// Block size of the backend + pub block_size: u32, + + /// Optional worker threads for the file backend, exposed for testing only. + pub workers: Option, +} + +/// A storage backend for a disk whose initial contents are given explicitly +/// by the specification. +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct BlobStorageBackend { + /// The disk's initial contents, encoded as a base64 string. + pub base64: String, + + /// Indicates whether the storage is read-only. + pub readonly: bool, +} + +impl std::fmt::Debug for BlobStorageBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BlobStorageBackend") + .field("base64", &"".to_string()) + .field("readonly", &self.readonly) + .finish() + } +} + +/// A network backend associated with a virtio-net (viona) VNIC on the host. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct VirtioNetworkBackend { + /// The name of the viona VNIC to use as a backend. + pub vnic_name: String, +} + +/// A network backend associated with a DLPI VNIC on the host. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct DlpiNetworkBackend { + /// The name of the VNIC to use as a backend. + pub vnic_name: String, +} diff --git a/crates/propolis-api-types-versions/src/initial/components/board.rs b/crates/propolis-api-types-versions/src/initial/components/board.rs new file mode 100644 index 000000000..1addc54b9 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/components/board.rs @@ -0,0 +1,182 @@ +// 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/. + +//! VM mainboard components. Every VM has a board, even if it has no other +//! peripherals. + +use std::collections::BTreeSet; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v1::instance_spec::CpuidVendor; + +/// An Intel 440FX-compatible chipset. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(deny_unknown_fields)] +pub struct I440Fx { + /// Specifies whether the chipset should allow PCI configuration space + /// to be accessed through the PCIe extended configuration mechanism. + pub enable_pcie: bool, +} + +/// A kind of virtual chipset. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde( + deny_unknown_fields, + rename_all = "snake_case", + tag = "type", + content = "value" +)] +pub enum Chipset { + /// An Intel 440FX-compatible chipset. + I440Fx(I440Fx), +} + +impl Default for Chipset { + fn default() -> Self { + Self::I440Fx(I440Fx { enable_pcie: false }) + } +} + +/// A set of CPUID values to expose to a guest. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Cpuid { + /// A list of CPUID leaves/subleaves and their associated values. + /// + /// Propolis 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. + // + // It would be nice if this were an associative collection type. + // Unfortunately, the most natural keys for such a collection are + // structs or tuples, and JSON doesn't allow objects to be used as + // property names. Instead of converting leaf/subleaf pairs to and from + // strings, just accept a flat Vec and have servers verify that e.g. no + // leaf/subleaf pairs are duplicated. + pub entries: Vec, + + /// The CPU vendor to emulate. + /// + /// CPUID 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. + pub vendor: CpuidVendor, +} + +/// A full description of a CPUID leaf/subleaf and the values it produces. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(deny_unknown_fields)] +pub struct CpuidEntry { + /// The leaf (function) number for this entry. + pub leaf: u32, + + /// The subleaf (index) number for this entry, if it uses subleaves. + pub subleaf: Option, + + /// The value to return in eax. + pub eax: u32, + + /// The value to return in ebx. + pub ebx: u32, + + /// The value to return in ecx. + pub ecx: u32, + + /// The value to return in edx. + pub edx: u32, +} + +/// Flags that enable "simple" Hyper-V enlightenments that require no +/// feature-specific configuration. +// +// NOTE: This enum's variants should never have any associated data (note that +// the type doesn't use serde's `tag` and `content` attributes). If a future +// enlightenment requires associated data, it should be put into a +// `HyperVExtendedFeatures` struct (or similar), and the `HyperV` variant of +// `GuestHypervisorInterface` should be extended to `Option`ally include that +// struct. +#[derive( + Clone, + Deserialize, + Serialize, + Debug, + JsonSchema, + Ord, + PartialOrd, + Eq, + PartialEq, +)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum HyperVFeatureFlag { + ReferenceTsc, +} + +/// A hypervisor interface to expose to the guest. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] +#[serde( + deny_unknown_fields, + rename_all = "snake_case", + tag = "type", + content = "value" +)] +pub enum GuestHypervisorInterface { + /// Expose a bhyve-like interface ("bhyve bhyve " as the hypervisor ID in + /// leaf 0x4000_0000 and no additional leaves or features). + #[default] + Bhyve, + + /// Expose a Hyper-V-compatible hypervisor interface with the supplied + /// features enabled. + HyperV { features: BTreeSet }, +} + +impl GuestHypervisorInterface { + pub(crate) fn is_default(&self) -> bool { + matches!(self, Self::Bhyve) + } +} + +/// A VM's mainboard. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Board { + /// The number of virtual logical processors attached to this VM. + pub cpus: u8, + + /// The amount of guest RAM attached to this VM. + pub memory_mb: u64, + + /// The chipset to expose to guest software. + pub chipset: Chipset, + + /// The hypervisor platform to expose to the guest. The default is a + /// bhyve-compatible interface with no additional features. + /// + /// For compatibility with older versions of Propolis, this field is only + /// serialized if it specifies a non-default interface. + #[serde( + default, + skip_serializing_if = "GuestHypervisorInterface::is_default" + )] + pub guest_hv_interface: GuestHypervisorInterface, + + /// The CPUID values to expose to the guest. If `None`, bhyve will derive + /// default values from the host's CPUID values. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cpuid: Option, + // TODO: Processor and NUMA topology. +} diff --git a/crates/propolis-api-types-versions/src/initial/components/devices.rs b/crates/propolis-api-types-versions/src/initial/components/devices.rs new file mode 100644 index 000000000..4c2e3f2e2 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/components/devices.rs @@ -0,0 +1,206 @@ +// 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: components that define VM properties that are +//! visible to a VM's guest software. + +use crate::v1::instance_spec::{PciPath, SpecKey}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// A disk that presents a virtio-block interface to the guest. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct VirtioDisk { + /// 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, +} + +/// 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. + pub serial_number: [u8; 20], +} + +/// A network card that presents a virtio-net interface to the guest. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct VirtioNic { + /// The name of the device's backend. + pub backend_id: SpecKey, + + /// 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. + pub interface_id: uuid::Uuid, + + /// The PCI path at which to attach this device. + pub pci_path: PciPath, +} + +/// A serial port identifier, which determines what I/O ports a guest can use to +/// access a port. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, Hash, +)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum SerialPortNumber { + Com1, + Com2, + Com3, + Com4, +} + +/// A serial port device. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(deny_unknown_fields)] +pub struct SerialPort { + /// The serial port number for this port. + pub num: SerialPortNumber, +} + +/// A PCI-PCI bridge. +#[derive( + Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, +)] +#[serde(deny_unknown_fields)] +pub struct PciPciBridge { + /// 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. + pub downstream_bus: u8, + + /// The PCI path at which to attach this bridge. + pub pci_path: PciPath, +} + +#[derive( + Clone, + Copy, + Deserialize, + Serialize, + Debug, + PartialEq, + Eq, + JsonSchema, + Default, +)] +#[serde(deny_unknown_fields)] +pub struct QemuPvpanic { + /// Enable the QEMU PVPANIC ISA bus device (I/O port 0x505). + pub enable_isa: bool, + // TODO(eliza): add support for the PCI PVPANIC device... +} + +/// Settings supplied to the guest's firmware image that specify the order in +/// which it should consider its options when selecting a device to try to boot +/// from. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] +#[serde(deny_unknown_fields)] +pub struct BootSettings { + /// An ordered list of components to attempt to boot from. + pub order: Vec, +} + +/// An entry in the boot order stored in a [`BootSettings`] component. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +pub struct BootOrderEntry { + /// The ID of another component in the spec that Propolis should try to + /// boot from. + /// + /// Currently, only disk device components are supported. + pub id: SpecKey, +} + +// +// Structs for Falcon devices. These devices don't support live migration. +// + +/// Describes a SoftNPU PCI device. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SoftNpuPciPort { + /// The PCI path at which to attach the guest to this port. + pub pci_path: PciPath, +} + +/// Describes a port in a SoftNPU emulated ASIC. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SoftNpuPort { + /// The data link name for this port. + pub link_name: String, + + /// The name of the port's associated DLPI backend. + pub backend_id: SpecKey, +} + +/// Describes a PCI device that shares host files with the guest using the P9 +/// protocol. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SoftNpuP9 { + /// The PCI path at which to attach the guest to this port. + pub pci_path: PciPath, +} + +/// Describes a filesystem to expose through a P9 device. +/// +/// This is only supported by Propolis servers compiled with the `falcon` +/// feature. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct P9fs { + /// The host source path to mount into the guest. + pub source: String, + + /// The 9P target filesystem tag. + pub target: String, + + /// 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. + pub chunk_size: u32, + + /// The PCI path at which to attach the guest to this P9 filesystem. + pub pci_path: PciPath, +} + +/// Describes a synthetic device that registers for VM lifecycle notifications +/// and returns errors during attempts to migrate. +/// +/// This is only supported by Propolis servers compiled with the +/// `failure-injection` feature. +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct MigrationFailureInjector { + /// The number of times this device should fail requests to export state. + pub fail_exports: u32, + + /// The number of times this device should fail requests to import state. + pub fail_imports: u32, +} diff --git a/crates/propolis-api-types-versions/src/initial/components/mod.rs b/crates/propolis-api-types-versions/src/initial/components/mod.rs new file mode 100644 index 000000000..52feea097 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/components/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/. + +//! Specifications for components that can be attached to a Propolis VM. +//! +//! Components are 'versionless' and can be added to any specification of any +//! format. Existing components must only change in backward-compatible ways. + +pub mod backends; +pub mod board; +pub mod devices; diff --git a/crates/propolis-api-types-versions/src/initial/disk.rs b/crates/propolis-api-types-versions/src/initial/disk.rs new file mode 100644 index 000000000..23310f9ac --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/disk.rs @@ -0,0 +1,38 @@ +// 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/. + +//! Disk and volume types for the INITIAL API version. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceVCRReplace { + pub vcr_json: String, +} + +/// Path parameters for snapshot requests. +#[derive(Deserialize, JsonSchema)] +pub struct SnapshotRequestPathParams { + pub id: String, + pub snapshot_id: Uuid, +} + +/// Path parameters for VCR requests. +#[derive(Deserialize, JsonSchema)] +pub struct VCRRequestPathParams { + pub id: String, +} + +/// Path parameters for volume status requests. +#[derive(Deserialize, JsonSchema)] +pub struct VolumeStatusPathParams { + pub id: String, +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct VolumeStatus { + pub active: bool, +} diff --git a/crates/propolis-api-types-versions/src/initial/instance.rs b/crates/propolis-api-types-versions/src/initial/instance.rs new file mode 100644 index 000000000..44d7d08e5 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/instance.rs @@ -0,0 +1,179 @@ +// 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 management types for the INITIAL API version. +//! +//! This module contains types for instance properties, state management, +//! initialization, and monitoring. + +use std::{collections::BTreeMap, net::SocketAddr}; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::components::{backends, devices}; +use super::instance_spec::{InstanceSpec, SpecKey}; +use super::migration::InstanceMigrateInitiateResponse; + +#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] +pub struct InstanceMetadata { + pub silo_id: Uuid, + pub project_id: Uuid, + pub sled_id: Uuid, + pub sled_serial: String, + pub sled_revision: u32, + pub sled_model: String, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct InstanceProperties { + /// Unique identifier for this Instance. + pub id: Uuid, + /// Human-readable name of the Instance. + pub name: String, + /// Free-form text description of an Instance. + pub description: String, + /// Metadata used to track statistics for this Instance. + pub metadata: InstanceMetadata, +} + +/// Current state of an Instance. +#[derive( + Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema, +)] +pub enum InstanceState { + Creating, + Starting, + Running, + Stopping, + Stopped, + Rebooting, + Migrating, + Repairing, + Failed, + Destroyed, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct Instance { + pub properties: InstanceProperties, + pub state: InstanceState, +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceGetResponse { + pub instance: Instance, +} + +/// Requested state of an Instance. +#[derive(Clone, Copy, Deserialize, Serialize, JsonSchema)] +pub struct InstanceStateChange { + pub state: InstanceStateRequested, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema)] +pub enum InstanceStateRequested { + Run, + Stop, + Reboot, +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceStateMonitorRequest { + pub gen: u64, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceStateMonitorResponse { + pub gen: u64, + pub state: InstanceState, + pub migration: super::migration::InstanceMigrateStatusResponse, +} + +/// An instance spec component that should be replaced during a live migration. +// +// When a caller asks Propolis to initialize via live migration, the target VM +// inherits the migration source's current instance spec. For the most part, +// the target can (and indeed in some cases must) use this spec without +// modifying it; this helps Propolis ensure that guest-visible configuration +// remains unchanged when a VM migrates. However, there are some components +// with no guest-visible state that may need to be reconfigured when a VM +// migrates. These include the following: +// +// - Crucible disks: After migrating, the target Propolis presents itself as a +// new client of the Crucible downstairs servers backing the VM's disks. +// Crucible requires the target to present a newer client generation number +// to allow the target to connect. In a full Oxide deployment, these numbers +// are managed by the control plane (i.e. it is not safe for Propolis to +// manage these values directly--new Crucible volume connection information +// must always come from Nexus). +// - Virtio network devices: Each virtio NIC in the guest needs to bind to a +// named VNIC object on the host. These names can change when a VM migrates +// from host to host. +// +// Each component that can be reconfigured this way has a variant in this enum; +// components not in the enum can't be reconfigured during migration. This +// saves the initialization API from having to reason about requests to replace +// a component that can't legally be replaced. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, tag = "component", content = "spec")] +pub enum ReplacementComponent { + MigrationFailureInjector(devices::MigrationFailureInjector), + CrucibleStorageBackend(backends::CrucibleStorageBackend), + VirtioNetworkBackend(backends::VirtioNetworkBackend), +} + +#[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, +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceEnsureResponse { + pub migrate: Option, +} + +/// Path parameters for instance-related endpoints using name. +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceNameParams { + pub instance_id: String, +} + +/// Path parameters for instance-related endpoints using UUID. +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstancePathParams { + pub instance_id: Uuid, +} + +/// Error codes used to populate the `error_code` field of Dropshot API responses. +#[derive( + Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema, +)] +pub enum ErrorCode { + /// This `propolis-server` process has not received an `InstanceEnsure` + /// request yet. + NoInstance, + /// This `propolis-server` process has already received an `InstanceEnsure` + /// request with a different ID. + AlreadyInitialized, + /// Cannot update a running server. + AlreadyRunning, + /// Instance creation failed + CreateFailed, +} diff --git a/crates/propolis-api-types-versions/src/initial/instance_spec.rs b/crates/propolis-api-types-versions/src/initial/instance_spec.rs new file mode 100644 index 000000000..cbfe6087e --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/instance_spec.rs @@ -0,0 +1,192 @@ +// 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 INITIAL API version. + +use std::collections::BTreeMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +pub use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor, PciPath}; + +use super::components::{backends, board, devices}; + +/// A key identifying a component in an instance spec. +// +// Some of the components Omicron attaches to Propolis VMs, like network +// interfaces and Crucible disks, are described by database records with UUID +// primary keys. It's natural to reuse these UUIDs as component identifiers in +// Propolis, especially because it lets Omicron functions that need to identify +// a specific component (e.g. a specific Crucible backend that should handle a +// disk snapshot request) pass that component's ID directly to Propolis. +// +// In some cases it's not desirable or possible to use UUIDs this way: +// +// - Some components (like the cloud-init disk) don't have their own rows in the +// database and so don't have obvious UUIDs to use. +// - Some objects (like Crucible disks) require both a device and a backend +// component in the spec, and these can't share the same key. +// - Propolis users outside the control plane may not have any component UUIDs +// at all and may just want to use strings to identify all their components. +// +// For these reasons, the key type may be represented as either a UUID or a +// String. This allows the more compact, more-easily-compared UUID format to be +// used wherever it is practical while still allowing callers to use strings as +// names if they have no UUIDs available or the most obvious UUID is in use +// elsewhere. The key type's From impls will try to parse strings into UUIDs +// before storing keys as strings. +#[derive( + Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, +)] +// Direct serde to use an untagged enum representation for this type. Since both +// Uuid and String serialize to strings, this allows other types that contain a +// Map to derive Serialize and successfully serialize to JSON. +// (This doesn't work with a tagged representation because JSON doesn't allow +// maps to be used as map keys.) +// +// Note that this makes the order of variants matter: serde will pick the first +// variant into which it can successfully deserialize an untagged enum value, +// and the point is to use the UUID representation for any value that can be +// interpreted as a UUID. +#[serde(untagged)] +pub enum SpecKey { + Uuid(Uuid), + Name(String), +} + +// Manually implement JsonSchema to help Progenitor generate the expected enum +// type for spec keys. +impl JsonSchema for SpecKey { + fn schema_name() -> String { + "SpecKey".to_owned() + } + + fn json_schema( + generator: &mut schemars::gen::SchemaGenerator, + ) -> schemars::schema::Schema { + use schemars::schema::*; + fn label_schema(label: &str, schema: Schema) -> Schema { + SchemaObject { + metadata: Some( + Metadata { + title: Some(label.to_string()), + ..Default::default() + } + .into(), + ), + subschemas: Some( + SubschemaValidation { + all_of: Some(vec![schema]), + ..Default::default() + } + .into(), + ), + ..Default::default() + } + .into() + } + + SchemaObject { + metadata: Some( + Metadata { + description: Some( + "A key identifying a component in an instance spec." + .to_string(), + ), + ..Default::default() + } + .into(), + ), + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + label_schema("uuid", generator.subschema_for::()), + label_schema("name", generator.subschema_for::()), + ]), + ..Default::default() + })), + ..Default::default() + } + .into() + } +} + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde( + deny_unknown_fields, + tag = "type", + content = "component", + rename_all = "snake_case" +)] +#[schemars(rename = "ComponentV0")] +pub enum Component { + VirtioDisk(devices::VirtioDisk), + NvmeDisk(devices::NvmeDisk), + VirtioNic(devices::VirtioNic), + SerialPort(devices::SerialPort), + PciPciBridge(devices::PciPciBridge), + QemuPvpanic(devices::QemuPvpanic), + BootSettings(devices::BootSettings), + SoftNpuPciPort(devices::SoftNpuPciPort), + SoftNpuPort(devices::SoftNpuPort), + SoftNpuP9(devices::SoftNpuP9), + P9fs(devices::P9fs), + MigrationFailureInjector(devices::MigrationFailureInjector), + CrucibleStorageBackend(backends::CrucibleStorageBackend), + FileStorageBackend(backends::FileStorageBackend), + BlobStorageBackend(backends::BlobStorageBackend), + VirtioNetworkBackend(backends::VirtioNetworkBackend), + DlpiNetworkBackend(backends::DlpiNetworkBackend), +} + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct InstanceSpec { + pub board: board::Board, + pub components: BTreeMap, +} + +/// DEPRECATED: A versioned instance spec. +/// +/// This structure is deprecated. It is notionally incompatible with +/// dropshot API versioning. If you wanted to add a `V1` variant to +/// `VersionedInstanceSpec`, it would change the existing blessed V1 OpenAPI +/// spec. Therefore you'd have to rename this to `VersionedInstanceSpecV0` and +/// create a new type with the new variant. This makes little sense however, +/// and is a remnant of an attempt at propolis versioning prior to dropshot +/// API versioning. +/// +/// Luckily this type is only exposed via `InstanceSpecGetResponse` which is not +/// used in Omicron. Therefore we can limit that method to the V1 OpenAPI spec +/// and stop any further use of this type. +/// +/// In addition to the disparate versioning mechanisms, there is also a +/// fundamental flaw in how this type was used in the existing code. It was +/// constructed in some cases from a `MaybeSpec` which contains a `Box`. +/// Unfortunately, `Spec` is a type erased container of any version of an +/// instance spec such as `InstanceSpecV0`,`InstanceSpecV1`, or future types. +/// There is no guarantee that we could take a `Spec` and figure out which +/// versioned spec it is supposed to convert to. This only worked in the initial +/// code because the only versioned spec was `InstanceSpecV0`. It's best to stop +/// using this type altogether. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, tag = "version", content = "spec")] +pub enum VersionedInstanceSpec { + V0(InstanceSpec), +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +#[serde(tag = "type", content = "value")] +pub enum InstanceSpecStatus { + WaitingForMigrationSource, + Present(VersionedInstanceSpec), +} + +#[derive(Clone, Deserialize, Serialize, JsonSchema)] +pub struct InstanceSpecGetResponse { + pub properties: super::instance::InstanceProperties, + pub state: super::instance::InstanceState, + pub spec: InstanceSpecStatus, +} diff --git a/crates/propolis-api-types-versions/src/initial/migration.rs b/crates/propolis-api-types-versions/src/initial/migration.rs new file mode 100644 index 000000000..dbbe23b06 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/migration.rs @@ -0,0 +1,88 @@ +// 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/. + +//! Migration types for the INITIAL API version. + +use std::net::SocketAddr; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +/// Request to initiate a migration. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceMigrateInitiateRequest { + pub migration_id: Uuid, + pub src_addr: SocketAddr, + pub src_uuid: Uuid, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceMigrateInitiateResponse { + pub migration_id: Uuid, +} + +/// Request to start a migration. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceMigrateStartRequest { + pub migration_id: Uuid, +} + +/// The status of an individual live migration. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct InstanceMigrationStatus { + /// The ID of this migration, supplied either by the external migration + /// requester (for targets) or the other side of the migration (for + /// sources). + pub id: Uuid, + /// The current phase the migration is in. + pub state: MigrationState, +} + +/// The statuses of the most recent attempts to live migrate into and out of +/// this Propolis. +/// +/// If 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. +/// +/// This 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. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct InstanceMigrateStatusResponse { + /// 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. + pub migration_in: Option, + /// 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. + pub migration_out: Option, +} + +#[derive( + Clone, + Copy, + Debug, + Deserialize, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + JsonSchema, +)] +pub enum MigrationState { + Sync, + RamPush, + Pause, + RamPushDirty, + Device, + Resume, + RamPull, + Server, + Finish, + Error, +} diff --git a/crates/propolis-api-types-versions/src/initial/mod.rs b/crates/propolis-api-types-versions/src/initial/mod.rs new file mode 100644 index 000000000..c6e1c7525 --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/mod.rs @@ -0,0 +1,15 @@ +// 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 `INITIAL` of the Propolis Server API. +//! +//! This is the first version of the API, supporting basic VM operations +//! without programmable SMBIOS. + +pub mod components; +pub mod disk; +pub mod instance; +pub mod instance_spec; +pub mod migration; +pub mod serial; diff --git a/crates/propolis-api-types-versions/src/initial/serial.rs b/crates/propolis-api-types-versions/src/initial/serial.rs new file mode 100644 index 000000000..d1ba0962c --- /dev/null +++ b/crates/propolis-api-types-versions/src/initial/serial.rs @@ -0,0 +1,69 @@ +// 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/. + +//! Serial console types for the INITIAL API version. + +use std::net::SocketAddr; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Request a specific range of an Instance's serial console output history. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct InstanceSerialConsoleHistoryRequest { + /// 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. + pub from_start: Option, + /// 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) + pub most_recent: Option, + /// 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`. + pub max_bytes: Option, +} + +/// Contents of an Instance's serial console buffer. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +pub struct InstanceSerialConsoleHistoryResponse { + /// 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. + pub data: Vec, + /// The absolute offset since boot (suitable for use as `byte_offset` in a + /// subsequent request) of the last byte returned in `data`. + pub last_byte_offset: u64, +} + +/// Connect to an Instance's serial console via websocket, optionally sending +/// bytes from the buffered history first. +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct InstanceSerialConsoleStreamRequest { + /// 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. + // TODO: if neither is specified, send enough serial buffer history to + // reconstruct the current contents and cursor state of an interactive + // terminal + pub from_start: Option, + /// 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) + pub most_recent: Option, +} + +/// Control message(s) sent through the websocket to serial console clients. +/// +/// Note: Because this is associated with the websocket, and not some REST +/// endpoint, Dropshot lacks the ability to communicate it via the OpenAPI +/// document underpinning the exposed interfaces. As such, clients (including +/// the `propolis-client` crate) are expected to define their own identical copy +/// of this type in order to consume it. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum InstanceSerialConsoleControlMessage { + Migrating { destination: SocketAddr, from_start: u64 }, +} diff --git a/crates/propolis-api-types-versions/src/latest.rs b/crates/propolis-api-types-versions/src/latest.rs new file mode 100644 index 000000000..802663468 --- /dev/null +++ b/crates/propolis-api-types-versions/src/latest.rs @@ -0,0 +1,104 @@ +// 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/. + +//! Re-exports of the latest versions of all published types. +//! +//! Business logic should use these re-exports rather than versioned +//! identifiers directly. + +pub mod components { + pub mod backends { + pub use crate::v1::components::backends::BlobStorageBackend; + pub use crate::v1::components::backends::CrucibleStorageBackend; + pub use crate::v1::components::backends::DlpiNetworkBackend; + pub use crate::v1::components::backends::FileStorageBackend; + pub use crate::v1::components::backends::VirtioNetworkBackend; + } + + pub mod board { + pub use crate::v1::components::board::Board; + pub use crate::v1::components::board::Chipset; + pub use crate::v1::components::board::Cpuid; + pub use crate::v1::components::board::CpuidEntry; + pub use crate::v1::components::board::GuestHypervisorInterface; + pub use crate::v1::components::board::HyperVFeatureFlag; + pub use crate::v1::components::board::I440Fx; + } + + pub mod devices { + 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; + pub use crate::v1::components::devices::SerialPort; + pub use crate::v1::components::devices::SerialPortNumber; + pub use crate::v1::components::devices::SoftNpuP9; + pub use crate::v1::components::devices::SoftNpuPciPort; + pub use crate::v1::components::devices::SoftNpuPort; + pub use crate::v1::components::devices::VirtioDisk; + pub use crate::v1::components::devices::VirtioNic; + } +} + +pub mod disk { + pub use crate::v1::disk::InstanceVCRReplace; + pub use crate::v1::disk::SnapshotRequestPathParams; + pub use crate::v1::disk::VCRRequestPathParams; + pub use crate::v1::disk::VolumeStatus; + pub use crate::v1::disk::VolumeStatusPathParams; +} + +pub mod instance { + pub use crate::v1::instance::ErrorCode; + pub use crate::v1::instance::Instance; + pub use crate::v1::instance::InstanceEnsureResponse; + pub use crate::v1::instance::InstanceGetResponse; + pub use crate::v1::instance::InstanceMetadata; + pub use crate::v1::instance::InstanceNameParams; + pub use crate::v1::instance::InstancePathParams; + pub use crate::v1::instance::InstanceProperties; + pub use crate::v1::instance::InstanceState; + pub use crate::v1::instance::InstanceStateChange; + pub use crate::v1::instance::InstanceStateMonitorRequest; + pub use crate::v1::instance::InstanceStateMonitorResponse; + 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 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; + pub use crate::v1::instance_spec::PciPath; + 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 mod migration { + pub use crate::v1::migration::InstanceMigrateInitiateRequest; + pub use crate::v1::migration::InstanceMigrateInitiateResponse; + pub use crate::v1::migration::InstanceMigrateStartRequest; + pub use crate::v1::migration::InstanceMigrateStatusResponse; + pub use crate::v1::migration::InstanceMigrationStatus; + pub use crate::v1::migration::MigrationState; +} + +pub mod serial { + pub use crate::v1::serial::InstanceSerialConsoleControlMessage; + pub use crate::v1::serial::InstanceSerialConsoleHistoryRequest; + pub use crate::v1::serial::InstanceSerialConsoleHistoryResponse; + pub use crate::v1::serial::InstanceSerialConsoleStreamRequest; +} diff --git a/crates/propolis-api-types-versions/src/lib.rs b/crates/propolis-api-types-versions/src/lib.rs new file mode 100644 index 000000000..6c3673ee8 --- /dev/null +++ b/crates/propolis-api-types-versions/src/lib.rs @@ -0,0 +1,36 @@ +// 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/. + +//! Versioned types for the Propolis Server API. +//! +//! # Adding a new API version +//! +//! When adding a new API version N with added or changed types: +//! +//! 1. Create /mod.rs, where is the lowercase +//! form of the new version's identifier, as defined in the API trait's +//! `api_versions!` macro. +//! +//! 2. Add to the end of this list: +//! +//! ```rust,ignore +//! #[path = "/mod.rs"] +//! pub mod vN; +//! ``` +//! +//! 3. Add your types to the new module, mirroring the module structure from +//! earlier versions. +//! +//! 4. Update `latest.rs` with new and updated types from the new version. +//! +//! For more information, see [RFD 619]. +//! +//! [RFD 619]: https://rfd.shared.oxide.computer/rfd/619 + +mod impls; +pub mod latest; +#[path = "initial/mod.rs"] +pub mod v1; +#[path = "programmable_smbios/mod.rs"] +pub mod v2; diff --git a/crates/propolis-api-types-versions/src/programmable_smbios/api.rs b/crates/propolis-api-types-versions/src/programmable_smbios/api.rs new file mode 100644 index 000000000..7f1553c71 --- /dev/null +++ b/crates/propolis-api-types-versions/src/programmable_smbios/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 PROGRAMMABLE_SMBIOS 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; +use crate::v1::instance::{InstanceProperties, ReplacementComponent}; +use crate::v1::instance_spec::SpecKey; + +#[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: v1::instance::InstanceInitializationMethod) -> Self { + match old { + v1::instance::InstanceInitializationMethod::Spec { spec } => { + Self::Spec { spec: spec.into() } + } + v1::instance::InstanceInitializationMethod::MigrationTarget { + migration_id, + src_addr, + replace_components, + } => Self::MigrationTarget { + migration_id, + src_addr, + replace_components, + }, + } + } +} + +impl From for InstanceEnsureRequest { + fn from(old: v1::instance::InstanceEnsureRequest) -> Self { + Self { properties: old.properties, init: old.init.into() } + } +} diff --git a/crates/propolis-api-types-versions/src/programmable_smbios/instance_spec.rs b/crates/propolis-api-types-versions/src/programmable_smbios/instance_spec.rs new file mode 100644 index 000000000..afa73a05f --- /dev/null +++ b/crates/propolis-api-types-versions/src/programmable_smbios/instance_spec.rs @@ -0,0 +1,82 @@ +// 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 PROGRAMMABLE_SMBIOS API version. + +use std::collections::BTreeMap; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::v1; +use crate::v1::components::board; +use crate::v1::instance::{InstanceProperties, InstanceState}; +use crate::v1::instance_spec::{Component, SpecKey}; + +#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SmbiosType1Input { + pub manufacturer: String, + pub product_name: String, + pub serial_number: String, + pub version: u64, +} + +#[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, +} + +impl From for v1::instance_spec::InstanceSpec { + fn from(new: InstanceSpec) -> Self { + Self { board: new.board, components: new.components } + } +} + +impl From for v1::instance_spec::InstanceSpecStatus { + fn from(new: InstanceSpecStatus) -> Self { + match new { + InstanceSpecStatus::WaitingForMigrationSource => { + Self::WaitingForMigrationSource + } + InstanceSpecStatus::Present(spec) => Self::Present( + v1::instance_spec::VersionedInstanceSpec::V0(spec.into()), + ), + } + } +} + +impl From + for v1::instance_spec::InstanceSpecGetResponse +{ + fn from(new: InstanceSpecGetResponse) -> Self { + Self { + properties: new.properties, + state: new.state, + spec: new.spec.into(), + } + } +} + +impl From for InstanceSpec { + fn from(old: v1::instance_spec::InstanceSpec) -> Self { + Self { board: old.board, components: old.components, smbios: None } + } +} diff --git a/crates/propolis-api-types-versions/src/programmable_smbios/mod.rs b/crates/propolis-api-types-versions/src/programmable_smbios/mod.rs new file mode 100644 index 000000000..bc9a78c3a --- /dev/null +++ b/crates/propolis-api-types-versions/src/programmable_smbios/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 `PROGRAMMABLE_SMBIOS` of the Propolis Server API. +//! +//! This version adds support for programmable SMBIOS Type 1 tables, +//! allowing custom manufacturer, product name, serial number, and version +//! fields to be set on VMs. + +pub mod api; +pub mod instance_spec; diff --git a/crates/propolis-api-types/Cargo.toml b/crates/propolis-api-types/Cargo.toml index e251195dc..c56effc84 100644 --- a/crates/propolis-api-types/Cargo.toml +++ b/crates/propolis-api-types/Cargo.toml @@ -9,11 +9,4 @@ doctest = false [dependencies] crucible-client-types.workspace = true -propolis_types.workspace = true -schemars.workspace = true -serde.workspace = true -thiserror.workspace = true -uuid.workspace = true - -[dev-dependencies] -serde_json.workspace = true +propolis-api-types-versions.workspace = true diff --git a/crates/propolis-api-types/src/disk.rs b/crates/propolis-api-types/src/disk.rs new file mode 100644 index 000000000..59397a041 --- /dev/null +++ b/crates/propolis-api-types/src/disk.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/. + +//! Disk-related types. + +pub use propolis_api_types_versions::latest::disk::*; diff --git a/crates/propolis-api-types/src/instance.rs b/crates/propolis-api-types/src/instance.rs new file mode 100644 index 000000000..2f07bf910 --- /dev/null +++ b/crates/propolis-api-types/src/instance.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/. + +//! Instance management types. + +pub use propolis_api_types_versions::latest::instance::*; diff --git a/crates/propolis-api-types/src/instance_spec/components/backends.rs b/crates/propolis-api-types/src/instance_spec/components/backends.rs index 0a711faab..624df37c7 100644 --- a/crates/propolis-api-types/src/instance_spec/components/backends.rs +++ b/crates/propolis-api-types/src/instance_spec/components/backends.rs @@ -2,93 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Backend configuration data: the structs that tell Propolis how to configure -//! its components to talk to other services supplied by the host OS or the -//! larger rack. +//! Storage and network backend specifications. -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::num::NonZeroUsize; - -/// A Crucible storage backend. -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct CrucibleStorageBackend { - /// 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. - /// - /// When 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. - pub request_json: String, - - /// Indicates whether the storage is read-only. - pub readonly: bool, -} - -impl std::fmt::Debug for CrucibleStorageBackend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Redact the contents of the VCR since they may contain volume - // encryption keys. - f.debug_struct("CrucibleStorageBackend") - .field("request_json", &"".to_string()) - .field("readonly", &self.readonly) - .finish() - } -} - -/// A storage backend backed by a file in the host system's file system. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct FileStorageBackend { - /// A path to a file that backs a disk. - pub path: String, - - /// Indicates whether the storage is read-only. - pub readonly: bool, - - /// Block size of the backend - pub block_size: u32, - - /// Optional worker threads for the file backend, exposed for testing only. - pub workers: Option, -} - -/// A storage backend for a disk whose initial contents are given explicitly -/// by the specification. -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct BlobStorageBackend { - /// The disk's initial contents, encoded as a base64 string. - pub base64: String, - - /// Indicates whether the storage is read-only. - pub readonly: bool, -} - -impl std::fmt::Debug for BlobStorageBackend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BlobStorageBackend") - .field("base64", &"".to_string()) - .field("readonly", &self.readonly) - .finish() - } -} - -/// A network backend associated with a virtio-net (viona) VNIC on the host. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct VirtioNetworkBackend { - /// The name of the viona VNIC to use as a backend. - pub vnic_name: String, -} - -/// A network backend associated with a DLPI VNIC on the host. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct DlpiNetworkBackend { - /// The name of the VNIC to use as a backend. - pub vnic_name: String, -} +pub use propolis_api_types_versions::latest::components::backends::*; diff --git a/crates/propolis-api-types/src/instance_spec/components/board.rs b/crates/propolis-api-types/src/instance_spec/components/board.rs index 9295296e6..107286f97 100644 --- a/crates/propolis-api-types/src/instance_spec/components/board.rs +++ b/crates/propolis-api-types/src/instance_spec/components/board.rs @@ -2,181 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! VM mainboard components. Every VM has a board, even if it has no other -//! peripherals. +//! Board-level specifications. -use std::collections::BTreeSet; - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::instance_spec::CpuidVendor; - -/// An Intel 440FX-compatible chipset. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, -)] -#[serde(deny_unknown_fields)] -pub struct I440Fx { - /// Specifies whether the chipset should allow PCI configuration space - /// to be accessed through the PCIe extended configuration mechanism. - pub enable_pcie: bool, -} - -/// A kind of virtual chipset. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, -)] -#[serde( - deny_unknown_fields, - rename_all = "snake_case", - tag = "type", - content = "value" -)] -pub enum Chipset { - /// An Intel 440FX-compatible chipset. - I440Fx(I440Fx), -} - -impl Default for Chipset { - fn default() -> Self { - Self::I440Fx(I440Fx { enable_pcie: false }) - } -} - -/// A set of CPUID values to expose to a guest. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct Cpuid { - /// A list of CPUID leaves/subleaves and their associated values. - /// - /// Propolis 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. - // - // It would be nice if this were an associative collection type. - // Unfortunately, the most natural keys for such a collection are - // structs or tuples, and JSON doesn't allow objects to be used as - // property names. Instead of converting leaf/subleaf pairs to and from - // strings, just accept a flat Vec and have servers verify that e.g. no - // leaf/subleaf pairs are duplicated. - pub entries: Vec, - - /// The CPU vendor to emulate. - /// - /// CPUID 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. - pub vendor: CpuidVendor, -} - -/// A full description of a CPUID leaf/subleaf and the values it produces. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, -)] -#[serde(deny_unknown_fields)] -pub struct CpuidEntry { - /// The leaf (function) number for this entry. - pub leaf: u32, - - /// The subleaf (index) number for this entry, if it uses subleaves. - pub subleaf: Option, - - /// The value to return in eax. - pub eax: u32, - - /// The value to return in ebx. - pub ebx: u32, - - /// The value to return in ecx. - pub ecx: u32, - - /// The value to return in edx. - pub edx: u32, -} - -/// Flags that enable "simple" Hyper-V enlightenments that require no -/// feature-specific configuration. -// -// NOTE: This enum's variants should never have any associated data (note that -// the type doesn't use serde's `tag` and `content` attributes). If a future -// enlightenment requires associated data, it should be put into a -// `HyperVExtendedFeatures` struct (or similar), and the `HyperV` variant of -// `GuestHypervisorInterface` should be extended to `Option`ally include that -// struct. -#[derive( - Clone, - Deserialize, - Serialize, - Debug, - JsonSchema, - Ord, - PartialOrd, - Eq, - PartialEq, -)] -#[serde(deny_unknown_fields, rename_all = "snake_case")] -pub enum HyperVFeatureFlag { - ReferenceTsc, -} - -/// A hypervisor interface to expose to the guest. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] -#[serde( - deny_unknown_fields, - rename_all = "snake_case", - tag = "type", - content = "value" -)] -pub enum GuestHypervisorInterface { - /// Expose a bhyve-like interface ("bhyve bhyve " as the hypervisor ID in - /// leaf 0x4000_0000 and no additional leaves or features). - #[default] - Bhyve, - - /// Expose a Hyper-V-compatible hypervisor interface with the supplied - /// features enabled. - HyperV { features: BTreeSet }, -} - -impl GuestHypervisorInterface { - fn is_default(&self) -> bool { - matches!(self, Self::Bhyve) - } -} - -/// A VM's mainboard. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct Board { - /// The number of virtual logical processors attached to this VM. - pub cpus: u8, - - /// The amount of guest RAM attached to this VM. - pub memory_mb: u64, - - /// The chipset to expose to guest software. - pub chipset: Chipset, - - /// The hypervisor platform to expose to the guest. The default is a - /// bhyve-compatible interface with no additional features. - /// - /// For compatibility with older versions of Propolis, this field is only - /// serialized if it specifies a non-default interface. - #[serde( - default, - skip_serializing_if = "GuestHypervisorInterface::is_default" - )] - pub guest_hv_interface: GuestHypervisorInterface, - - /// The CPUID values to expose to the guest. If `None`, bhyve will derive - /// default values from the host's CPUID values. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cpuid: Option, - // TODO: Processor and NUMA topology. -} +pub use propolis_api_types_versions::latest::components::board::*; diff --git a/crates/propolis-api-types/src/instance_spec/components/devices.rs b/crates/propolis-api-types/src/instance_spec/components/devices.rs index 7bb2d9fc5..3035ad16e 100644 --- a/crates/propolis-api-types/src/instance_spec/components/devices.rs +++ b/crates/propolis-api-types/src/instance_spec/components/devices.rs @@ -2,205 +2,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Device configuration data: components that define VM properties that are -//! visible to a VM's guest software. +//! Device specifications. -use crate::instance_spec::{PciPath, SpecKey}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// A disk that presents a virtio-block interface to the guest. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct VirtioDisk { - /// 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, -} - -/// 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. - pub serial_number: [u8; 20], -} - -/// A network card that presents a virtio-net interface to the guest. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct VirtioNic { - /// The name of the device's backend. - pub backend_id: SpecKey, - - /// 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. - pub interface_id: uuid::Uuid, - - /// The PCI path at which to attach this device. - pub pci_path: PciPath, -} - -/// A serial port identifier, which determines what I/O ports a guest can use to -/// access a port. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, Hash, -)] -#[serde(deny_unknown_fields, rename_all = "snake_case")] -pub enum SerialPortNumber { - Com1, - Com2, - Com3, - Com4, -} - -/// A serial port device. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, -)] -#[serde(deny_unknown_fields)] -pub struct SerialPort { - /// The serial port number for this port. - pub num: SerialPortNumber, -} - -/// A PCI-PCI bridge. -#[derive( - Clone, Copy, Deserialize, Serialize, Debug, PartialEq, Eq, JsonSchema, -)] -#[serde(deny_unknown_fields)] -pub struct PciPciBridge { - /// 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. - pub downstream_bus: u8, - - /// The PCI path at which to attach this bridge. - pub pci_path: PciPath, -} - -#[derive( - Clone, - Copy, - Deserialize, - Serialize, - Debug, - PartialEq, - Eq, - JsonSchema, - Default, -)] -#[serde(deny_unknown_fields)] -pub struct QemuPvpanic { - /// Enable the QEMU PVPANIC ISA bus device (I/O port 0x505). - pub enable_isa: bool, - // TODO(eliza): add support for the PCI PVPANIC device... -} - -/// Settings supplied to the guest's firmware image that specify the order in -/// which it should consider its options when selecting a device to try to boot -/// from. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)] -#[serde(deny_unknown_fields)] -pub struct BootSettings { - /// An ordered list of components to attempt to boot from. - pub order: Vec, -} - -/// An entry in the boot order stored in a [`BootSettings`] component. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -pub struct BootOrderEntry { - /// The ID of another component in the spec that Propolis should try to - /// boot from. - /// - /// Currently, only disk device components are supported. - pub id: SpecKey, -} - -// -// Structs for Falcon devices. These devices don't support live migration. -// - -/// Describes a SoftNPU PCI device. -/// -/// This is only supported by Propolis servers compiled with the `falcon` -/// feature. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct SoftNpuPciPort { - /// The PCI path at which to attach the guest to this port. - pub pci_path: PciPath, -} - -/// Describes a port in a SoftNPU emulated ASIC. -/// -/// This is only supported by Propolis servers compiled with the `falcon` -/// feature. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct SoftNpuPort { - /// The data link name for this port. - pub link_name: String, - - /// The name of the port's associated DLPI backend. - pub backend_id: SpecKey, -} - -/// Describes a PCI device that shares host files with the guest using the P9 -/// protocol. -/// -/// This is only supported by Propolis servers compiled with the `falcon` -/// feature. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct SoftNpuP9 { - /// The PCI path at which to attach the guest to this port. - pub pci_path: PciPath, -} - -/// Describes a filesystem to expose through a P9 device. -/// -/// This is only supported by Propolis servers compiled with the `falcon` -/// feature. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct P9fs { - /// The host source path to mount into the guest. - pub source: String, - - /// The 9P target filesystem tag. - pub target: String, - - /// 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. - pub chunk_size: u32, - - /// The PCI path at which to attach the guest to this P9 filesystem. - pub pci_path: PciPath, -} - -/// Describes a synthetic device that registers for VM lifecycle notifications -/// and returns errors during attempts to migrate. -/// -/// This is only supported by Propolis servers compiled with the -/// `failure-injection` feature. -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct MigrationFailureInjector { - /// The number of times this device should fail requests to export state. - pub fail_exports: u32, - - /// The number of times this device should fail requests to import state. - pub fail_imports: u32, -} +pub use propolis_api_types_versions::latest::components::devices::*; diff --git a/crates/propolis-api-types/src/instance_spec/components/mod.rs b/crates/propolis-api-types/src/instance_spec/components/mod.rs index ef5eb9071..fbf32f7b0 100644 --- a/crates/propolis-api-types/src/instance_spec/components/mod.rs +++ b/crates/propolis-api-types/src/instance_spec/components/mod.rs @@ -2,18 +2,7 @@ // 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/. -//! Specifications for components that can be attached to a Propolis VM. -//! -//! # Versioning and compatibility -//! -//! Components are 'versionless' and can be added to any specification of any -//! format. Existing components must only change in backward-compatible ways -//! (i.e. so that old versions of the component can deserialize into an -//! equivalent new-version component). If possible, changes to a new component -//! should be expressed such that older versions of the component are forward- -//! compatible with the new version (i.e. such that the new component will -//! serialize, if possible, into a form that can be deserialized by an old -//! version of this library into an equivalent old-version component). +//! Component specifications that can be attached to a VM. pub mod backends; pub mod board; diff --git a/crates/propolis-api-types/src/instance_spec/mod.rs b/crates/propolis-api-types/src/instance_spec/mod.rs index 71b9079a6..2ffa3f85c 100644 --- a/crates/propolis-api-types/src/instance_spec/mod.rs +++ b/crates/propolis-api-types/src/instance_spec/mod.rs @@ -2,384 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -//! Instance specifications: abstract descriptions of a VM's devices and config. -//! -//! An instance spec describes a VM's virtual devices, backends, and other -//! guest environment configuration supplied by the Propolis VMM. RFD 283 -//! contains more details about how specs are used throughout the Oxide stack -//! and about the versioning considerations described below. -//! -//! # Module layout -//! -//! The data types in this module are taxonomized into "components" and -//! versioned "spec structures." Components are the "leaves" of a spec; each -//! component specifies an individual component or piece of functionality that -//! can be added to a VM. The strongly versioned structure types arrange these -//! components in a specific way; each organization is a version of the overall -//! instance spec structure. -//! -//! # Versioning & compatibility -//! -//! Instance specs may be sent between Propolises, sled agents, and Nexus -//! processes that use different versions of this library, so the library needs -//! to provide a versioning scheme that allows specs to be extended over time. -//! Such scheme must balance safety against developer toil. Strongly versioning -//! data types--requiring a new API endpoint or type definition every time -//! something changes--minimizes the risk that a data structure will be -//! misinterpreted, but is very toilsome to maintain, since changing one -//! structure may require many other structures to be revised and -//! `From`/`TryFrom` impls to be added for all the new version combinations. -//! Weaker versioning schemes require less toil to maintain but run the risk -//! that a spec user will be too permissive and will misconfigure a VM because -//! it missed some important context in a -//! spec that it was passed. -//! -//! This module balances these concerns as follows: -//! -//! - **Components** are versionless but are allowed to be extended in backward- -//! compatible ways (i.e., such that a spec produced by an old library can be -//! interpreted correctly by a newer library). Breaking changes to components -//! are not allowed and require a new component to be defined. -//! - **Spec structures** are strongly versioned. Backward-compatible changes to -//! an existing version are technically allowed, but completely restructuring -//! a spec requires a new spec version and a corresponding variant in the -//! `VersionedInstanceSpec` structure. -//! -//! This scheme assumes that (a) components are likely to be added or changed -//! much more frequently than the spec structure itself will be revised, and (b) -//! most changes to existing components can easily be made backward-compatible -//! (e.g. by wrapping new functionality in an `Option` and taking a `None` value -//! to mean "do what all previous versions did"). -//! -//! ## Compatibility rules & breaking changes -//! -//! Changes to existing data types must be backward compatible with older spec -//! versions: a spec produced by an old version of the library must always be -//! deserializable by a new version of the library. -//! -//! The following component changes are not backward compatible: -//! -//! - Adding a new required field to a struct or enum variant -//! - Removing a field from a struct or enum variant -//! - Renaming structs, enums, or their fields or variants -//! -//! Adding new *optional* fields to a struct or enum variant is OK provided that -//! the default value's semantics match the semantics expected by users of older -//! specs that don't provide the optional data. -//! -//! Forward compatibility--writing the library so that old versions can -//! interpret specs generated by new versions--is not generally guaranteed. -//! Where possible, however, spec components should be written so that it is -//! possible to downgrade from a newer spec version to an older one if a -//! component's configuration can be represented in both versions. -//! -//! ## Serde attributes -//! -//! This module doesn't directly verify that a specific Propolis version can -//! support all of the features in any particular specification. However, users -//! can generally expect that if Propolis is willing to deserialize a spec, then -//! it should be able (in at least some circumstances) to support all of the -//! features that can be expressed in that spec. To help guarantee this property -//! (i.e., if Propolis can deserialize it, then it's at least well-formed), this -//! module uses a few common `serde` attributes. -//! -//! Structs and enums in this module should be tagged with the -//! `#[serde(deny_unknown_fields)]` attribute to reduce the risk that old code -//! will silently drop information from a spec produced by newer code with more -//! available fields. -//! -//! New optional fields should use the `#[serde(default)]` field attribute to -//! provide backward compatibility to old specs. They can also use the -//! `#[serde(skip_serializing_if)]` attribute to avoid serializing new fields -//! that have their default values. -//! -//! ### Example -//! -//! As an example, consider a (hypothetical) virtio device that has backend name -//! and PCI path fields: -//! -//! ``` -//! # use serde::{Serialize, Deserialize}; -//! # use schemars::JsonSchema; -//! # use propolis_types::PciPath; -//! -//! #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -//! #[serde(deny_unknown_fields)] -//! struct VirtioComponent { -//! backend_name: String, -//! pci_path: PciPath -//! } -//! ``` -//! -//! Suppose Propolis then adds support for configuring the number of virtqueues -//! this device exposes to the guest. This can be expressed compatibly as -//! follows: -//! -//! ``` -//! # use serde::{Serialize, Deserialize}; -//! # use schemars::JsonSchema; -//! # use propolis_types::PciPath; -//! -//! #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -//! #[serde(deny_unknown_fields)] -//! struct VirtioComponent { -//! backend_name: String, -//! pci_path: PciPath, -//! -//! #[serde(default, skip_serializing_if = "Option::is_none")] -//! num_virtqueues: Option -//! } -//! ``` -//! -//! Old component specs will continue to deserialize with `num_virtqueues` set -//! to `None`. In this case Propolis ensures that the device gets the default -//! number of virtqueues it had before this configuration option was added. If -//! this spec is serialized again, the `num_virtqueues` option is omitted, so -//! the spec can be deserialized by downlevel versions of the library. Note -//! again that the former behavior (new library accepts old spec) is required, -//! while the latter behavior (old library accepts new spec) is nice to have and -//! may not always be possible to provide (e.g. if the value is `Some`). -//! -//! ## Naming of versioned structures -//! -//! Dropshot's OpenAPI schema generator has a known limitation. If a type or one -//! of its dependent types appears in an API, Dropshot adds to the API's schema -//! an object type with the type's name. If two separate types with the same -//! name but *different module paths* appear in the API, Dropshot chooses one -//! to include and silently ignores the rest. This issue is -//! [dropshot#383](https://github.com/oxidecomputer/dropshot/issues/383). To -//! avoid it, strongly versioned types in this module use a "V#" suffix in their -//! names, even though they may reside in separate versioned modules. - -#![allow(rustdoc::private_intra_doc_links)] - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -pub use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor, PciPath}; -use uuid::Uuid; +//! Instance specification types. pub mod components; -pub mod v0; - -/// A key identifying a component in an instance spec. -// -// Some of the components Omicron attaches to Propolis VMs, like network -// interfaces and Crucible disks, are described by database records with UUID -// primary keys. It's natural to reuse these UUIDs as component identifiers in -// Propolis, especially because it lets Omicron functions that need to identify -// a specific component (e.g. a specific Crucible backend that should handle a -// disk snapshot request) pass that component's ID directly to Propolis. -// -// In some cases it's not desirable or possible to use UUIDs this way: -// -// - Some components (like the cloud-init disk) don't have their own rows in the -// database and so don't have obvious UUIDs to use. -// - Some objects (like Crucible disks) require both a device and a backend -// component in the spec, and these can't share the same key. -// - Propolis users outside the control plane may not have any component UUIDs -// at all and may just want to use strings to identify all their components. -// -// For these reasons, the key type may be represented as either a UUID or a -// String. This allows the more compact, more-easily-compared UUID format to be -// used wherever it is practical while still allowing callers to use strings as -// names if they have no UUIDs available or the most obvious UUID is in use -// elsewhere. The key type's From impls will try to parse strings into UUIDs -// before storing keys as strings. -#[derive( - Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd, -)] -// Direct serde to use an untagged enum representation for this type. Since both -// Uuid and String serialize to strings, this allows other types that contain a -// Map to derive Serialize and successfully serialize to JSON. -// (This doesn't work with a tagged representation because JSON doesn't allow -// maps to be used as map keys.) -// -// Note that this makes the order of variants matter: serde will pick the first -// variant into which it can successfully deserialize an untagged enum value, -// and the point is to use the UUID representation for any value that can be -// interpreted as a UUID. -#[serde(untagged)] -pub enum SpecKey { - Uuid(Uuid), - Name(String), -} - -impl std::fmt::Display for SpecKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Uuid(uuid) => write!(f, "{uuid}"), - Self::Name(name) => write!(f, "{name}"), - } - } -} - -impl std::str::FromStr for SpecKey { - type Err = core::convert::Infallible; - fn from_str(s: &str) -> Result { - Ok(s.into()) - } -} - -impl From<&str> for SpecKey { - fn from(s: &str) -> Self { - match Uuid::parse_str(s) { - Ok(uuid) => Self::Uuid(uuid), - Err(_) => Self::Name(s.to_owned()), - } - } -} - -impl From for SpecKey { - fn from(value: String) -> Self { - match Uuid::parse_str(value.as_str()) { - Ok(uuid) => Self::Uuid(uuid), - Err(_) => Self::Name(value), - } - } -} - -impl From for SpecKey { - fn from(value: Uuid) -> Self { - Self::Uuid(value) - } -} - -// Manually implement JsonSchema to help Progenitor generate the expected enum -// type for spec keys. -impl JsonSchema for SpecKey { - fn schema_name() -> String { - "SpecKey".to_owned() - } - - fn json_schema( - generator: &mut schemars::gen::SchemaGenerator, - ) -> schemars::schema::Schema { - use schemars::schema::*; - fn label_schema(label: &str, schema: Schema) -> Schema { - SchemaObject { - metadata: Some( - Metadata { - title: Some(label.to_string()), - ..Default::default() - } - .into(), - ), - subschemas: Some( - SubschemaValidation { - all_of: Some(vec![schema]), - ..Default::default() - } - .into(), - ), - ..Default::default() - } - .into() - } - - SchemaObject { - metadata: Some( - Metadata { - description: Some( - "A key identifying a component in an instance spec." - .to_string(), - ), - ..Default::default() - } - .into(), - ), - subschemas: Some(Box::new(SubschemaValidation { - one_of: Some(vec![ - label_schema("uuid", generator.subschema_for::()), - label_schema("name", generator.subschema_for::()), - ]), - ..Default::default() - })), - ..Default::default() - } - .into() - } -} - -/// DEPRECATED: A versioned instance spec. -/// -/// This structure is deprecated. It is notionally incompatible with -/// dropshot API versioning. If you wanted to add a `V1` variant to -/// `VersionedInstanceSpec`, it would change the existing blessed V1 OpenAPI -/// spec. Therefore you'd have to rename this to `VersionedInstanceSpecV0` and -/// create a new type with the new variant. This makes little sense however, -/// and is a remnant of an attempt at propolis versioning prior to dropshot -/// API versioning. -/// -/// Luckily this type is only exposed via `InstanceSpecGetResponse` which is not -/// used in Omicron. Therefore we can limit that method to the V1 OpenAPI spec -/// and stop any further use of this type. -/// -/// In addition to the disparate versioning mechanisms, there is also a -/// fundamental flaw in how this type was used in the existing code. It was -/// constructed in some cases from a `MaybeSpec` which contains a `Box`. -/// Unfortunately, `Spec` is a type erased container of any version of an -/// instance spec such as `InstanceSpecV0`,`InstanceSpecV1`, or future types. -/// There is no guarantee that we could take a `Spec` and figure out which -/// versioned spec it is supposed to convert to. This only worked in the initial -/// code because the only versioned spec was `InstanceSpecV0`. It's best to stop -/// using this type altogether. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields, tag = "version", content = "spec")] -pub enum VersionedInstanceSpec { - V0(v0::InstanceSpecV0), -} - -#[cfg(test)] -mod test { - use std::collections::BTreeMap; - - use uuid::Uuid; - - use super::{components::devices::QemuPvpanic, v0::ComponentV0, SpecKey}; - - type TestMap = BTreeMap; - - // Verifies that UUID-type spec keys that are serialized and deserialized - // continue to be interpreted as UUID-type spec keys. - #[test] - fn spec_key_uuid_roundtrip() { - let id = Uuid::new_v4(); - let mut map = TestMap::new(); - map.insert( - SpecKey::Uuid(id), - ComponentV0::QemuPvpanic(QemuPvpanic { enable_isa: true }), - ); - - let ser = serde_json::to_string(&map).unwrap(); - let unser: TestMap = serde_json::from_str(&ser).unwrap(); - let key = unser.keys().next().expect("one key in the map"); - let SpecKey::Uuid(got_id) = key else { - panic!("expected SpecKey::Uuid, got {key}"); - }; - - assert_eq!(*got_id, id); - } - - // Verifies that serializing a name-type spec key that happens to be the - // string representation of a UUID causes the key to deserialize as a - // UUID-type key. - #[test] - fn spec_key_uuid_string_deserializes_as_uuid_variant() { - let id = Uuid::new_v4(); - let mut map = TestMap::new(); - map.insert( - SpecKey::Name(id.to_string()), - ComponentV0::QemuPvpanic(QemuPvpanic { enable_isa: true }), - ); - - let ser = serde_json::to_string(&map).unwrap(); - let unser: TestMap = serde_json::from_str(&ser).unwrap(); - let key = unser.keys().next().expect("one key in the map"); - let SpecKey::Uuid(got_id) = key else { - panic!("expected SpecKey::Uuid, got {key}"); - }; - assert_eq!(*got_id, id); - } -} +pub use propolis_api_types_versions::latest::instance_spec::*; diff --git a/crates/propolis-api-types/src/instance_spec/v0.rs b/crates/propolis-api-types/src/instance_spec/v0.rs deleted file mode 100644 index 389df2efa..000000000 --- a/crates/propolis-api-types/src/instance_spec/v0.rs +++ /dev/null @@ -1,58 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use std::collections::BTreeMap; - -use crate::instance_spec::{components, SpecKey, VersionedInstanceSpec}; -use crate::{InstanceProperties, InstanceState}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde( - deny_unknown_fields, - tag = "type", - content = "component", - rename_all = "snake_case" -)] -pub enum ComponentV0 { - VirtioDisk(components::devices::VirtioDisk), - NvmeDisk(components::devices::NvmeDisk), - VirtioNic(components::devices::VirtioNic), - SerialPort(components::devices::SerialPort), - PciPciBridge(components::devices::PciPciBridge), - QemuPvpanic(components::devices::QemuPvpanic), - BootSettings(components::devices::BootSettings), - SoftNpuPciPort(components::devices::SoftNpuPciPort), - SoftNpuPort(components::devices::SoftNpuPort), - SoftNpuP9(components::devices::SoftNpuP9), - P9fs(components::devices::P9fs), - MigrationFailureInjector(components::devices::MigrationFailureInjector), - CrucibleStorageBackend(components::backends::CrucibleStorageBackend), - FileStorageBackend(components::backends::FileStorageBackend), - BlobStorageBackend(components::backends::BlobStorageBackend), - VirtioNetworkBackend(components::backends::VirtioNetworkBackend), - DlpiNetworkBackend(components::backends::DlpiNetworkBackend), -} - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct InstanceSpecV0 { - pub board: components::board::Board, - pub components: BTreeMap, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceSpecGetResponseV0 { - pub properties: InstanceProperties, - pub state: InstanceState, - pub spec: InstanceSpecStatusV0, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "type", content = "value")] -pub enum InstanceSpecStatusV0 { - WaitingForMigrationSource, - Present(VersionedInstanceSpec), -} diff --git a/crates/propolis-api-types/src/lib.rs b/crates/propolis-api-types/src/lib.rs index 16c5309b9..d730d98c7 100644 --- a/crates/propolis-api-types/src/lib.rs +++ b/crates/propolis-api-types/src/lib.rs @@ -2,420 +2,17 @@ // 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/. -//! Definitions for types exposed by the propolis-server API - -use std::{collections::BTreeMap, fmt, net::SocketAddr}; - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -pub mod v0; - -// Re-export the instance spec boot settings types so they can also be used in -// legacy instance ensure requests. -pub use crate::instance_spec::components::devices::{ - BootOrderEntry, BootSettings, -}; -use instance_spec::{components, v0::ComponentV0, SpecKey}; +//! Definitions for types exposed by the propolis-server API. +//! +//! This crate re-exports the latest versions of all API types from +//! `propolis-api-types-versions`. For versioned type access, depend +//! on that crate directly. + +pub mod disk; +pub mod instance; +pub mod instance_spec; +pub mod migration; +pub mod serial; // Re-export volume construction requests since they're part of a disk request. pub use crucible_client_types::VolumeConstructionRequest; - -pub mod instance_spec; - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceVCRReplace { - pub vcr_json: String, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceNameParams { - pub instance_id: String, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstancePathParams { - pub instance_id: Uuid, -} - -#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)] -pub struct InstanceMetadata { - pub silo_id: Uuid, - pub project_id: Uuid, - pub sled_id: Uuid, - pub sled_serial: String, - pub sled_revision: u32, - pub sled_model: String, -} - -/// An instance spec component that should be replaced during a live migration. -// -// When a caller asks Propolis to initialize via live migration, the target VM -// inherits the migration source's current instance spec. For the most part, -// the target can (and indeed in some cases must) use this spec without -// modifying it; this helps Propolis ensure that guest-visible configuration -// remains unchanged when a VM migrates. However, there are some components -// with no guest-visible state that may need to be reconfigured when a VM -// migrates. These include the following: -// -// - Crucible disks: After migrating, the target Propolis presents itself as a -// new client of the Crucible downstairs servers backing the VM's disks. -// Crucible requires the target to present a newer client generation number -// to allow the target to connect. In a full Oxide deployment, these numbers -// are managed by the control plane (i.e. it is not safe for Propolis to -// manage these values directly--new Crucible volume connection information -// must always come from Nexus). -// - Virtio network devices: Each virtio NIC in the guest needs to bind to a -// named VNIC object on the host. These names can change when a VM migrates -// from host to host. -// -// Each component that can be reconfigured this way has a variant in this enum; -// components not in the enum can't be reconfigured during migration. This -// saves the initialization API from having to reason about requests to replace -// a component that can't legally be replaced. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields, tag = "component", content = "spec")] -pub enum ReplacementComponent { - MigrationFailureInjector(components::devices::MigrationFailureInjector), - CrucibleStorageBackend(components::backends::CrucibleStorageBackend), - VirtioNetworkBackend(components::backends::VirtioNetworkBackend), -} - -impl From for instance_spec::v0::ComponentV0 { - fn from(value: ReplacementComponent) -> Self { - match value { - ReplacementComponent::MigrationFailureInjector(c) => { - ComponentV0::MigrationFailureInjector(c) - } - ReplacementComponent::CrucibleStorageBackend(c) => { - ComponentV0::CrucibleStorageBackend(c) - } - ReplacementComponent::VirtioNetworkBackend(c) => { - ComponentV0::VirtioNetworkBackend(c) - } - } - } -} - -#[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, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceEnsureResponse { - pub migrate: Option, -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceMigrateInitiateRequest { - pub migration_id: Uuid, - pub src_addr: SocketAddr, - pub src_uuid: Uuid, -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceMigrateInitiateResponse { - pub migration_id: Uuid, -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceMigrateStartRequest { - pub migration_id: Uuid, -} - -/// The status of an individual live migration. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] -pub struct InstanceMigrationStatus { - /// The ID of this migration, supplied either by the external migration - /// requester (for targets) or the other side of the migration (for - /// sources). - pub id: Uuid, - /// The current phase the migration is in. - pub state: MigrationState, -} - -/// The statuses of the most recent attempts to live migrate into and out of -/// this Propolis. -/// -/// If 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. -/// -/// This 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. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] -pub struct InstanceMigrateStatusResponse { - /// 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. - pub migration_in: Option, - /// 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. - pub migration_out: Option, -} - -#[derive( - Clone, - Copy, - Debug, - Deserialize, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - JsonSchema, -)] -pub enum MigrationState { - Sync, - RamPush, - Pause, - RamPushDirty, - Device, - Resume, - RamPull, - Server, - Finish, - Error, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceGetResponse { - pub instance: Instance, -} - -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -pub struct InstanceSpec { - pub board: components::board::Board, - pub components: BTreeMap, - pub smbios: Option, -} - -// Information put into the SMBIOS type 1 table in a VM -#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)] -#[serde(deny_unknown_fields)] -pub struct SmbiosType1Input { - pub manufacturer: String, - pub product_name: String, - pub serial_number: String, - pub version: u64, -} - -#[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, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -pub struct InstanceStateMonitorRequest { - pub gen: u64, -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceStateMonitorResponse { - pub gen: u64, - pub state: InstanceState, - pub migration: InstanceMigrateStatusResponse, -} - -/// Requested state of an Instance. -#[derive(Clone, Copy, Deserialize, Serialize, JsonSchema)] -pub struct InstanceStateChange { - pub state: InstanceStateRequested, -} - -#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema)] -pub enum InstanceStateRequested { - Run, - Stop, - Reboot, -} - -/// Current state of an Instance. -#[derive( - Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema, -)] -pub enum InstanceState { - Creating, - Starting, - Running, - Stopping, - Stopped, - Rebooting, - Migrating, - Repairing, - Failed, - Destroyed, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct InstanceProperties { - /// Unique identifier for this Instance. - pub id: Uuid, - /// Human-readable name of the Instance. - pub name: String, - /// Free-form text description of an Instance. - pub description: String, - /// Metadata used to track statistics for this Instance. - pub metadata: InstanceMetadata, -} - -impl InstanceProperties { - /// Return the name of the VMM resource backing this VM. - pub fn vm_name(&self) -> String { - self.id.to_string() - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct Instance { - pub properties: InstanceProperties, - pub state: InstanceState, -} - -/// Request a specific range of an Instance's serial console output history. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -pub struct InstanceSerialConsoleHistoryRequest { - /// 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. - pub from_start: Option, - /// 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) - pub most_recent: Option, - /// 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`. - pub max_bytes: Option, -} - -/// Contents of an Instance's serial console buffer. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceSerialConsoleHistoryResponse { - /// 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. - pub data: Vec, - /// The absolute offset since boot (suitable for use as `byte_offset` in a subsequent request) - /// of the last byte returned in `data`. - pub last_byte_offset: u64, -} - -/// Connect to an Instance's serial console via websocket, optionally sending -/// bytes from the buffered history first. -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq)] -pub struct InstanceSerialConsoleStreamRequest { - /// 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. - // TODO: if neither is specified, send enough serial buffer history to reconstruct - // the current contents and cursor state of an interactive terminal - pub from_start: Option, - /// 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) - pub most_recent: Option, -} - -/// Control message(s) sent through the websocket to serial console clients. -/// -/// Note: Because this is associated with the websocket, and not some REST -/// endpoint, Dropshot lacks the ability to communicate it via the OpenAPI -/// document underpinning the exposed interfaces. As such, clients (including -/// the `propolis-client` crate) are expected to define their own identical copy -/// of this type in order to consume it. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub enum InstanceSerialConsoleControlMessage { - Migrating { destination: SocketAddr, from_start: u64 }, -} - -#[derive(Deserialize, JsonSchema)] -pub struct SnapshotRequestPathParams { - pub id: String, - pub snapshot_id: Uuid, -} - -#[derive(Deserialize, JsonSchema)] -pub struct VCRRequestPathParams { - pub id: String, -} - -#[derive(Deserialize, JsonSchema)] -pub struct VolumeStatusPathParams { - pub id: String, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct VolumeStatus { - pub active: bool, -} - -/// Error codes used to populate the `error_code` field of Dropshot API responses. -#[derive( - Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema, -)] -pub enum ErrorCode { - /// This `propolis-server` process has not received an `InstanceEnsure` - /// request yet. - NoInstance, - /// This `propolis-server` process has already received an `InstanceEnsure` - /// request with a different ID. - AlreadyInitialized, - /// Cannot update a running server. - AlreadyRunning, - /// Instance creation failed - CreateFailed, -} - -impl fmt::Display for ErrorCode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(self, f) - } -} - -impl std::str::FromStr for ErrorCode { - type Err = &'static str; - fn from_str(s: &str) -> Result { - match s.trim() { - s if s.eq_ignore_ascii_case("NoInstance") => Ok(Self::NoInstance), - s if s.eq_ignore_ascii_case("AlreadyInitialized") => { - Ok(ErrorCode::AlreadyInitialized) - } - s if s.eq_ignore_ascii_case("AlreadyRunning") => { - Ok(ErrorCode::AlreadyRunning) - } - s if s.eq_ignore_ascii_case("CreateFailed") => { - Ok(ErrorCode::CreateFailed) - } - _ => Err("unknown error code, expected one of: \ - 'NoInstance', 'AlreadyInitialized', 'AlreadyRunning', \ - 'CreateFailed'"), - } - } -} diff --git a/crates/propolis-api-types/src/migration.rs b/crates/propolis-api-types/src/migration.rs new file mode 100644 index 000000000..4e9bdb444 --- /dev/null +++ b/crates/propolis-api-types/src/migration.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/. + +//! Migration types. + +pub use propolis_api_types_versions::latest::migration::*; diff --git a/crates/propolis-api-types/src/serial.rs b/crates/propolis-api-types/src/serial.rs new file mode 100644 index 000000000..584c0be2f --- /dev/null +++ b/crates/propolis-api-types/src/serial.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/. + +//! Serial console types. + +pub use propolis_api_types_versions::latest::serial::*; diff --git a/crates/propolis-api-types/src/v0.rs b/crates/propolis-api-types/src/v0.rs deleted file mode 100644 index 5426a3ca5..000000000 --- a/crates/propolis-api-types/src/v0.rs +++ /dev/null @@ -1,40 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -//! Types used in the v0 API of `propolis-api-server` - -use std::{collections::BTreeMap, net::SocketAddr}; - -use crate::instance_spec::VersionedInstanceSpec; -use crate::instance_spec::{v0::InstanceSpecV0, SpecKey}; -use crate::{InstanceProperties, ReplacementComponent}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "method", content = "value")] -pub enum InstanceInitializationMethodV0 { - Spec { - spec: InstanceSpecV0, - }, - MigrationTarget { - migration_id: Uuid, - src_addr: SocketAddr, - replace_components: BTreeMap, - }, -} - -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct InstanceEnsureRequestV0 { - pub properties: InstanceProperties, - pub init: InstanceInitializationMethodV0, -} - -#[derive(Clone, Deserialize, Serialize, JsonSchema)] -#[serde(tag = "type", content = "value")] -pub enum InstanceSpecStatusV0 { - WaitingForMigrationSource, - Present(VersionedInstanceSpec), -} diff --git a/crates/propolis-server-api/Cargo.toml b/crates/propolis-server-api/Cargo.toml index 8d1699e7c..a98c2b94d 100644 --- a/crates/propolis-server-api/Cargo.toml +++ b/crates/propolis-server-api/Cargo.toml @@ -8,4 +8,4 @@ edition = "2024" crucible-client-types.workspace = true dropshot.workspace = true dropshot-api-manager-types.workspace = true -propolis_api_types.workspace = true +propolis-api-types-versions.workspace = true diff --git a/crates/propolis-server-api/src/lib.rs b/crates/propolis-server-api/src/lib.rs index 0fbb54b56..5d20b55bf 100644 --- a/crates/propolis-server-api/src/lib.rs +++ b/crates/propolis-server-api/src/lib.rs @@ -8,16 +8,7 @@ use dropshot::{ WebsocketChannelResult, WebsocketConnection, }; use dropshot_api_manager_types::api_versions; -use propolis_api_types::{ - InstanceEnsureRequest, InstanceEnsureResponse, InstanceGetResponse, - InstanceMigrateStartRequest, InstanceMigrateStatusResponse, - InstanceSerialConsoleHistoryRequest, InstanceSerialConsoleHistoryResponse, - InstanceSerialConsoleStreamRequest, InstanceSpecGetResponse, - InstanceStateMonitorRequest, InstanceStateMonitorResponse, - InstanceStateRequested, InstanceVCRReplace, SnapshotRequestPathParams, - VCRRequestPathParams, VolumeStatus, VolumeStatusPathParams, - instance_spec::v0::InstanceSpecGetResponseV0, v0::InstanceEnsureRequestV0, -}; +use propolis_api_types_versions::{latest, v1}; api_versions!([ // WHEN CHANGING THE API (part 1 of 2): @@ -58,18 +49,31 @@ pub trait PropolisServerApi { }] async fn instance_ensure( rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError>; + request: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + >; #[endpoint { + operation_id = "instance_ensure", method = PUT, path = "/instance", versions = ..VERSION_PROGRAMMABLE_SMBIOS }] - async fn v0_instance_ensure( + async fn instance_ensure_v1( rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError>; + request: TypedBody, + ) -> Result< + HttpResponseCreated, + HttpError, + > { + Self::instance_ensure( + rqctx, + request.map(latest::instance::InstanceEnsureRequest::from), + ) + .await + } #[endpoint { method = GET, @@ -78,16 +82,27 @@ pub trait PropolisServerApi { }] async fn instance_spec_get( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; #[endpoint { + operation_id = "instance_spec_get", method = GET, path = "/instance/spec", versions = ..VERSION_PROGRAMMABLE_SMBIOS }] - async fn v0_instance_spec_get( + async fn instance_spec_get_v1( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + > { + Ok(Self::instance_spec_get(rqctx) + .await? + .map(v1::instance_spec::InstanceSpecGetResponse::from)) + } #[endpoint { method = GET, @@ -95,7 +110,7 @@ pub trait PropolisServerApi { }] async fn instance_get( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result, HttpError>; #[endpoint { method = GET, @@ -103,8 +118,11 @@ pub trait PropolisServerApi { }] async fn instance_state_monitor( rqctx: RequestContext, - request: TypedBody, - ) -> Result, HttpError>; + request: TypedBody, + ) -> Result< + HttpResponseOk, + HttpError, + >; #[endpoint { method = PUT, @@ -112,7 +130,7 @@ pub trait PropolisServerApi { }] async fn instance_state_put( rqctx: RequestContext, - request: TypedBody, + request: TypedBody, ) -> Result; #[endpoint { @@ -121,8 +139,11 @@ pub trait PropolisServerApi { }] async fn instance_serial_history_get( rqctx: RequestContext, - query: Query, - ) -> Result, HttpError>; + query: Query, + ) -> Result< + HttpResponseOk, + HttpError, + >; #[channel { protocol = WEBSOCKETS, @@ -130,7 +151,7 @@ pub trait PropolisServerApi { }] async fn instance_serial( rqctx: RequestContext, - query: Query, + query: Query, websock: WebsocketConnection, ) -> WebsocketChannelResult; @@ -179,7 +200,7 @@ pub trait PropolisServerApi { }] async fn instance_migrate_start( rqctx: RequestContext, - path_params: Path, + path_params: Path, websock: WebsocketConnection, ) -> dropshot::WebsocketChannelResult; @@ -189,7 +210,10 @@ pub trait PropolisServerApi { }] async fn instance_migrate_status( rqctx: RequestContext, - ) -> Result, HttpError>; + ) -> Result< + HttpResponseOk, + HttpError, + >; /// Issues a snapshot request to a crucible backend. #[endpoint { @@ -198,7 +222,7 @@ pub trait PropolisServerApi { }] async fn instance_issue_crucible_snapshot_request( rqctx: RequestContext, - path_params: Path, + path_params: Path, ) -> Result, HttpError>; /// Gets the status of a Crucible volume backing a disk @@ -208,8 +232,8 @@ pub trait PropolisServerApi { }] async fn disk_volume_status( rqctx: RequestContext, - path_params: Path, - ) -> Result, HttpError>; + path_params: Path, + ) -> Result, HttpError>; /// Issues a volume_construction_request replace to a crucible backend. #[endpoint { @@ -218,8 +242,8 @@ pub trait PropolisServerApi { }] async fn instance_issue_crucible_vcr_request( rqctx: RequestContext, - path_params: Path, - request: TypedBody, + path_params: Path, + request: TypedBody, ) -> Result, HttpError>; /// Issues an NMI to the instance. diff --git a/lib/propolis-client/Cargo.toml b/lib/propolis-client/Cargo.toml index d157a7174..54315666f 100644 --- a/lib/propolis-client/Cargo.toml +++ b/lib/propolis-client/Cargo.toml @@ -14,6 +14,7 @@ futures.workspace = true progenitor.workspace = true progenitor-client.workspace = true propolis_api_types.workspace = true +propolis-api-types-versions.workspace = true rand.workspace = true reqwest = { workspace = true, features = ["json", "rustls-tls"] } schemars = { workspace = true, features = ["uuid1"] } diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index 63df11ddf..39c112edf 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -19,16 +19,20 @@ /// counterparts. This obviates the need to maintain `From` impls to convert /// between native and generated types. pub mod instance_spec { + pub use propolis_api_types::instance::{ + InstanceMetadata, InstanceProperties, ReplacementComponent, + }; pub use propolis_api_types::instance_spec::{ components::{backends::*, board::*, devices::*}, - v0::*, - *, + InstanceSpec, InstanceSpecGetResponse, InstanceSpecStatus, + SmbiosType1Input, *, }; - - pub use propolis_api_types::{ - InstanceMetadata, InstanceProperties, InstanceSpec, - InstanceSpecGetResponse, InstanceSpecStatus, ReplacementComponent, - SmbiosType1Input, + // Re-export v1 types with V0 suffix for backward compatibility with + // progenitor-generated clients. + pub use propolis_api_types_versions::v1::instance_spec::{ + Component as ComponentV0, InstanceSpec as InstanceSpecV0, + InstanceSpecGetResponse as InstanceSpecGetResponseV0, + InstanceSpecStatus as InstanceSpecStatusV0, VersionedInstanceSpec, }; } From 800b05e0e77e89e7e6c2832e3447fbcab2915a40 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 23 Dec 2025 23:18:09 +0000 Subject: [PATCH 2/3] update clients Created using spr 1.3.6-beta.1 --- Cargo.lock | 2 +- bin/mock-server/Cargo.toml | 1 + bin/mock-server/src/lib/api_types.rs | 2 +- lib/propolis-client/Cargo.toml | 1 - lib/propolis-client/src/lib.rs | 34 ++++++++++++++-------------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78c42debc..65d5a7d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5441,7 +5441,6 @@ dependencies = [ "progenitor 0.10.0", "progenitor-client 0.10.0", "propolis-api-types-versions", - "propolis_api_types", "rand 0.9.2", "reqwest", "schemars", @@ -5492,6 +5491,7 @@ dependencies = [ "futures", "hyper", "progenitor 0.10.0", + "propolis-api-types-versions", "propolis_api_types", "propolis_types", "rand 0.9.2", diff --git a/bin/mock-server/Cargo.toml b/bin/mock-server/Cargo.toml index a5bf39379..d7e22e217 100644 --- a/bin/mock-server/Cargo.toml +++ b/bin/mock-server/Cargo.toml @@ -28,6 +28,7 @@ futures.workspace = true hyper.workspace = true serde.workspace = true propolis_api_types.workspace = true +propolis-api-types-versions.workspace = true propolis_types.workspace = true semver.workspace = true serde_json.workspace = true diff --git a/bin/mock-server/src/lib/api_types.rs b/bin/mock-server/src/lib/api_types.rs index 606a07a95..870ffc01b 100644 --- a/bin/mock-server/src/lib/api_types.rs +++ b/bin/mock-server/src/lib/api_types.rs @@ -9,7 +9,7 @@ progenitor::generate_api!( spec = "../../openapi/propolis-server/propolis-server-latest.json", derives = [schemars::JsonSchema], replace = { - SpecKey = propolis_api_types::instance_spec::SpecKey, + SpecKey = propolis_api_types_versions::latest::instance_spec::SpecKey, }, patch = { InstanceMetadata = { derives = [Clone, Eq, PartialEq] }, diff --git a/lib/propolis-client/Cargo.toml b/lib/propolis-client/Cargo.toml index 54315666f..c52e67fa5 100644 --- a/lib/propolis-client/Cargo.toml +++ b/lib/propolis-client/Cargo.toml @@ -13,7 +13,6 @@ crucible-client-types.workspace = true futures.workspace = true progenitor.workspace = true progenitor-client.workspace = true -propolis_api_types.workspace = true propolis-api-types-versions.workspace = true rand.workspace = true reqwest = { workspace = true, features = ["json", "rustls-tls"] } diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index 39c112edf..508e94995 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -19,14 +19,13 @@ /// counterparts. This obviates the need to maintain `From` impls to convert /// between native and generated types. pub mod instance_spec { - pub use propolis_api_types::instance::{ - InstanceMetadata, InstanceProperties, ReplacementComponent, + pub use propolis_api_types_versions::latest::components::{ + backends::*, board::*, devices::*, }; - pub use propolis_api_types::instance_spec::{ - components::{backends::*, board::*, devices::*}, - InstanceSpec, InstanceSpecGetResponse, InstanceSpecStatus, - SmbiosType1Input, *, + pub use propolis_api_types_versions::latest::instance::{ + InstanceMetadata, InstanceProperties, ReplacementComponent, }; + pub use propolis_api_types_versions::latest::instance_spec::*; // Re-export v1 types with V0 suffix for backward compatibility with // progenitor-generated clients. pub use propolis_api_types_versions::v1::instance_spec::{ @@ -47,17 +46,18 @@ progenitor::generate_api!( interface = Builder, tags = Separate, replace = { - PciPath = crate::instance_spec::PciPath, - ReplacementComponent = crate::instance_spec::ReplacementComponent, - InstanceSpecV0 = crate::instance_spec::InstanceSpecV0, - InstanceSpec = crate::instance_spec::InstanceSpec, - InstanceSpecStatus = crate::instance_spec::InstanceSpecStatus, - InstanceProperties = crate::instance_spec::InstanceProperties, - InstanceMetadata = crate::instance_spec::InstanceMetadata, - InstanceSpecGetResponse = crate::instance_spec::InstanceSpecGetResponse, - SmbiosType1Input = crate::instance_spec::SmbiosType1Input, - VersionedInstanceSpec = crate::instance_spec::VersionedInstanceSpec, - CpuidEntry = crate::instance_spec::CpuidEntry, + PciPath = propolis_api_types_versions::latest::instance_spec::PciPath, + ReplacementComponent = propolis_api_types_versions::latest::instance::ReplacementComponent, + // V0 types for backward compatibility (from v1) + InstanceSpecV0 = propolis_api_types_versions::v1::instance_spec::InstanceSpec, + InstanceSpec = propolis_api_types_versions::latest::instance_spec::InstanceSpec, + InstanceSpecStatus = propolis_api_types_versions::latest::instance_spec::InstanceSpecStatus, + 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, + 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, }, // Automatically derive JsonSchema for instance spec-related types so that // they can be reused in sled-agent's API. This can't be done with a From 936bd60bb13d02b6d76b97a60ff1ec703b927251 Mon Sep 17 00:00:00 2001 From: Rain Date: Tue, 23 Dec 2025 23:32:57 +0000 Subject: [PATCH 3/3] more fixups Created using spr 1.3.6-beta.1 --- .../src/lib/serial/history_buffer.rs | 20 ++++++++++--------- lib/propolis-client/src/lib.rs | 2 -- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bin/propolis-server/src/lib/serial/history_buffer.rs b/bin/propolis-server/src/lib/serial/history_buffer.rs index 6d300a7b6..98f50737d 100644 --- a/bin/propolis-server/src/lib/serial/history_buffer.rs +++ b/bin/propolis-server/src/lib/serial/history_buffer.rs @@ -6,7 +6,7 @@ //! first mebibyte and the most recent mebibyte of console output. use dropshot::HttpError; -use propolis_api_types::serial as api; +use propolis_api_types::serial as api_serial; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; use std::convert::TryFrom; @@ -45,17 +45,19 @@ pub(crate) enum SerialHistoryOffset { MostRecent(usize), } -impl TryFrom<&api::InstanceSerialConsoleStreamRequest> for SerialHistoryOffset { +impl TryFrom<&api_serial::InstanceSerialConsoleStreamRequest> + for SerialHistoryOffset +{ type Error = (); fn try_from( - req: &api::InstanceSerialConsoleStreamRequest, + req: &api_serial::InstanceSerialConsoleStreamRequest, ) -> Result { match req { - api::InstanceSerialConsoleStreamRequest { + api_serial::InstanceSerialConsoleStreamRequest { from_start: Some(offset), most_recent: None, } => Ok(SerialHistoryOffset::FromStart(*offset as usize)), - api::InstanceSerialConsoleStreamRequest { + api_serial::InstanceSerialConsoleStreamRequest { from_start: None, most_recent: Some(offset), } => Ok(SerialHistoryOffset::MostRecent(*offset as usize)), @@ -64,21 +66,21 @@ impl TryFrom<&api::InstanceSerialConsoleStreamRequest> for SerialHistoryOffset { } } -impl TryFrom<&api::InstanceSerialConsoleHistoryRequest> +impl TryFrom<&api_serial::InstanceSerialConsoleHistoryRequest> for SerialHistoryOffset { type Error = HttpError; fn try_from( - req: &api::InstanceSerialConsoleHistoryRequest, + req: &api_serial::InstanceSerialConsoleHistoryRequest, ) -> Result { match req { - api::InstanceSerialConsoleHistoryRequest { + api_serial::InstanceSerialConsoleHistoryRequest { from_start: Some(offset), most_recent: None, .. } => Ok(SerialHistoryOffset::FromStart(*offset as usize)), - api::InstanceSerialConsoleHistoryRequest { + api_serial::InstanceSerialConsoleHistoryRequest { from_start: None, most_recent: Some(offset), .. diff --git a/lib/propolis-client/src/lib.rs b/lib/propolis-client/src/lib.rs index 508e94995..f64085bc2 100644 --- a/lib/propolis-client/src/lib.rs +++ b/lib/propolis-client/src/lib.rs @@ -48,8 +48,6 @@ progenitor::generate_api!( replace = { PciPath = propolis_api_types_versions::latest::instance_spec::PciPath, ReplacementComponent = propolis_api_types_versions::latest::instance::ReplacementComponent, - // V0 types for backward compatibility (from v1) - InstanceSpecV0 = propolis_api_types_versions::v1::instance_spec::InstanceSpec, InstanceSpec = propolis_api_types_versions::latest::instance_spec::InstanceSpec, InstanceSpecStatus = propolis_api_types_versions::latest::instance_spec::InstanceSpecStatus, InstanceProperties = propolis_api_types_versions::latest::instance::InstanceProperties,