diff --git a/Cargo.lock b/Cargo.lock index af9a63114..3c87afbde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5546,6 +5546,7 @@ version = "0.1.0" dependencies = [ "crucible-client-types", "dropshot", + "dropshot-api-manager-types", "propolis_api_types", ] diff --git a/bin/propolis-server/src/lib/migrate/destination.rs b/bin/propolis-server/src/lib/migrate/destination.rs index 24ac63a01..4985ffbf2 100644 --- a/bin/propolis-server/src/lib/migrate/destination.rs +++ b/bin/propolis-server/src/lib/migrate/destination.rs @@ -4,6 +4,7 @@ use bitvec::prelude as bv; use futures::{SinkExt, StreamExt}; +use hyper::header::HeaderValue; use propolis::common::{GuestAddr, Lifecycle, PAGE_SIZE}; use propolis::migrate::{ MigrateCtx, MigrateStateError, Migrator, PayloadOffer, PayloadOffers, @@ -17,9 +18,11 @@ use std::convert::TryInto; use std::io; use std::net::SocketAddr; use std::sync::Arc; +use tokio::net::TcpStream; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; use tokio_tungstenite::tungstenite::protocol::CloseFrame; -use tokio_tungstenite::{tungstenite, WebSocketStream}; +use tokio_tungstenite::{tungstenite, MaybeTlsStream, WebSocketStream}; use uuid::Uuid; use crate::migrate::codec; @@ -80,17 +83,9 @@ pub(crate) async fn initiate( info!(log, "negotiating migration as destination"); // Build upgrade request to the source instance - // (we do this by hand to avoid a dependency from propolis-server to - // propolis-client) - // TODO(#165): https (wss) - // TODO: We need to make sure the src_addr is a valid target - let src_migrate_url = format!( - "ws://{}/instance/migrate/{}/start", - migrate_info.src_addr, migration_id, - ); - info!(log, "Begin migration"; "src_migrate_url" => &src_migrate_url); - let (mut conn, _) = - tokio_tungstenite::connect_async(src_migrate_url).await?; + let mut conn = + migration_start_connect(&log, migrate_info.src_addr, migration_id) + .await?; // Generate a list of protocols that this target supports, then send them to // the source and allow it to choose its favorite. @@ -153,6 +148,34 @@ pub(crate) async fn initiate( }) } +async fn migration_start_connect( + log: &slog::Logger, + src_addr: SocketAddr, + migration_id: Uuid, +) -> Result>, MigrateError> { + // We do this by hand to avoid a dependency from propolis-server to + // propolis-client. + // TODO(#165): https (wss) + // TODO: We need to make sure the src_addr is a valid target + let src_migrate_url = + format!("ws://{}/instance/migrate/{}/start", src_addr, migration_id); + info!(log, "Begin migration"; "src_migrate_url" => &src_migrate_url); + let mut req = src_migrate_url.into_client_request()?; + + // Add the api-version header. This assumes the instance_migrate_start API + // hasn't changed. See the note in crates/propolis-server-api/src/lib.rs. + req.headers_mut().insert( + omicron_common::api::VERSION_HEADER, + HeaderValue::from_str( + &propolis_server_api::VERSION_INITIAL.to_string(), + ) + .expect("VERSION_INITIAL is \"1.0.0\" which is a valid header value"), + ); + + let (conn, _) = tokio_tungstenite::connect_async(req).await?; + Ok(conn) +} + /// The runner for version 0 of the LM protocol, using RON encoding. struct RonV0 { /// The ID for this migration. diff --git a/crates/propolis-server-api/Cargo.toml b/crates/propolis-server-api/Cargo.toml index 9f7e19829..8d1699e7c 100644 --- a/crates/propolis-server-api/Cargo.toml +++ b/crates/propolis-server-api/Cargo.toml @@ -7,4 +7,5 @@ edition = "2024" [dependencies] crucible-client-types.workspace = true dropshot.workspace = true +dropshot-api-manager-types.workspace = true propolis_api_types.workspace = true diff --git a/crates/propolis-server-api/src/lib.rs b/crates/propolis-server-api/src/lib.rs index 9e0fb9484..5d4119a66 100644 --- a/crates/propolis-server-api/src/lib.rs +++ b/crates/propolis-server-api/src/lib.rs @@ -7,6 +7,7 @@ use dropshot::{ HttpResponseUpdatedNoContent, Path, Query, RequestContext, TypedBody, WebsocketChannelResult, WebsocketConnection, }; +use dropshot_api_manager_types::api_versions; use propolis_api_types::{ InstanceEnsureRequest, InstanceEnsureResponse, InstanceGetResponse, InstanceMigrateStartRequest, InstanceMigrateStatusResponse, @@ -17,6 +18,33 @@ use propolis_api_types::{ VCRRequestPathParams, VolumeStatus, VolumeStatusPathParams, }; +api_versions!([ + // WHEN CHANGING THE API (part 1 of 2): + // + // +- Pick a new semver and define it in the list below. The list MUST + // | remain sorted, which generally means that your version should go at + // | the very top. + // | + // | Duplicate this line, uncomment the *second* copy, update that copy for + // | your new API version, and leave the first copy commented out as an + // | example for the next person. + // v + // (next_int, IDENT), + (1, INITIAL), +]); + +// WHEN CHANGING THE API (part 2 of 2): +// +// The call to `api_versions!` above defines constants of type +// `semver::Version` that you can use in your Dropshot API definition to specify +// the version when a particular endpoint was added or removed. For example, if +// you used: +// +// (2, ADD_FOOBAR) +// +// Then you could use `VERSION_ADD_FOOBAR` as the version in which endpoints +// were added or removed. + #[dropshot::api_description] pub trait PropolisServerApi { type Context; @@ -116,6 +144,12 @@ pub trait PropolisServerApi { // // Part 1 is verified by the Dropshot API manager. For part 2, // propolis-server has internal support for protocol negotiation. + // + // Note that we currently bypass Progenitor and always pass in + // VERSION_INITIAL. See `migration_start_connect` in + // propolis-server/src/lib/migrate/destination.rs for where we do it. If we + // introduce a change to this API, we'll have to carefully consider version + // skew between the source and destination servers. #[channel { protocol = WEBSOCKETS, path = "/instance/migrate/{migration_id}/start",