From e4641722b3036e10e0528dcb1d208218ea337337 Mon Sep 17 00:00:00 2001 From: John Gallagher Date: Tue, 19 Dec 2023 09:50:41 -0800 Subject: [PATCH 01/13] [mgs] Use slog-error-chain to clean up error types and logging (#4717) These are all pretty mechanical changes: * Use `#[source]` and `#[from]` correctly * Add a bit of context to a few error cases (e.g., including the `SpIdentifier` when returning an `SpCommunicationFailed` error) * Use `InlineErrorChain` instead of `anyhow` to convert error chains into strings (avoiding the intermediate `anyhow::Error` heap allocation) * Switch to `Utf8PathBuf` for command line args and related errors --- Cargo.lock | 21 ++++ Cargo.toml | 1 + gateway/Cargo.toml | 2 + gateway/src/bin/mgs.rs | 8 +- gateway/src/config.rs | 53 +++------ gateway/src/error.rs | 148 ++++++++++++++---------- gateway/src/http_entrypoints.rs | 193 +++++++++++++++++++++---------- gateway/src/lib.rs | 12 +- gateway/src/management_switch.rs | 16 ++- gateway/src/serial_console.rs | 16 ++- 10 files changed, 291 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 121e31550f..74c01d3411 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4698,6 +4698,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "camino", "clap 4.4.3", "dropshot", "expectorate", @@ -4723,6 +4724,7 @@ dependencies = [ "signal-hook-tokio", "slog", "slog-dtrace", + "slog-error-chain", "sp-sim", "subprocess", "thiserror", @@ -7842,6 +7844,25 @@ dependencies = [ "slog-term", ] +[[package]] +name = "slog-error-chain" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f69041f45774602108e47fb25e705dc23acfb2" +dependencies = [ + "slog", + "slog-error-chain-derive", +] + +[[package]] +name = "slog-error-chain-derive" +version = "0.1.0" +source = "git+https://github.com/oxidecomputer/slog-error-chain?branch=main#15f69041f45774602108e47fb25e705dc23acfb2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "slog-json" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 841c7bb16b..ca134536f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -339,6 +339,7 @@ slog = { version = "2.7", features = [ "dynamic-keys", "max_level_trace", "relea slog-async = "2.8" slog-dtrace = "0.2" slog-envlogger = "2.2" +slog-error-chain = { git = "https://github.com/oxidecomputer/slog-error-chain", branch = "main", features = ["derive"] } slog-term = "2.9" smf = "0.2" snafu = "0.7" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 75c31e9977..f2e5f83a8a 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -7,6 +7,7 @@ license = "MPL-2.0" [dependencies] anyhow.workspace = true base64.workspace = true +camino.workspace = true clap.workspace = true dropshot.workspace = true futures.workspace = true @@ -25,6 +26,7 @@ signal-hook.workspace = true signal-hook-tokio.workspace = true slog.workspace = true slog-dtrace.workspace = true +slog-error-chain.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["full"] } tokio-stream.workspace = true diff --git a/gateway/src/bin/mgs.rs b/gateway/src/bin/mgs.rs index 6917d4f174..39810ea06a 100644 --- a/gateway/src/bin/mgs.rs +++ b/gateway/src/bin/mgs.rs @@ -5,6 +5,7 @@ //! Executable program to run gateway, the management gateway service use anyhow::{anyhow, Context}; +use camino::Utf8PathBuf; use clap::Parser; use futures::StreamExt; use omicron_common::cmd::{fatal, CmdError}; @@ -12,7 +13,6 @@ use omicron_gateway::{run_openapi, start_server, Config, MgsArguments}; use signal_hook::consts::signal; use signal_hook_tokio::Signals; use std::net::SocketAddrV6; -use std::path::PathBuf; use uuid::Uuid; #[derive(Debug, Parser)] @@ -24,7 +24,7 @@ enum Args { /// Start an MGS server Run { #[clap(name = "CONFIG_FILE_PATH", action)] - config_file_path: PathBuf, + config_file_path: Utf8PathBuf, /// Read server ID and address(es) for dropshot server from our SMF /// properties (only valid when running as a service on illumos) @@ -81,9 +81,7 @@ async fn do_run() -> Result<(), CmdError> { address, } => { let config = Config::from_file(&config_file_path) - .with_context(|| { - format!("failed to parse {}", config_file_path.display()) - }) + .map_err(anyhow::Error::new) .map_err(CmdError::Failure)?; let mut signals = Signals::new([signal::SIGUSR1]) diff --git a/gateway/src/config.rs b/gateway/src/config.rs index adbd16c6a1..afdb046881 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -6,10 +6,11 @@ //! configuration use crate::management_switch::SwitchConfig; +use camino::Utf8Path; +use camino::Utf8PathBuf; use dropshot::ConfigLogging; use serde::{Deserialize, Serialize}; -use std::path::Path; -use std::path::PathBuf; +use slog_error_chain::SlogInlineError; use thiserror::Error; /// Configuration for a gateway server @@ -30,13 +31,11 @@ impl Config { /// Load a `Config` from the given TOML file /// /// This config object can then be used to create a new gateway server. - // The format is described in the README. // TODO add a README - pub fn from_file>(path: P) -> Result { - let path = path.as_ref(); + pub fn from_file(path: &Utf8Path) -> Result { let file_contents = std::fs::read_to_string(path) - .map_err(|e| (path.to_path_buf(), e))?; + .map_err(|err| LoadError::Io { path: path.into(), err })?; let config_parsed: Config = toml::from_str(&file_contents) - .map_err(|e| (path.to_path_buf(), e))?; + .map_err(|err| LoadError::Parse { path: path.into(), err })?; Ok(config_parsed) } } @@ -46,32 +45,18 @@ pub struct PartialDropshotConfig { pub request_body_max_bytes: usize, } -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum LoadError { - #[error("error reading \"{}\": {}", path.display(), err)] - Io { path: PathBuf, err: std::io::Error }, - #[error("error parsing \"{}\": {}", path.display(), err)] - Parse { path: PathBuf, err: toml::de::Error }, -} - -impl From<(PathBuf, std::io::Error)> for LoadError { - fn from((path, err): (PathBuf, std::io::Error)) -> Self { - LoadError::Io { path, err } - } -} - -impl From<(PathBuf, toml::de::Error)> for LoadError { - fn from((path, err): (PathBuf, toml::de::Error)) -> Self { - LoadError::Parse { path, err } - } -} - -impl std::cmp::PartialEq for LoadError { - fn eq(&self, other: &std::io::Error) -> bool { - if let LoadError::Io { err, .. } = self { - err.kind() == other.kind() - } else { - false - } - } + #[error("error reading \"{path}\"")] + Io { + path: Utf8PathBuf, + #[source] + err: std::io::Error, + }, + #[error("error parsing \"{path}\"")] + Parse { + path: Utf8PathBuf, + #[source] + err: toml::de::Error, + }, } diff --git a/gateway/src/error.rs b/gateway/src/error.rs index 6daf9312ba..5933daa340 100644 --- a/gateway/src/error.rs +++ b/gateway/src/error.rs @@ -5,16 +5,17 @@ //! Error handling facilities for the management gateway. use crate::management_switch::SpIdentifier; -use anyhow::anyhow; use dropshot::HttpError; use gateway_messages::SpError; pub use gateway_sp_comms::error::CommunicationError; use gateway_sp_comms::error::UpdateError; use gateway_sp_comms::BindError; +use slog_error_chain::InlineErrorChain; +use slog_error_chain::SlogInlineError; use std::time::Duration; use thiserror::Error; -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum StartupError { #[error("invalid configuration file: {}", .reasons.join(", "))] InvalidConfig { reasons: Vec }, @@ -23,116 +24,137 @@ pub enum StartupError { BindError(#[from] BindError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, SlogInlineError)] pub enum SpCommsError { #[error("discovery process not yet complete")] DiscoveryNotYetComplete, #[error("location discovery failed: {reason}")] DiscoveryFailed { reason: String }, - #[error("nonexistent SP (type {:?}, slot {})", .0.typ, .0.slot)] + #[error("nonexistent SP {0:?}")] SpDoesNotExist(SpIdentifier), - #[error( - "unknown socket address for SP (type {:?}, slot {})", - .0.typ, - .0.slot, - )] + #[error("unknown socket address for SP {0:?}")] SpAddressUnknown(SpIdentifier), #[error( "timeout ({timeout:?}) elapsed communicating with {sp:?} on port {port}" )] Timeout { timeout: Duration, port: usize, sp: Option }, - #[error("error communicating with SP: {0}")] - SpCommunicationFailed(#[from] CommunicationError), - #[error("updating SP failed: {0}")] - UpdateFailed(#[from] UpdateError), + #[error("error communicating with SP {sp:?}")] + SpCommunicationFailed { + sp: SpIdentifier, + #[source] + err: CommunicationError, + }, + #[error("updating SP {sp:?} failed")] + UpdateFailed { + sp: SpIdentifier, + #[source] + err: UpdateError, + }, } impl From for HttpError { - fn from(err: SpCommsError) -> Self { - match err { + fn from(error: SpCommsError) -> Self { + match error { SpCommsError::SpDoesNotExist(_) => HttpError::for_bad_request( Some("InvalidSp".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError( - SpError::SerialConsoleAlreadyAttached, - ), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError( + SpError::SerialConsoleAlreadyAttached, + ), + .. + } => HttpError::for_bad_request( Some("SerialConsoleAttached".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError(SpError::RequestUnsupportedForSp), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError(SpError::RequestUnsupportedForSp), + .. + } => HttpError::for_bad_request( Some("RequestUnsupportedForSp".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError( - SpError::RequestUnsupportedForComponent, - ), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError( + SpError::RequestUnsupportedForComponent, + ), + .. + } => HttpError::for_bad_request( Some("RequestUnsupportedForComponent".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed( - CommunicationError::SpError(SpError::InvalidSlotForComponent), - ) => HttpError::for_bad_request( + SpCommsError::SpCommunicationFailed { + err: + CommunicationError::SpError(SpError::InvalidSlotForComponent), + .. + } => HttpError::for_bad_request( Some("InvalidSlotForComponent".to_string()), - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::UpdateFailed(UpdateError::ImageTooLarge) => { - HttpError::for_bad_request( - Some("ImageTooLarge".to_string()), - format!("{:#}", anyhow!(err)), - ) - } - SpCommsError::UpdateFailed(UpdateError::Communication( - CommunicationError::SpError(SpError::UpdateSlotBusy), - )) => http_err_with_message( + SpCommsError::UpdateFailed { + err: UpdateError::ImageTooLarge, + .. + } => HttpError::for_bad_request( + Some("ImageTooLarge".to_string()), + InlineErrorChain::new(&error).to_string(), + ), + SpCommsError::UpdateFailed { + err: + UpdateError::Communication(CommunicationError::SpError( + SpError::UpdateSlotBusy, + )), + .. + } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateSlotBusy", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::UpdateFailed(UpdateError::Communication( - CommunicationError::SpError(SpError::UpdateInProgress { - .. - }), - )) => http_err_with_message( + SpCommsError::UpdateFailed { + err: + UpdateError::Communication(CommunicationError::SpError( + SpError::UpdateInProgress { .. }, + )), + .. + } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateInProgress", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::DiscoveryNotYetComplete => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "DiscoveryNotYetComplete", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::SpAddressUnknown(_) => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "SpAddressUnknown", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::DiscoveryFailed { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "DiscoveryFailed ", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), SpCommsError::Timeout { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "Timeout ", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), - SpCommsError::SpCommunicationFailed(_) => http_err_with_message( - http::StatusCode::SERVICE_UNAVAILABLE, - "SpCommunicationFailed", - format!("{:#}", anyhow!(err)), - ), - SpCommsError::UpdateFailed(_) => http_err_with_message( + SpCommsError::SpCommunicationFailed { .. } => { + http_err_with_message( + http::StatusCode::SERVICE_UNAVAILABLE, + "SpCommunicationFailed", + InlineErrorChain::new(&error).to_string(), + ) + } + SpCommsError::UpdateFailed { .. } => http_err_with_message( http::StatusCode::SERVICE_UNAVAILABLE, "UpdateFailed", - format!("{:#}", anyhow!(err)), + InlineErrorChain::new(&error).to_string(), ), } } diff --git a/gateway/src/http_entrypoints.rs b/gateway/src/http_entrypoints.rs index 2db6121f1d..e33e8dd4a6 100644 --- a/gateway/src/http_entrypoints.rs +++ b/gateway/src/http_entrypoints.rs @@ -566,10 +566,12 @@ async fn sp_get( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp_id = path.into_inner().sp; - let sp = apictx.mgmt_switch.sp(sp_id.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; - let state = sp.state().await.map_err(SpCommsError::from)?; + let state = sp.state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(state.into())) } @@ -588,9 +590,12 @@ async fn sp_startup_options_get( ) -> Result, HttpError> { let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; - let options = sp.get_startup_options().await.map_err(SpCommsError::from)?; + let options = sp.get_startup_options().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(options.into())) } @@ -610,11 +615,12 @@ async fn sp_startup_options_set( ) -> Result { let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = mgmt_switch.sp(sp_id)?; - sp.set_startup_options(body.into_inner().into()) - .await - .map_err(SpCommsError::from)?; + sp.set_startup_options(body.into_inner().into()).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -632,8 +638,11 @@ async fn sp_component_list( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; - let inventory = sp.inventory().await.map_err(SpCommsError::from)?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; + let inventory = sp.inventory().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(inventory.into())) } @@ -653,11 +662,13 @@ async fn sp_component_get( ) -> Result>, HttpError> { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let details = - sp.component_details(component).await.map_err(SpCommsError::from)?; + let details = sp.component_details(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(details.entries.into_iter().map(Into::into).collect())) } @@ -690,7 +701,8 @@ async fn sp_component_caboose_get( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let ComponentCabooseSlot { firmware_slot } = query_params.into_inner(); let component = component_from_str(&component)?; @@ -714,19 +726,31 @@ async fn sp_component_caboose_get( CABOOSE_KEY_GIT_COMMIT, ) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let board = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_BOARD) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let name = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_NAME) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let version = sp .read_component_caboose(component, firmware_slot, CABOOSE_KEY_VERSION) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let git_commit = from_utf8(&CABOOSE_KEY_GIT_COMMIT, git_commit)?; let board = from_utf8(&CABOOSE_KEY_BOARD, board)?; @@ -752,10 +776,13 @@ async fn sp_component_clear_status( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - sp.component_clear_status(component).await.map_err(SpCommsError::from)?; + sp.component_clear_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -775,13 +802,13 @@ async fn sp_component_active_slot_get( ) -> Result, HttpError> { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let slot = sp - .component_active_slot(component) - .await - .map_err(SpCommsError::from)?; + let slot = sp.component_active_slot(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(SpComponentFirmwareSlot { slot })) } @@ -809,14 +836,15 @@ async fn sp_component_active_slot_set( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let slot = body.into_inner().slot; let persist = query_params.into_inner().persist; - sp.set_component_active_slot(component, slot, persist) - .await - .map_err(SpCommsError::from)?; + sp.set_component_active_slot(component, slot, persist).await.map_err( + |err| SpCommsError::SpCommunicationFailed { sp: sp_id, err }, + )?; Ok(HttpResponseUpdatedNoContent {}) } @@ -843,21 +871,27 @@ async fn sp_component_serial_console_attach( ) -> WebsocketEndpointResult { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); let component = component_from_str(&component)?; // Ensure we can attach to this SP's serial console. let console = apictx .mgmt_switch - .sp(sp.into())? + .sp(sp_id)? .serial_console_attach(component) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; let log = apictx.log.new(slog::o!("sp" => format!("{sp:?}"))); // We've successfully attached to the SP's serial console: upgrade the // websocket and run our side of that connection. - websocket.handle(move |conn| crate::serial_console::run(console, conn, log)) + websocket.handle(move |conn| { + crate::serial_console::run(sp_id, console, conn, log) + }) } /// Detach the websocket connection attached to the given SP component's serial @@ -875,9 +909,12 @@ async fn sp_component_serial_console_detach( // TODO-cleanup: "component" support for the serial console is half baked; // we don't use it at all to detach. let PathSpComponent { sp, component: _ } = path.into_inner(); + let sp_id = sp.into(); - let sp = apictx.mgmt_switch.sp(sp.into())?; - sp.serial_console_detach().await.map_err(SpCommsError::from)?; + let sp = apictx.mgmt_switch.sp(sp_id)?; + sp.serial_console_detach().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -927,13 +964,17 @@ async fn sp_component_reset( ) -> Result { let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; sp.reset_component_prepare(component) .and_then(|()| sp.reset_component_trigger(component)) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -964,7 +1005,8 @@ async fn sp_component_update( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let ComponentUpdateIdSlot { id, firmware_slot } = query_params.into_inner(); @@ -973,7 +1015,7 @@ async fn sp_component_update( sp.start_update(component, id, firmware_slot, image) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::UpdateFailed { sp: sp_id, err })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -993,11 +1035,13 @@ async fn sp_component_update_status( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; - let status = - sp.update_status(component).await.map_err(SpCommsError::from)?; + let status = sp.update_status(component).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(status.into())) } @@ -1020,11 +1064,14 @@ async fn sp_component_update_abort( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp_id = sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let component = component_from_str(&component)?; let UpdateAbortBody { id } = body.into_inner(); - sp.update_abort(component, id).await.map_err(SpCommsError::from)?; + sp.update_abort(component, id).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1043,6 +1090,7 @@ async fn sp_rot_cmpa_get( let apictx = rqctx.context(); let PathSpComponent { sp, component } = path.into_inner(); + let sp_id = sp.into(); // Ensure the caller knows they're asking for the RoT if component_from_str(&component)? != SpComponent::ROT { @@ -1052,8 +1100,10 @@ async fn sp_rot_cmpa_get( )); } - let sp = apictx.mgmt_switch.sp(sp.into())?; - let data = sp.read_rot_cmpa().await.map_err(SpCommsError::from)?; + let sp = apictx.mgmt_switch.sp(sp_id)?; + let data = sp.read_rot_cmpa().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; let base64_data = base64::engine::general_purpose::STANDARD.encode(data); @@ -1076,6 +1126,7 @@ async fn sp_rot_cfpa_get( let PathSpComponent { sp, component } = path.into_inner(); let GetCfpaParams { slot } = params.into_inner(); + let sp_id = sp.into(); // Ensure the caller knows they're asking for the RoT if component_from_str(&component)? != SpComponent::ROT { @@ -1085,13 +1136,13 @@ async fn sp_rot_cfpa_get( )); } - let sp = apictx.mgmt_switch.sp(sp.into())?; + let sp = apictx.mgmt_switch.sp(sp_id)?; let data = match slot { RotCfpaSlot::Active => sp.read_rot_active_cfpa().await, RotCfpaSlot::Inactive => sp.read_rot_inactive_cfpa().await, RotCfpaSlot::Scratch => sp.read_rot_scratch_cfpa().await, } - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; let base64_data = base64::engine::general_purpose::STANDARD.encode(data); @@ -1141,16 +1192,19 @@ async fn ignition_get( let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; - let sp = path.into_inner().sp; - let ignition_target = mgmt_switch.ignition_target(sp.into())?; + let sp_id = path.into_inner().sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; let state = mgmt_switch .ignition_controller() .ignition_state(ignition_target) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; - let info = SpIgnitionInfo { id: sp, details: state.into() }; + let info = SpIgnitionInfo { id: sp_id.into(), details: state.into() }; Ok(HttpResponseOk(info)) } @@ -1173,13 +1227,17 @@ async fn ignition_command( let apictx = rqctx.context(); let mgmt_switch = &apictx.mgmt_switch; let PathSpIgnitionCommand { sp, command } = path.into_inner(); - let ignition_target = mgmt_switch.ignition_target(sp.into())?; + let sp_id = sp.into(); + let ignition_target = mgmt_switch.ignition_target(sp_id)?; mgmt_switch .ignition_controller() .ignition_command(ignition_target, command.into()) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1197,9 +1255,12 @@ async fn sp_power_state_get( path: Path, ) -> Result, HttpError> { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; - let power_state = sp.power_state().await.map_err(SpCommsError::from)?; + let power_state = sp.power_state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseOk(power_state.into())) } @@ -1218,10 +1279,13 @@ async fn sp_power_state_set( body: TypedBody, ) -> Result { let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let power_state = body.into_inner(); - sp.set_power_state(power_state.into()).await.map_err(SpCommsError::from)?; + sp.set_power_state(power_state.into()).await.map_err(|err| { + SpCommsError::SpCommunicationFailed { sp: sp_id, err } + })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1241,7 +1305,8 @@ async fn sp_installinator_image_id_set( use ipcc_key_value::Key; let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; let image_id = ipcc_key_value::InstallinatorImageId::from(body.into_inner()); @@ -1251,7 +1316,7 @@ async fn sp_installinator_image_id_set( image_id.serialize(), ) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp: sp_id, err })?; Ok(HttpResponseUpdatedNoContent {}) } @@ -1268,12 +1333,16 @@ async fn sp_installinator_image_id_delete( use ipcc_key_value::Key; let apictx = rqctx.context(); - let sp = apictx.mgmt_switch.sp(path.into_inner().sp.into())?; + let sp_id = path.into_inner().sp.into(); + let sp = apictx.mgmt_switch.sp(sp_id)?; // We clear the image ID by setting it to a 0-length vec. sp.set_ipcc_key_lookup_value(Key::InstallinatorImageId as u8, Vec::new()) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { + sp: sp_id, + err, + })?; Ok(HttpResponseUpdatedNoContent {}) } diff --git a/gateway/src/lib.rs b/gateway/src/lib.rs index 10fcf1539c..5aa833f6e2 100644 --- a/gateway/src/lib.rs +++ b/gateway/src/lib.rs @@ -35,6 +35,7 @@ use slog::info; use slog::o; use slog::warn; use slog::Logger; +use slog_error_chain::InlineErrorChain; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::mem; @@ -138,7 +139,10 @@ impl Server { match gateway_sp_comms::register_probes() { Ok(_) => debug!(log, "successfully registered DTrace USDT probes"), Err(err) => { - warn!(log, "failed to register DTrace USDT probes: {}", err); + warn!( + log, "failed to register DTrace USDT probes"; + InlineErrorChain::new(&err), + ); } } @@ -328,9 +332,9 @@ pub async fn start_server( ); let log = slog::Logger::root(drain.fuse(), slog::o!(FileKv)); if let slog_dtrace::ProbeRegistration::Failed(e) = registration { - let msg = format!("failed to register DTrace probes: {}", e); - error!(log, "{}", msg); - return Err(msg); + let err = InlineErrorChain::new(&e); + error!(log, "failed to register DTrace probes"; &err); + return Err(format!("failed to register DTrace probes: {err}")); } else { debug!(log, "registered DTrace probes"); } diff --git a/gateway/src/management_switch.rs b/gateway/src/management_switch.rs index 03fdda2cca..0571dc051e 100644 --- a/gateway/src/management_switch.rs +++ b/gateway/src/management_switch.rs @@ -383,7 +383,14 @@ impl ManagementSwitch { > { let controller = self.ignition_controller(); let location_map = self.location_map()?; - let bulk_state = controller.bulk_ignition_state().await?; + let bulk_state = + controller.bulk_ignition_state().await.map_err(|err| { + SpCommsError::SpCommunicationFailed { + sp: location_map + .port_to_id(self.local_ignition_controller_port), + err, + } + })?; Ok(bulk_state.into_iter().enumerate().filter_map(|(target, state)| { // If the SP returns an ignition target we don't have a port @@ -402,11 +409,8 @@ impl ManagementSwitch { None => { warn!( self.log, - concat!( - "ignoring unknown ignition target {}", - " returned by ignition controller SP" - ), - target, + "ignoring unknown ignition target {target} \ + returned by ignition controller SP", ); None } diff --git a/gateway/src/serial_console.rs b/gateway/src/serial_console.rs index 3e49f8526a..49aa807e55 100644 --- a/gateway/src/serial_console.rs +++ b/gateway/src/serial_console.rs @@ -5,6 +5,7 @@ // Copyright 2022 Oxide Computer Company use crate::error::SpCommsError; +use crate::SpIdentifier; use dropshot::WebsocketChannelResult; use dropshot::WebsocketConnection; use futures::stream::SplitSink; @@ -19,6 +20,7 @@ use slog::error; use slog::info; use slog::warn; use slog::Logger; +use slog_error_chain::SlogInlineError; use std::borrow::Cow; use std::ops::Deref; use std::ops::DerefMut; @@ -34,7 +36,7 @@ use tokio_tungstenite::tungstenite::protocol::WebSocketConfig; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::WebSocketStream; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, SlogInlineError)] enum SerialTaskError { #[error(transparent)] SpCommsError(#[from] SpCommsError), @@ -43,6 +45,7 @@ enum SerialTaskError { } pub(crate) async fn run( + sp: SpIdentifier, console: AttachedSerialConsole, conn: WebsocketConnection, log: Logger, @@ -80,7 +83,7 @@ pub(crate) async fn run( let (console_tx, mut console_rx) = console.split(); let console_tx = DetachOnDrop::new(console_tx); let mut ws_recv_handle = - tokio::spawn(ws_recv_task(ws_stream, console_tx, log.clone())); + tokio::spawn(ws_recv_task(sp, ws_stream, console_tx, log.clone())); loop { tokio::select! { @@ -112,7 +115,9 @@ pub(crate) async fn run( Ok(()) => (), Err(TrySendError::Full(data)) => { warn!( - log, "channel full; discarding serial console data from SP"; + log, + "channel full; discarding serial \ + console data from SP"; "length" => data.len(), ); } @@ -160,6 +165,7 @@ async fn ws_sink_task( } async fn ws_recv_task( + sp: SpIdentifier, mut ws_stream: SplitStream>, mut console_tx: DetachOnDrop, log: Logger, @@ -175,7 +181,7 @@ async fn ws_recv_task( console_tx .write(data) .await - .map_err(SpCommsError::from)?; + .map_err(|err| SpCommsError::SpCommunicationFailed { sp, err })?; keepalive.reset(); } Some(Ok(Message::Close(_))) | None => { @@ -194,7 +200,7 @@ async fn ws_recv_task( } _= keepalive.tick() => { - console_tx.keepalive().await.map_err(SpCommsError::from)?; + console_tx.keepalive().await.map_err(|err| SpCommsError::SpCommunicationFailed { sp, err })?; } } } From 3382a33887a42db409bf3b8c780cd4125cc35f51 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Tue, 19 Dec 2023 12:34:20 -0800 Subject: [PATCH 02/13] fix Name regex in json schema (#4718) --- common/src/api/external/mod.rs | 2 +- common/tests/output/pagination-schema.txt | 4 ++-- openapi/bootstrap-agent.json | 2 +- openapi/nexus-internal.json | 4 ++-- openapi/nexus.json | 4 ++-- openapi/sled-agent.json | 2 +- schema/all-zone-requests.json | 2 +- schema/all-zones-requests.json | 2 +- schema/rss-service-plan-v2.json | 2 +- schema/rss-sled-plan.json | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index aa783ac9ca..446152137a 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -316,7 +316,7 @@ impl JsonSchema for Name { r#"^"#, // Cannot match a UUID r#"(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)"#, - r#"^[a-z][a-z0-9-]*[a-zA-Z0-9]*"#, + r#"^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?"#, r#"$"#, ) .to_string(), diff --git a/common/tests/output/pagination-schema.txt b/common/tests/output/pagination-schema.txt index 7cbaf439d6..436e614994 100644 --- a/common/tests/output/pagination-schema.txt +++ b/common/tests/output/pagination-schema.txt @@ -139,7 +139,7 @@ schema for pagination parameters: page selector, scan by name only "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NameSortMode": { "description": "Supported set of sort modes for scanning by name only\n\nCurrently, we only support scanning in ascending order.", @@ -228,7 +228,7 @@ schema for pagination parameters: page selector, scan by name or id "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NameOrId": { "oneOf": [ diff --git a/openapi/bootstrap-agent.json b/openapi/bootstrap-agent.json index 0c5bd15050..2a7ff43202 100644 --- a/openapi/bootstrap-agent.json +++ b/openapi/bootstrap-agent.json @@ -491,7 +491,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/nexus-internal.json b/openapi/nexus-internal.json index f909710ab4..a1d70d838b 100644 --- a/openapi/nexus-internal.json +++ b/openapi/nexus-internal.json @@ -4268,7 +4268,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, @@ -5578,7 +5578,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/nexus.json b/openapi/nexus.json index 4c89706a1c..35586375e8 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -12812,7 +12812,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, @@ -15020,7 +15020,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index d71f8de644..6076df6dbb 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -5246,7 +5246,7 @@ "title": "A name unique within the parent collection", "description": "Names must begin with a lower case ASCII letter, be composed exclusively of lowercase ASCII, uppercase ASCII, numbers, and '-', and may not end with a '-'. Names cannot be a UUID though they may contain a UUID.", "type": "string", - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$", + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$", "minLength": 1, "maxLength": 63 }, diff --git a/schema/all-zone-requests.json b/schema/all-zone-requests.json index 4eb56d379d..8c324a15bd 100644 --- a/schema/all-zone-requests.json +++ b/schema/all-zone-requests.json @@ -210,7 +210,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/all-zones-requests.json b/schema/all-zones-requests.json index 0e43e9ee21..7a07e2f9ae 100644 --- a/schema/all-zones-requests.json +++ b/schema/all-zones-requests.json @@ -94,7 +94,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/rss-service-plan-v2.json b/schema/rss-service-plan-v2.json index 0bcd27b9cc..62ce358938 100644 --- a/schema/rss-service-plan-v2.json +++ b/schema/rss-service-plan-v2.json @@ -179,7 +179,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NetworkInterface": { "description": "Information required to construct a virtual network interface", diff --git a/schema/rss-sled-plan.json b/schema/rss-sled-plan.json index 2ef7a7b58a..0396ccc685 100644 --- a/schema/rss-sled-plan.json +++ b/schema/rss-sled-plan.json @@ -355,7 +355,7 @@ "type": "string", "maxLength": 63, "minLength": 1, - "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z][a-z0-9-]*[a-zA-Z0-9]*$" + "pattern": "^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)^[a-z]([a-zA-Z0-9-]*[a-zA-Z0-9]+)?$" }, "NewPasswordHash": { "title": "A password hash in PHC string format", From 6783a5af9361a41840959fbb614d4bbd064b4f45 Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Tue, 19 Dec 2023 18:32:26 -0500 Subject: [PATCH 03/13] Fix fake pagination for sled_list_uninitialized (#4720) --- nexus/src/external_api/http_entrypoints.rs | 8 +++++++ openapi/nexus.json | 25 ++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8a4aeaeff5..3e38558760 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -4659,8 +4659,16 @@ async fn rack_view( }] async fn sled_list_uninitialized( rqctx: RequestContext>, + query: Query>, ) -> Result>, HttpError> { let apictx = rqctx.context(); + // We don't actually support real pagination + let pag_params = query.into_inner(); + if let dropshot::WhichPage::Next(last_seen) = &pag_params.page { + return Err( + Error::invalid_value(last_seen.clone(), "bad page token").into() + ); + } let handler = async { let nexus = &apictx.nexus; let opctx = crate::context::op_context_for_external_api(&rqctx).await?; diff --git a/openapi/nexus.json b/openapi/nexus.json index 35586375e8..4131460149 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -4022,6 +4022,28 @@ ], "summary": "List uninitialized sleds in a given rack", "operationId": "sled_list_uninitialized", + "parameters": [ + { + "in": "query", + "name": "limit", + "description": "Maximum number of items returned by a single call", + "schema": { + "nullable": true, + "type": "integer", + "format": "uint32", + "minimum": 1 + } + }, + { + "in": "query", + "name": "page_token", + "description": "Token returned by previous call to retrieve the subsequent page", + "schema": { + "nullable": true, + "type": "string" + } + } + ], "responses": { "200": { "description": "successful operation", @@ -4039,6 +4061,9 @@ "5XX": { "$ref": "#/components/responses/Error" } + }, + "x-dropshot-pagination": { + "required": [] } } }, From f2fb5af6e3c86fc231768f6faf58281b912a33e4 Mon Sep 17 00:00:00 2001 From: James MacMahon Date: Tue, 19 Dec 2023 22:07:16 -0500 Subject: [PATCH 04/13] Deserialize pre-6.0.0 RegionSnapshot objects (#4721) Schema update 6.0.0 added the `deleted` column to the region_snapshot table and added the `deleted` field to the RegionSnapshot object. If an old RegionSnapshot was serialized before this schema update (as part of a volume delete) into the `resources_to_clean_up` column of the volume table, _and_ if that volume delete failed and unwound, Nexus will fail to deserialize that column after that schema update + model change if there is another request to delete that volume. Add `#[serde(default)]` to RegionSnapshot's deleting field so that Nexus can deserialize pre-6.0.0 RegionSnapshot objects. This will default to `false` which matches what the ALTER COLUMN's default setting was in the 6.0.0 schema upgrade. Fixes oxidecomputer/customer-support#72 --- nexus/db-model/src/region_snapshot.rs | 10 +- nexus/db-queries/src/db/datastore/volume.rs | 115 ++++++++++++++++++++ 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/nexus/db-model/src/region_snapshot.rs b/nexus/db-model/src/region_snapshot.rs index af1cf8b2b3..2ea59f99f0 100644 --- a/nexus/db-model/src/region_snapshot.rs +++ b/nexus/db-model/src/region_snapshot.rs @@ -27,12 +27,16 @@ pub struct RegionSnapshot { pub region_id: Uuid, pub snapshot_id: Uuid, - // used for identifying volumes that reference this + /// used for identifying volumes that reference this pub snapshot_addr: String, - // how many volumes reference this? + /// how many volumes reference this? pub volume_references: i64, - // true if part of a volume's `resources_to_clean_up` already + /// true if part of a volume's `resources_to_clean_up` already + // this column was added in `schema/crdb/6.0.0/up1.sql` with a default of + // false, so instruct serde to deserialize default as false if an old + // serialized version of RegionSnapshot is being deserialized. + #[serde(default)] pub deleting: bool, } diff --git a/nexus/db-queries/src/db/datastore/volume.rs b/nexus/db-queries/src/db/datastore/volume.rs index 4f31efd610..d0b093ff45 100644 --- a/nexus/db-queries/src/db/datastore/volume.rs +++ b/nexus/db-queries/src/db/datastore/volume.rs @@ -1059,3 +1059,118 @@ pub fn read_only_resources_associated_with_volume( } } } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::db::datastore::datastore_test; + use nexus_test_utils::db::test_setup_database; + use omicron_test_utils::dev; + + // Assert that Nexus will not fail to deserialize an old version of + // CrucibleResources that was serialized before schema update 6.0.0. + #[tokio::test] + async fn test_deserialize_old_crucible_resources() { + let logctx = + dev::test_setup_log("test_deserialize_old_crucible_resources"); + let log = logctx.log.new(o!()); + let mut db = test_setup_database(&log).await; + let (_opctx, db_datastore) = datastore_test(&logctx, &db).await; + + // Start with a fake volume, doesn't matter if it's empty + + let volume_id = Uuid::new_v4(); + let _volume = db_datastore + .volume_create(nexus_db_model::Volume::new( + volume_id, + serde_json::to_string(&VolumeConstructionRequest::Volume { + id: volume_id, + block_size: 512, + sub_volumes: vec![], + read_only_parent: None, + }) + .unwrap(), + )) + .await + .unwrap(); + + // Add old CrucibleResources json in the `resources_to_clean_up` column - + // this was before the `deleting` column / field was added to + // ResourceSnapshot. + + { + use db::schema::volume::dsl; + + let conn = + db_datastore.pool_connection_unauthorized().await.unwrap(); + + let resources_to_clean_up = r#"{ + "V1": { + "datasets_and_regions": [], + "datasets_and_snapshots": [ + [ + { + "identity": { + "id": "844ee8d5-7641-4b04-bca8-7521e258028a", + "time_created": "2023-12-19T21:38:34.000000Z", + "time_modified": "2023-12-19T21:38:34.000000Z" + }, + "time_deleted": null, + "rcgen": 1, + "pool_id": "81a98506-4a97-4d92-8de5-c21f6fc71649", + "ip": "fd00:1122:3344:101::1", + "port": 32345, + "kind": "Crucible", + "size_used": 10737418240 + }, + { + "dataset_id": "b69edd77-1b3e-4f11-978c-194a0a0137d0", + "region_id": "8d668bf9-68cc-4387-8bc0-b4de7ef9744f", + "snapshot_id": "f548332c-6026-4eff-8c1c-ba202cd5c834", + "snapshot_addr": "[fd00:1122:3344:101::2]:19001", + "volume_references": 0 + } + ] + ] + } +} +"#; + + diesel::update(dsl::volume) + .filter(dsl::id.eq(volume_id)) + .set(dsl::resources_to_clean_up.eq(resources_to_clean_up)) + .execute_async(&*conn) + .await + .unwrap(); + } + + // Soft delete the volume, which runs the CTE + + let cr = db_datastore + .decrease_crucible_resource_count_and_soft_delete_volume(volume_id) + .await + .unwrap(); + + // Assert the contents of the returned CrucibleResources + + let datasets_and_regions = + db_datastore.regions_to_delete(&cr).await.unwrap(); + let datasets_and_snapshots = + db_datastore.snapshots_to_delete(&cr).await.unwrap(); + + assert!(datasets_and_regions.is_empty()); + assert_eq!(datasets_and_snapshots.len(), 1); + + let region_snapshot = &datasets_and_snapshots[0].1; + + assert_eq!( + region_snapshot.snapshot_id, + "f548332c-6026-4eff-8c1c-ba202cd5c834".parse().unwrap() + ); + assert_eq!(region_snapshot.deleting, false); + + db.cleanup().await.unwrap(); + logctx.cleanup_successful(); + } +} From 94944ccf5349479c9f0d1235fe504f2570253474 Mon Sep 17 00:00:00 2001 From: Sean Klein Date: Tue, 19 Dec 2023 22:56:26 -0800 Subject: [PATCH 05/13] Remove lazy_static in favor of once_cell (#4699) Fixes https://github.com/oxidecomputer/omicron/issues/4697 --- Cargo.lock | 7 +- Cargo.toml | 1 - common/Cargo.toml | 2 +- common/src/address.rs | 108 +- nexus/Cargo.toml | 1 - nexus/db-queries/Cargo.toml | 2 +- nexus/db-queries/src/authn/external/spoof.rs | 29 +- nexus/db-queries/src/authz/api_resources.rs | 7 +- .../db-queries/src/db/datastore/silo_user.rs | 10 +- nexus/db-queries/src/db/fixed_data/mod.rs | 15 +- nexus/db-queries/src/db/fixed_data/project.rs | 20 +- .../src/db/fixed_data/role_assignment.rs | 13 +- .../src/db/fixed_data/role_builtin.rs | 34 +- nexus/db-queries/src/db/fixed_data/silo.rs | 104 +- .../db-queries/src/db/fixed_data/silo_user.rs | 54 +- .../src/db/fixed_data/user_builtin.rs | 106 +- nexus/db-queries/src/db/fixed_data/vpc.rs | 42 +- .../src/db/fixed_data/vpc_firewall_rule.rs | 73 +- .../src/db/fixed_data/vpc_subnet.rs | 53 +- .../src/db/queries/network_interface.rs | 60 +- nexus/db-queries/src/db/saga_recovery.rs | 12 +- nexus/defaults/Cargo.toml | 2 +- nexus/defaults/src/lib.rs | 88 +- nexus/src/external_api/console_api.rs | 12 +- nexus/tests/integration_tests/endpoints.rs | 1256 +++++++++-------- nexus/tests/integration_tests/unauthorized.rs | 32 +- 26 files changed, 1155 insertions(+), 988 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74c01d3411..962fe68e02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4164,7 +4164,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "newtype_derive", "nexus-db-model", @@ -4177,6 +4176,7 @@ dependencies = [ "omicron-sled-agent", "omicron-test-utils", "omicron-workspace-hack", + "once_cell", "openapiv3", "openssl", "oso", @@ -4213,9 +4213,9 @@ name = "nexus-defaults" version = "0.1.0" dependencies = [ "ipnetwork", - "lazy_static", "omicron-common", "omicron-workspace-hack", + "once_cell", "rand 0.8.5", "serde_json", ] @@ -4618,10 +4618,10 @@ dependencies = [ "hex", "http", "ipnetwork", - "lazy_static", "libc", "macaddr", "omicron-workspace-hack", + "once_cell", "parse-display", "progenitor", "proptest", @@ -4773,7 +4773,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "mg-admin-client", "mime_guess", diff --git a/Cargo.toml b/Cargo.toml index ca134536f5..d651a13bf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,7 +229,6 @@ ipnetwork = { version = "0.20", features = ["schemars"] } itertools = "0.12.0" key-manager = { path = "key-manager" } kstat-rs = "0.2.3" -lazy_static = "1.4.0" libc = "0.2.151" linear-map = "1.2.0" macaddr = { version = "1.0.1", features = ["serde_std"] } diff --git a/common/Cargo.toml b/common/Cargo.toml index 49997e619c..3941f5303e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -17,7 +17,6 @@ hex.workspace = true http.workspace = true ipnetwork.workspace = true macaddr.workspace = true -lazy_static.workspace = true proptest = { workspace = true, optional = true } rand.workspace = true reqwest = { workspace = true, features = ["rustls-tls", "stream"] } @@ -38,6 +37,7 @@ uuid.workspace = true parse-display.workspace = true progenitor.workspace = true omicron-workspace-hack.workspace = true +once_cell.workspace = true [dev-dependencies] camino-tempfile.workspace = true diff --git a/common/src/address.rs b/common/src/address.rs index 992e8f0406..94361a2705 100644 --- a/common/src/address.rs +++ b/common/src/address.rs @@ -9,6 +9,7 @@ use crate::api::external::{self, Error, Ipv4Net, Ipv6Net}; use ipnetwork::{Ipv4Network, Ipv6Network}; +use once_cell::sync::Lazy; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV6}; @@ -76,65 +77,78 @@ pub const NTP_PORT: u16 = 123; // that situation (which may be as soon as allocating ephemeral IPs). pub const NUM_SOURCE_NAT_PORTS: u16 = 1 << 14; -lazy_static::lazy_static! { - // Services that require external connectivity are given an OPTE port - // with a "Service VNIC" record. Like a "Guest VNIC", a service is - // placed within a VPC (a built-in services VPC), along with a VPC subnet. - // But unlike guest instances which are created at runtime by Nexus, these - // services are created by RSS early on. So, we have some fixed values - // used to bootstrap service OPTE ports. Each service kind uses a distinct - // VPC subnet which RSS will allocate addresses from for those services. - // The specific values aren't deployment-specific as they are virtualized - // within OPTE. - - /// The IPv6 prefix assigned to the built-in services VPC. - // The specific prefix here was randomly chosen from the expected VPC - // prefix range (`fd00::/48`). See `random_vpc_ipv6_prefix`. - // Furthermore, all the below *_OPTE_IPV6_SUBNET constants are - // /64's within this prefix. - pub static ref SERVICE_VPC_IPV6_PREFIX: Ipv6Net = Ipv6Net( +// Services that require external connectivity are given an OPTE port +// with a "Service VNIC" record. Like a "Guest VNIC", a service is +// placed within a VPC (a built-in services VPC), along with a VPC subnet. +// But unlike guest instances which are created at runtime by Nexus, these +// services are created by RSS early on. So, we have some fixed values +// used to bootstrap service OPTE ports. Each service kind uses a distinct +// VPC subnet which RSS will allocate addresses from for those services. +// The specific values aren't deployment-specific as they are virtualized +// within OPTE. + +/// The IPv6 prefix assigned to the built-in services VPC. +// The specific prefix here was randomly chosen from the expected VPC +// prefix range (`fd00::/48`). See `random_vpc_ipv6_prefix`. +// Furthermore, all the below *_OPTE_IPV6_SUBNET constants are +// /64's within this prefix. +pub static SERVICE_VPC_IPV6_PREFIX: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 0, 0, 0, 0, 0), Ipv6Net::VPC_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for External DNS OPTE ports. - pub static ref DNS_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 1, 0), 24).unwrap()); - - /// The IPv6 subnet for External DNS OPTE ports. - pub static ref DNS_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for External DNS OPTE ports. +pub static DNS_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 1, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for External DNS OPTE ports. +pub static DNS_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 1, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for Nexus OPTE ports. - pub static ref NEXUS_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 2, 0), 24).unwrap()); - - /// The IPv6 subnet for Nexus OPTE ports. - pub static ref NEXUS_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for Nexus OPTE ports. +pub static NEXUS_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 2, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for Nexus OPTE ports. +pub static NEXUS_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 2, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); - - /// The IPv4 subnet for Boundary NTP OPTE ports. - pub static ref NTP_OPTE_IPV4_SUBNET: Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 3, 0), 24).unwrap()); - - /// The IPv6 subnet for Boundary NTP OPTE ports. - pub static ref NTP_OPTE_IPV6_SUBNET: Ipv6Net = Ipv6Net( + ) + .unwrap(), + ) +}); + +/// The IPv4 subnet for Boundary NTP OPTE ports. +pub static NTP_OPTE_IPV4_SUBNET: Lazy = Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 3, 0), 24).unwrap()) +}); + +/// The IPv6 subnet for Boundary NTP OPTE ports. +pub static NTP_OPTE_IPV6_SUBNET: Lazy = Lazy::new(|| { + Ipv6Net( Ipv6Network::new( Ipv6Addr::new(0xfd77, 0xe9d2, 0x9cd9, 3, 0, 0, 0, 0), Ipv6Net::VPC_SUBNET_IPV6_PREFIX_LENGTH, - ).unwrap(), - ); -} + ) + .unwrap(), + ) +}); // Anycast is a mechanism in which a single IP address is shared by multiple // devices, and the destination is located based on routing distance. diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index 704a7ab7bd..25833ec104 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -32,7 +32,6 @@ http.workspace = true hyper.workspace = true internal-dns.workspace = true ipnetwork.workspace = true -lazy_static.workspace = true macaddr.workspace = true mime_guess.workspace = true # Not under "dev-dependencies"; these also need to be implemented for diff --git a/nexus/db-queries/Cargo.toml b/nexus/db-queries/Cargo.toml index 9d8afd1fea..d5320be733 100644 --- a/nexus/db-queries/Cargo.toml +++ b/nexus/db-queries/Cargo.toml @@ -24,9 +24,9 @@ headers.workspace = true http.workspace = true hyper.workspace = true ipnetwork.workspace = true -lazy_static.workspace = true macaddr.workspace = true newtype_derive.workspace = true +once_cell.workspace = true openssl.workspace = true oso.workspace = true paste.workspace = true diff --git a/nexus/db-queries/src/authn/external/spoof.rs b/nexus/db-queries/src/authn/external/spoof.rs index 0b5896a6f8..9b5ed94bde 100644 --- a/nexus/db-queries/src/authn/external/spoof.rs +++ b/nexus/db-queries/src/authn/external/spoof.rs @@ -16,7 +16,7 @@ use anyhow::Context; use async_trait::async_trait; use headers::authorization::{Authorization, Bearer}; use headers::HeaderMapExt; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use uuid::Uuid; // This scheme is intended for demos, development, and testing until we have a @@ -54,18 +54,21 @@ const SPOOF_RESERVED_BAD_CREDS: &str = "this-fake-ID-it-is-truly-excellent"; // subsets of the base64 character set, so we do not bother encoding them. const SPOOF_PREFIX: &str = "oxide-spoof-"; -lazy_static! { - /// Actor (id) used for the special "bad credentials" error - static ref SPOOF_RESERVED_BAD_CREDS_ACTOR: Actor = Actor::UserBuiltin { - user_builtin_id: "22222222-2222-2222-2222-222222222222".parse().unwrap(), - }; - /// Complete HTTP header value to trigger the "bad actor" error - pub static ref SPOOF_HEADER_BAD_ACTOR: Authorization = - make_header_value_str(SPOOF_RESERVED_BAD_ACTOR).unwrap(); - /// Complete HTTP header value to trigger the "bad creds" error - pub static ref SPOOF_HEADER_BAD_CREDS: Authorization = - make_header_value_str(SPOOF_RESERVED_BAD_CREDS).unwrap(); -} +/// Actor (id) used for the special "bad credentials" error +static SPOOF_RESERVED_BAD_CREDS_ACTOR: Lazy = + Lazy::new(|| Actor::UserBuiltin { + user_builtin_id: "22222222-2222-2222-2222-222222222222" + .parse() + .unwrap(), + }); + +/// Complete HTTP header value to trigger the "bad actor" error +pub static SPOOF_HEADER_BAD_ACTOR: Lazy> = + Lazy::new(|| make_header_value_str(SPOOF_RESERVED_BAD_ACTOR).unwrap()); + +/// Complete HTTP header value to trigger the "bad creds" error +pub static SPOOF_HEADER_BAD_CREDS: Lazy> = + Lazy::new(|| make_header_value_str(SPOOF_RESERVED_BAD_CREDS).unwrap()); /// Implements a (test-only) authentication scheme where the client simply /// provides the actor information in a custom bearer token and we always trust diff --git a/nexus/db-queries/src/authz/api_resources.rs b/nexus/db-queries/src/authz/api_resources.rs index 2dfe2f7174..8485b8f11f 100644 --- a/nexus/db-queries/src/authz/api_resources.rs +++ b/nexus/db-queries/src/authz/api_resources.rs @@ -42,9 +42,9 @@ use crate::db::DataStore; use authz_macros::authz_resource; use futures::future::BoxFuture; use futures::FutureExt; -use lazy_static::lazy_static; use nexus_types::external_api::shared::{FleetRole, ProjectRole, SiloRole}; use omicron_common::api::external::{Error, LookupType, ResourceType}; +use once_cell::sync::Lazy; use oso::PolarClass; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -169,9 +169,8 @@ pub struct Fleet; /// Singleton representing the [`Fleet`] itself for authz purposes pub const FLEET: Fleet = Fleet; -lazy_static! { - pub static ref FLEET_LOOKUP: LookupType = LookupType::ById(*FLEET_ID); -} +pub static FLEET_LOOKUP: Lazy = + Lazy::new(|| LookupType::ById(*FLEET_ID)); impl Eq for Fleet {} impl PartialEq for Fleet { diff --git a/nexus/db-queries/src/db/datastore/silo_user.rs b/nexus/db-queries/src/db/datastore/silo_user.rs index 6084f8c2ab..59cb19a609 100644 --- a/nexus/db-queries/src/db/datastore/silo_user.rs +++ b/nexus/db-queries/src/db/datastore/silo_user.rs @@ -363,11 +363,11 @@ impl DataStore { let builtin_users = [ // Note: "db_init" is also a builtin user, but that one by necessity // is created with the database. - &*authn::USER_SERVICE_BALANCER, - &*authn::USER_INTERNAL_API, - &*authn::USER_INTERNAL_READ, - &*authn::USER_EXTERNAL_AUTHN, - &*authn::USER_SAGA_RECOVERY, + &authn::USER_SERVICE_BALANCER, + &authn::USER_INTERNAL_API, + &authn::USER_INTERNAL_READ, + &authn::USER_EXTERNAL_AUTHN, + &authn::USER_SAGA_RECOVERY, ] .iter() .map(|u| { diff --git a/nexus/db-queries/src/db/fixed_data/mod.rs b/nexus/db-queries/src/db/fixed_data/mod.rs index 5c91407134..4f896eb5d1 100644 --- a/nexus/db-queries/src/db/fixed_data/mod.rs +++ b/nexus/db-queries/src/db/fixed_data/mod.rs @@ -31,7 +31,7 @@ // 001de000-074c built-in services vpc // 001de000-c470 built-in services vpc subnets -use lazy_static::lazy_static; +use once_cell::sync::Lazy; pub mod project; pub mod role_assignment; @@ -43,13 +43,12 @@ pub mod vpc; pub mod vpc_firewall_rule; pub mod vpc_subnet; -lazy_static! { - /* See above for where this uuid comes from. */ - pub static ref FLEET_ID: uuid::Uuid = - "001de000-1334-4000-8000-000000000000" - .parse() - .expect("invalid uuid for builtin fleet id"); -} +/* See above for where this uuid comes from. */ +pub static FLEET_ID: Lazy = Lazy::new(|| { + "001de000-1334-4000-8000-000000000000" + .parse() + .expect("invalid uuid for builtin fleet id") +}); #[cfg(test)] fn assert_valid_uuid(id: &uuid::Uuid) { diff --git a/nexus/db-queries/src/db/fixed_data/project.rs b/nexus/db-queries/src/db/fixed_data/project.rs index 52450438c0..e240900e0c 100644 --- a/nexus/db-queries/src/db/fixed_data/project.rs +++ b/nexus/db-queries/src/db/fixed_data/project.rs @@ -4,18 +4,20 @@ use crate::db; use crate::db::datastore::SERVICES_DB_NAME; -use lazy_static::lazy_static; use nexus_types::external_api::params; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in project for internal services on the rack. - pub static ref SERVICES_PROJECT_ID: uuid::Uuid = "001de000-4401-4000-8000-000000000000" +/// UUID of built-in project for internal services on the rack. +pub static SERVICES_PROJECT_ID: Lazy = Lazy::new(|| { + "001de000-4401-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin services project id"); + .expect("invalid uuid for builtin services project id") +}); - /// Built-in Project for internal services on the rack. - pub static ref SERVICES_PROJECT: db::model::Project = db::model::Project::new_with_id( +/// Built-in Project for internal services on the rack. +pub static SERVICES_PROJECT: Lazy = Lazy::new(|| { + db::model::Project::new_with_id( *SERVICES_PROJECT_ID, *super::silo::INTERNAL_SILO_ID, params::ProjectCreate { @@ -24,5 +26,5 @@ lazy_static! { description: "Built-in project for Oxide Services".to_string(), }, }, - ); -} + ) +}); diff --git a/nexus/db-queries/src/db/fixed_data/role_assignment.rs b/nexus/db-queries/src/db/fixed_data/role_assignment.rs index 7d7ddffab6..d6c95d47b6 100644 --- a/nexus/db-queries/src/db/fixed_data/role_assignment.rs +++ b/nexus/db-queries/src/db/fixed_data/role_assignment.rs @@ -8,10 +8,10 @@ use super::user_builtin; use super::FLEET_ID; use crate::db::model::IdentityType; use crate::db::model::RoleAssignment; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; -lazy_static! { - pub static ref BUILTIN_ROLE_ASSIGNMENTS: Vec = +pub static BUILTIN_ROLE_ASSIGNMENTS: Lazy> = + Lazy::new(|| { vec![ // The "internal-api" user gets the "admin" role on the sole Fleet. // This is a pretty elevated privilege. @@ -24,7 +24,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // The "USER_SERVICE_BALANCER" user gets the "admin" role on the // Fleet. // @@ -38,7 +37,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - // The "internal-read" user gets the "viewer" role on the sole // Fleet. This will grant them the ability to read various control // plane data (like the list of sleds), which is in turn used to @@ -50,7 +48,6 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_VIEWER.role_name, ), - // The "external-authenticator" user gets the "authenticator" role // on the sole fleet. This grants them the ability to create // sessions. @@ -61,5 +58,5 @@ lazy_static! { *FLEET_ID, role_builtin::FLEET_AUTHENTICATOR.role_name, ), - ]; -} + ] + }); diff --git a/nexus/db-queries/src/db/fixed_data/role_builtin.rs b/nexus/db-queries/src/db/fixed_data/role_builtin.rs index 865f6328f4..f58077fc3f 100644 --- a/nexus/db-queries/src/db/fixed_data/role_builtin.rs +++ b/nexus/db-queries/src/db/fixed_data/role_builtin.rs @@ -3,8 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Built-in roles -use lazy_static::lazy_static; use omicron_common::api; +use once_cell::sync::Lazy; #[derive(Clone, Debug)] pub struct RoleBuiltinConfig { @@ -13,28 +13,36 @@ pub struct RoleBuiltinConfig { pub description: &'static str, } -lazy_static! { - pub static ref FLEET_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { +pub static FLEET_ADMIN: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "admin", description: "Fleet Administrator", - }; - pub static ref FLEET_AUTHENTICATOR: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static FLEET_AUTHENTICATOR: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "external-authenticator", description: "Fleet External Authenticator", - }; - pub static ref FLEET_VIEWER: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static FLEET_VIEWER: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Fleet, role_name: "viewer", description: "Fleet Viewer", - }; - pub static ref SILO_ADMIN: RoleBuiltinConfig = RoleBuiltinConfig { + }); + +pub static SILO_ADMIN: Lazy = + Lazy::new(|| RoleBuiltinConfig { resource_type: api::external::ResourceType::Silo, role_name: "admin", description: "Silo Administrator", - }; - pub static ref BUILTIN_ROLES: Vec = vec![ + }); + +pub static BUILTIN_ROLES: Lazy> = Lazy::new(|| { + vec![ FLEET_ADMIN.clone(), FLEET_AUTHENTICATOR.clone(), FLEET_VIEWER.clone(), @@ -69,8 +77,8 @@ lazy_static! { role_name: "viewer", description: "Project Viewer", }, - ]; -} + ] +}); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/silo.rs b/nexus/db-queries/src/db/fixed_data/silo.rs index 6eba849ee3..62bcc61c1e 100644 --- a/nexus/db-queries/src/db/fixed_data/silo.rs +++ b/nexus/db-queries/src/db/fixed_data/silo.rs @@ -3,62 +3,66 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::db; -use lazy_static::lazy_static; use nexus_types::external_api::{params, shared}; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - pub static ref SILO_ID: uuid::Uuid = "001de000-5110-4000-8000-000000000000" +pub static SILO_ID: Lazy = Lazy::new(|| { + "001de000-5110-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin silo id"); + .expect("invalid uuid for builtin silo id") +}); - /// "Default" Silo - /// - /// This was historically used for demos and the unit tests. The plan is to - /// remove it per omicron#2305. - pub static ref DEFAULT_SILO: db::model::Silo = - db::model::Silo::new_with_id( - *SILO_ID, - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: "default-silo".parse().unwrap(), - description: "default silo".to_string(), - }, - // This quota is actually _unused_ because the default silo - // isn't constructed in the same way a normal silo would be. - quotas: params::SiloQuotasCreate::empty(), - discoverable: false, - identity_mode: shared::SiloIdentityMode::LocalOnly, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), +/// "Default" Silo +/// +/// This was historically used for demos and the unit tests. The plan is to +/// remove it per omicron#2305. +pub static DEFAULT_SILO: Lazy = Lazy::new(|| { + db::model::Silo::new_with_id( + *SILO_ID, + params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: "default-silo".parse().unwrap(), + description: "default silo".to_string(), }, - ) - .unwrap(); + // This quota is actually _unused_ because the default silo + // isn't constructed in the same way a normal silo would be. + quotas: params::SiloQuotasCreate::empty(), + discoverable: false, + identity_mode: shared::SiloIdentityMode::LocalOnly, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }, + ) + .unwrap() +}); - /// UUID of built-in internal silo. - pub static ref INTERNAL_SILO_ID: uuid::Uuid = - "001de000-5110-4000-8000-000000000001" - .parse() - .expect("invalid uuid for builtin silo id"); +/// UUID of built-in internal silo. +pub static INTERNAL_SILO_ID: Lazy = Lazy::new(|| { + "001de000-5110-4000-8000-000000000001" + .parse() + .expect("invalid uuid for builtin silo id") +}); - /// Built-in Silo to house internal resources. It contains no users and - /// can't be logged into. - pub static ref INTERNAL_SILO: db::model::Silo = - db::model::Silo::new_with_id( - *INTERNAL_SILO_ID, - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: "oxide-internal".parse().unwrap(), - description: "Built-in internal Silo.".to_string(), - }, - // The internal silo contains no virtual resources, so it has no allotted capacity. - quotas: params::SiloQuotasCreate::empty(), - discoverable: false, - identity_mode: shared::SiloIdentityMode::LocalOnly, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), +/// Built-in Silo to house internal resources. It contains no users and +/// can't be logged into. +pub static INTERNAL_SILO: Lazy = Lazy::new(|| { + db::model::Silo::new_with_id( + *INTERNAL_SILO_ID, + params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: "oxide-internal".parse().unwrap(), + description: "Built-in internal Silo.".to_string(), }, - ).unwrap(); -} + // The internal silo contains no virtual resources, so it has no allotted capacity. + quotas: params::SiloQuotasCreate::empty(), + discoverable: false, + identity_mode: shared::SiloIdentityMode::LocalOnly, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }, + ) + .unwrap() +}); diff --git a/nexus/db-queries/src/db/fixed_data/silo_user.rs b/nexus/db-queries/src/db/fixed_data/silo_user.rs index d54bcfa59f..b5253b68e3 100644 --- a/nexus/db-queries/src/db/fixed_data/silo_user.rs +++ b/nexus/db-queries/src/db/fixed_data/silo_user.rs @@ -6,25 +6,26 @@ use super::role_builtin; use crate::db; use crate::db::identity::Asset; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; -lazy_static! { - /// Test user that's granted all privileges, used for automated testing and - /// local development - // TODO-security Once we have a way to bootstrap the initial Silo with the - // initial privileged user, this user should be created in the test suite, - // not automatically at Nexus startup. See omicron#2305. - pub static ref USER_TEST_PRIVILEGED: db::model::SiloUser = - db::model::SiloUser::new( - *db::fixed_data::silo::SILO_ID, - // "4007" looks a bit like "root". - "001de000-05e4-4000-8000-000000004007".parse().unwrap(), - "privileged".into(), - ); +/// Test user that's granted all privileges, used for automated testing and +/// local development +// TODO-security Once we have a way to bootstrap the initial Silo with the +// initial privileged user, this user should be created in the test suite, +// not automatically at Nexus startup. See omicron#2305. +pub static USER_TEST_PRIVILEGED: Lazy = Lazy::new(|| { + db::model::SiloUser::new( + *db::fixed_data::silo::SILO_ID, + // "4007" looks a bit like "root". + "001de000-05e4-4000-8000-000000004007".parse().unwrap(), + "privileged".into(), + ) +}); - /// Role assignments needed for the privileged user - pub static ref ROLE_ASSIGNMENTS_PRIVILEGED: - Vec = vec![ +/// Role assignments needed for the privileged user +pub static ROLE_ASSIGNMENTS_PRIVILEGED: Lazy> = + Lazy::new(|| { + vec![ // The "test-privileged" user gets the "admin" role on the sole // Fleet as well as the default Silo. db::model::RoleAssignment::new( @@ -34,7 +35,6 @@ lazy_static! { *db::fixed_data::FLEET_ID, role_builtin::FLEET_ADMIN.role_name, ), - db::model::RoleAssignment::new( db::model::IdentityType::SiloUser, USER_TEST_PRIVILEGED.id(), @@ -42,20 +42,22 @@ lazy_static! { *db::fixed_data::silo::SILO_ID, role_builtin::SILO_ADMIN.role_name, ), - ]; + ] + }); - /// Test user that's granted no privileges, used for automated testing - // TODO-security Once we have a way to bootstrap the initial Silo with the - // initial privileged user, this user should be created in the test suite, - // not automatically at Nexus startup. See omicron#2305. - pub static ref USER_TEST_UNPRIVILEGED: db::model::SiloUser = +/// Test user that's granted no privileges, used for automated testing +// TODO-security Once we have a way to bootstrap the initial Silo with the +// initial privileged user, this user should be created in the test suite, +// not automatically at Nexus startup. See omicron#2305. +pub static USER_TEST_UNPRIVILEGED: Lazy = + Lazy::new(|| { db::model::SiloUser::new( *db::fixed_data::silo::SILO_ID, // 60001 is the decimal uid for "nobody" on Helios. "001de000-05e4-4000-8000-000000060001".parse().unwrap(), "unprivileged".into(), - ); -} + ) + }); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/user_builtin.rs b/nexus/db-queries/src/db/fixed_data/user_builtin.rs index 87f33fa355..1e96802683 100644 --- a/nexus/db-queries/src/db/fixed_data/user_builtin.rs +++ b/nexus/db-queries/src/db/fixed_data/user_builtin.rs @@ -3,8 +3,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. //! Built-in users -use lazy_static::lazy_static; use omicron_common::api; +use once_cell::sync::Lazy; use uuid::Uuid; pub struct UserBuiltinConfig { @@ -27,61 +27,65 @@ impl UserBuiltinConfig { } } -lazy_static! { - /// Internal user used for seeding initial database data - // NOTE: This uuid and name are duplicated in dbinit.sql. - pub static ref USER_DB_INIT: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "0001" is the first possible user that wouldn't be confused with - // 0, or root. - "001de000-05e4-4000-8000-000000000001", - "db-init", - "used for seeding initial database data", - ); +/// Internal user used for seeding initial database data +// NOTE: This uuid and name are duplicated in dbinit.sql. +pub static USER_DB_INIT: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "0001" is the first possible user that wouldn't be confused with + // 0, or root. + "001de000-05e4-4000-8000-000000000001", + "db-init", + "used for seeding initial database data", + ) +}); - /// Internal user for performing operations to manage the - /// provisioning of services across the fleet. - pub static ref USER_SERVICE_BALANCER: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-00000000bac3", - "service-balancer", - "used for Nexus-driven service balancing", - ); +/// Internal user for performing operations to manage the +/// provisioning of services across the fleet. +pub static USER_SERVICE_BALANCER: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-00000000bac3", + "service-balancer", + "used for Nexus-driven service balancing", + ) +}); - /// Internal user used by Nexus when handling internal API requests - pub static ref USER_INTERNAL_API: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000002", - "internal-api", - "used by Nexus when handling internal API requests", - ); +/// Internal user used by Nexus when handling internal API requests +pub static USER_INTERNAL_API: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-000000000002", + "internal-api", + "used by Nexus when handling internal API requests", + ) +}); - /// Internal user used by Nexus to read privileged control plane data - pub static ref USER_INTERNAL_READ: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "4ead" looks like "read" - "001de000-05e4-4000-8000-000000004ead", - "internal-read", - "used by Nexus to read privileged control plane data", - ); +/// Internal user used by Nexus to read privileged control plane data +pub static USER_INTERNAL_READ: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "4ead" looks like "read" + "001de000-05e4-4000-8000-000000004ead", + "internal-read", + "used by Nexus to read privileged control plane data", + ) +}); - /// Internal user used by Nexus when recovering sagas - pub static ref USER_SAGA_RECOVERY: UserBuiltinConfig = - UserBuiltinConfig::new_static( - // "3a8a" looks a bit like "saga". - "001de000-05e4-4000-8000-000000003a8a", - "saga-recovery", - "used by Nexus when recovering sagas", - ); +/// Internal user used by Nexus when recovering sagas +pub static USER_SAGA_RECOVERY: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + // "3a8a" looks a bit like "saga". + "001de000-05e4-4000-8000-000000003a8a", + "saga-recovery", + "used by Nexus when recovering sagas", + ) +}); - /// Internal user used by Nexus when authenticating external requests - pub static ref USER_EXTERNAL_AUTHN: UserBuiltinConfig = - UserBuiltinConfig::new_static( - "001de000-05e4-4000-8000-000000000003", - "external-authn", - "used by Nexus when authenticating external requests", - ); -} +/// Internal user used by Nexus when authenticating external requests +pub static USER_EXTERNAL_AUTHN: Lazy = Lazy::new(|| { + UserBuiltinConfig::new_static( + "001de000-05e4-4000-8000-000000000003", + "external-authn", + "used by Nexus when authenticating external requests", + ) +}); #[cfg(test)] mod test { diff --git a/nexus/db-queries/src/db/fixed_data/vpc.rs b/nexus/db-queries/src/db/fixed_data/vpc.rs index 6571e5c5f9..c71b655ddc 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc.rs @@ -4,31 +4,35 @@ use crate::db; use crate::db::datastore::SERVICES_DB_NAME; -use lazy_static::lazy_static; use nexus_types::external_api::params; use omicron_common::address::SERVICE_VPC_IPV6_PREFIX; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in VPC for internal services on the rack. - pub static ref SERVICES_VPC_ID: uuid::Uuid = "001de000-074c-4000-8000-000000000000" +/// UUID of built-in VPC for internal services on the rack. +pub static SERVICES_VPC_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000000" .parse() - .expect("invalid uuid for builtin services vpc id"); + .expect("invalid uuid for builtin services vpc id") +}); - /// UUID of VpcRouter for built-in Services VPC. - pub static ref SERVICES_VPC_ROUTER_ID: uuid::Uuid = - "001de000-074c-4000-8000-000000000001" - .parse() - .expect("invalid uuid for builtin services vpc router id"); +/// UUID of VpcRouter for built-in Services VPC. +pub static SERVICES_VPC_ROUTER_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000001" + .parse() + .expect("invalid uuid for builtin services vpc router id") +}); - /// UUID of default route for built-in Services VPC. - pub static ref SERVICES_VPC_DEFAULT_ROUTE_ID: uuid::Uuid = - "001de000-074c-4000-8000-000000000002" - .parse() - .expect("invalid uuid for builtin services vpc default route id"); +/// UUID of default route for built-in Services VPC. +pub static SERVICES_VPC_DEFAULT_ROUTE_ID: Lazy = Lazy::new(|| { + "001de000-074c-4000-8000-000000000002" + .parse() + .expect("invalid uuid for builtin services vpc default route id") +}); - /// Built-in VPC for internal services on the rack. - pub static ref SERVICES_VPC: db::model::IncompleteVpc = db::model::IncompleteVpc::new( +/// Built-in VPC for internal services on the rack. +pub static SERVICES_VPC: Lazy = Lazy::new(|| { + db::model::IncompleteVpc::new( *SERVICES_VPC_ID, *super::project::SERVICES_PROJECT_ID, *SERVICES_VPC_ROUTER_ID, @@ -43,5 +47,5 @@ lazy_static! { ) // `IncompleteVpc::new` only fails if given an invalid `ipv6_prefix` // but we know `SERVICE_VPC_IPV6_PREFIX` is valid. - .unwrap(); -} + .unwrap() +}); diff --git a/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs b/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs index 3fae24abee..5062b1a11c 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc_firewall_rule.rs @@ -2,72 +2,63 @@ // 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 lazy_static::lazy_static; use nexus_types::identity::Resource; use omicron_common::api::external::{ L4PortRange, VpcFirewallRuleAction, VpcFirewallRuleDirection, VpcFirewallRuleFilter, VpcFirewallRulePriority, VpcFirewallRuleProtocol, VpcFirewallRuleStatus, VpcFirewallRuleTarget, VpcFirewallRuleUpdate, }; +use once_cell::sync::Lazy; -lazy_static! { - /// Built-in VPC firewall rule for External DNS. - pub static ref DNS_VPC_FW_RULE: VpcFirewallRuleUpdate = VpcFirewallRuleUpdate { +/// Built-in VPC firewall rule for External DNS. +pub static DNS_VPC_FW_RULE: Lazy = + Lazy::new(|| VpcFirewallRuleUpdate { name: "external-dns-inbound".parse().unwrap(), description: "allow inbound connections for DNS from anywhere" .to_string(), status: VpcFirewallRuleStatus::Enabled, direction: VpcFirewallRuleDirection::Inbound, - targets: vec![ - VpcFirewallRuleTarget::Subnet( - super::vpc_subnet::DNS_VPC_SUBNET.name().clone(), - ), - ], + targets: vec![VpcFirewallRuleTarget::Subnet( + super::vpc_subnet::DNS_VPC_SUBNET.name().clone(), + )], filters: VpcFirewallRuleFilter { hosts: None, protocols: Some(vec![VpcFirewallRuleProtocol::Udp]), - ports: Some( - vec![ - L4PortRange { - first: 53.try_into().unwrap(), - last: 53.try_into().unwrap(), - }, - ], - ), + ports: Some(vec![L4PortRange { + first: 53.try_into().unwrap(), + last: 53.try_into().unwrap(), + }]), }, action: VpcFirewallRuleAction::Allow, priority: VpcFirewallRulePriority(65534), - }; + }); - /// Built-in VPC firewall rule for Nexus. - pub static ref NEXUS_VPC_FW_RULE: VpcFirewallRuleUpdate = VpcFirewallRuleUpdate { +/// Built-in VPC firewall rule for Nexus. +pub static NEXUS_VPC_FW_RULE: Lazy = + Lazy::new(|| VpcFirewallRuleUpdate { name: "nexus-inbound".parse().unwrap(), - description: "allow inbound connections for console & api from anywhere" - .to_string(), + description: + "allow inbound connections for console & api from anywhere" + .to_string(), status: VpcFirewallRuleStatus::Enabled, direction: VpcFirewallRuleDirection::Inbound, - targets: vec![ - VpcFirewallRuleTarget::Subnet( - super::vpc_subnet::NEXUS_VPC_SUBNET.name().clone(), - ), - ], + targets: vec![VpcFirewallRuleTarget::Subnet( + super::vpc_subnet::NEXUS_VPC_SUBNET.name().clone(), + )], filters: VpcFirewallRuleFilter { hosts: None, protocols: Some(vec![VpcFirewallRuleProtocol::Tcp]), - ports: Some( - vec![ - L4PortRange { - first: 80.try_into().unwrap(), - last: 80.try_into().unwrap(), - }, - L4PortRange { - first: 443.try_into().unwrap(), - last: 443.try_into().unwrap(), - }, - ], - ), + ports: Some(vec![ + L4PortRange { + first: 80.try_into().unwrap(), + last: 80.try_into().unwrap(), + }, + L4PortRange { + first: 443.try_into().unwrap(), + last: 443.try_into().unwrap(), + }, + ]), }, action: VpcFirewallRuleAction::Allow, priority: VpcFirewallRulePriority(65534), - }; -} + }); diff --git a/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs b/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs index 59bc87b34c..c42d4121c9 100644 --- a/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs +++ b/nexus/db-queries/src/db/fixed_data/vpc_subnet.rs @@ -3,32 +3,37 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::db::model::VpcSubnet; -use lazy_static::lazy_static; use omicron_common::address::{ DNS_OPTE_IPV4_SUBNET, DNS_OPTE_IPV6_SUBNET, NEXUS_OPTE_IPV4_SUBNET, NEXUS_OPTE_IPV6_SUBNET, NTP_OPTE_IPV4_SUBNET, NTP_OPTE_IPV6_SUBNET, }; use omicron_common::api::external::IdentityMetadataCreateParams; +use once_cell::sync::Lazy; -lazy_static! { - /// UUID of built-in VPC Subnet for External DNS. - pub static ref DNS_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000001" +/// UUID of built-in VPC Subnet for External DNS. +pub static DNS_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000001" .parse() - .expect("invalid uuid for builtin external dns vpc subnet id"); + .expect("invalid uuid for builtin external dns vpc subnet id") +}); - /// UUID of built-in VPC Subnet for Nexus. - pub static ref NEXUS_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000002" +/// UUID of built-in VPC Subnet for Nexus. +pub static NEXUS_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000002" .parse() - .expect("invalid uuid for builtin nexus vpc subnet id"); + .expect("invalid uuid for builtin nexus vpc subnet id") +}); - /// UUID of built-in VPC Subnet for Boundary NTP. - pub static ref NTP_VPC_SUBNET_ID: uuid::Uuid = "001de000-c470-4000-8000-000000000003" +/// UUID of built-in VPC Subnet for Boundary NTP. +pub static NTP_VPC_SUBNET_ID: Lazy = Lazy::new(|| { + "001de000-c470-4000-8000-000000000003" .parse() - .expect("invalid uuid for builtin boundary ntp vpc subnet id"); + .expect("invalid uuid for builtin boundary ntp vpc subnet id") +}); - - /// Built-in VPC Subnet for External DNS. - pub static ref DNS_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for External DNS. +pub static DNS_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *DNS_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -38,10 +43,12 @@ lazy_static! { }, *DNS_OPTE_IPV4_SUBNET, *DNS_OPTE_IPV6_SUBNET, - ); + ) +}); - /// Built-in VPC Subnet for Nexus. - pub static ref NEXUS_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for Nexus. +pub static NEXUS_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *NEXUS_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -51,10 +58,12 @@ lazy_static! { }, *NEXUS_OPTE_IPV4_SUBNET, *NEXUS_OPTE_IPV6_SUBNET, - ); + ) +}); - /// Built-in VPC Subnet for Boundary NTP. - pub static ref NTP_VPC_SUBNET: VpcSubnet = VpcSubnet::new( +/// Built-in VPC Subnet for Boundary NTP. +pub static NTP_VPC_SUBNET: Lazy = Lazy::new(|| { + VpcSubnet::new( *NTP_VPC_SUBNET_ID, *super::vpc::SERVICES_VPC_ID, IdentityMetadataCreateParams { @@ -64,5 +73,5 @@ lazy_static! { }, *NTP_OPTE_IPV4_SUBNET, *NTP_OPTE_IPV6_SUBNET, - ); -} + ) +}); diff --git a/nexus/db-queries/src/db/queries/network_interface.rs b/nexus/db-queries/src/db/queries/network_interface.rs index 1dbe57da6f..6d00b4bc29 100644 --- a/nexus/db-queries/src/db/queries/network_interface.rs +++ b/nexus/db-queries/src/db/queries/network_interface.rs @@ -30,6 +30,7 @@ use nexus_db_model::NetworkInterfaceKindEnum; use omicron_common::api::external; use omicron_common::api::external::MacAddr; use omicron_common::nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES; +use once_cell::sync::Lazy; use std::net::IpAddr; use uuid::Uuid; @@ -42,36 +43,35 @@ pub(crate) const MAX_NICS: usize = 8; // These are sentinel values and other constants used to verify the state of the // system when operating on network interfaces -lazy_static::lazy_static! { - // States an instance must be in to operate on its network interfaces, in - // most situations. - static ref INSTANCE_STOPPED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Stopped); - - static ref INSTANCE_FAILED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Failed); - - // An instance can be in the creating state while we manipulate its - // interfaces. The intention is for this only to be the case during sagas. - static ref INSTANCE_CREATING: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Creating); - - // A sentinel value for the instance state when the instance actually does - // not exist. - static ref INSTANCE_DESTROYED: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Destroyed); - - // A sentinel value for the instance state when the instance has an active - // VMM, irrespective of that VMM's actual state. - static ref INSTANCE_RUNNING: db::model::InstanceState = - db::model::InstanceState(external::InstanceState::Running); - - static ref NO_INSTANCE_SENTINEL_STRING: String = - String::from(NO_INSTANCE_SENTINEL); - - static ref INSTANCE_BAD_STATE_SENTINEL_STRING: String = - String::from(INSTANCE_BAD_STATE_SENTINEL); -} + +// States an instance must be in to operate on its network interfaces, in +// most situations. +static INSTANCE_STOPPED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Stopped)); + +static INSTANCE_FAILED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Failed)); + +// An instance can be in the creating state while we manipulate its +// interfaces. The intention is for this only to be the case during sagas. +static INSTANCE_CREATING: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Creating)); + +// A sentinel value for the instance state when the instance actually does +// not exist. +static INSTANCE_DESTROYED: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Destroyed)); + +// A sentinel value for the instance state when the instance has an active +// VMM, irrespective of that VMM's actual state. +static INSTANCE_RUNNING: Lazy = + Lazy::new(|| db::model::InstanceState(external::InstanceState::Running)); + +static NO_INSTANCE_SENTINEL_STRING: Lazy = + Lazy::new(|| String::from(NO_INSTANCE_SENTINEL)); + +static INSTANCE_BAD_STATE_SENTINEL_STRING: Lazy = + Lazy::new(|| String::from(INSTANCE_BAD_STATE_SENTINEL)); // Uncastable sentinel used to detect when an instance exists, but is not // in the right state to have its network interfaces altered diff --git a/nexus/db-queries/src/db/saga_recovery.rs b/nexus/db-queries/src/db/saga_recovery.rs index 802093b889..55cda03c3c 100644 --- a/nexus/db-queries/src/db/saga_recovery.rs +++ b/nexus/db-queries/src/db/saga_recovery.rs @@ -305,9 +305,9 @@ mod test { use super::*; use crate::context::OpContext; use crate::db::test_utils::UnpluggableCockroachDbSecStore; - use lazy_static::lazy_static; use nexus_test_utils::db::test_setup_database; use omicron_test_utils::dev; + use once_cell::sync::Lazy; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use steno::{ new_action_noop_undo, Action, ActionContext, ActionError, @@ -376,12 +376,10 @@ mod test { type ExecContextType = TestContext; } - lazy_static! { - static ref ACTION_N1: Arc> = - new_action_noop_undo("n1_action", node_one); - static ref ACTION_N2: Arc> = - new_action_noop_undo("n2_action", node_two); - } + static ACTION_N1: Lazy>> = + Lazy::new(|| new_action_noop_undo("n1_action", node_one)); + static ACTION_N2: Lazy>> = + Lazy::new(|| new_action_noop_undo("n2_action", node_two)); fn registry_create() -> Arc> { let mut registry = ActionRegistry::new(); diff --git a/nexus/defaults/Cargo.toml b/nexus/defaults/Cargo.toml index 0724b5bf4d..535b78054b 100644 --- a/nexus/defaults/Cargo.toml +++ b/nexus/defaults/Cargo.toml @@ -6,7 +6,7 @@ license = "MPL-2.0" [dependencies] ipnetwork.workspace = true -lazy_static.workspace = true +once_cell.workspace = true rand.workspace = true serde_json.workspace = true diff --git a/nexus/defaults/src/lib.rs b/nexus/defaults/src/lib.rs index be1ce2193c..dd08b4e4ab 100644 --- a/nexus/defaults/src/lib.rs +++ b/nexus/defaults/src/lib.rs @@ -6,10 +6,10 @@ use ipnetwork::Ipv4Network; use ipnetwork::Ipv6Network; -use lazy_static::lazy_static; use omicron_common::api::external; use omicron_common::api::external::Ipv4Net; use omicron_common::api::external::Ipv6Net; +use once_cell::sync::Lazy; use std::net::Ipv4Addr; use std::net::Ipv6Addr; @@ -17,51 +17,51 @@ use std::net::Ipv6Addr; /// instance. pub const DEFAULT_PRIMARY_NIC_NAME: &str = "net0"; -lazy_static! { - /// The default IPv4 subnet range assigned to the default VPC Subnet, when - /// the VPC is created, if one is not provided in the request. See - /// for details. - pub static ref DEFAULT_VPC_SUBNET_IPV4_BLOCK: external::Ipv4Net = - Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 0, 0), 22).unwrap()); -} +/// The default IPv4 subnet range assigned to the default VPC Subnet, when +/// the VPC is created, if one is not provided in the request. See +/// for details. +pub static DEFAULT_VPC_SUBNET_IPV4_BLOCK: Lazy = + Lazy::new(|| { + Ipv4Net(Ipv4Network::new(Ipv4Addr::new(172, 30, 0, 0), 22).unwrap()) + }); -lazy_static! { - pub static ref DEFAULT_FIREWALL_RULES: external::VpcFirewallRuleUpdateParams = +pub static DEFAULT_FIREWALL_RULES: Lazy = + Lazy::new(|| { serde_json::from_str(r#"{ - "rules": [ - { - "name": "allow-internal-inbound", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "hosts": [ { "type": "vpc", "value": "default" } ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound traffic to all instances within the VPC if originated within the VPC" - }, - { - "name": "allow-ssh", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "ports": [ "22" ], "protocols": [ "TCP" ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound TCP connections on port 22 from anywhere" - }, - { - "name": "allow-icmp", - "status": "enabled", - "direction": "inbound", - "targets": [ { "type": "vpc", "value": "default" } ], - "filters": { "protocols": [ "ICMP" ] }, - "action": "allow", - "priority": 65534, - "description": "allow inbound ICMP traffic from anywhere" - } - ] - }"#).unwrap(); -} + "rules": [ + { + "name": "allow-internal-inbound", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "hosts": [ { "type": "vpc", "value": "default" } ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound traffic to all instances within the VPC if originated within the VPC" + }, + { + "name": "allow-ssh", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "ports": [ "22" ], "protocols": [ "TCP" ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound TCP connections on port 22 from anywhere" + }, + { + "name": "allow-icmp", + "status": "enabled", + "direction": "inbound", + "targets": [ { "type": "vpc", "value": "default" } ], + "filters": { "protocols": [ "ICMP" ] }, + "action": "allow", + "priority": 65534, + "description": "allow inbound ICMP traffic from anywhere" + } + ] + }"#).unwrap() + }); /// Generate a random VPC IPv6 prefix, in the range `fd00::/48`. pub fn random_vpc_ipv6_prefix() -> Result { diff --git a/nexus/src/external_api/console_api.rs b/nexus/src/external_api/console_api.rs index d779d34459..90450c3145 100644 --- a/nexus/src/external_api/console_api.rs +++ b/nexus/src/external_api/console_api.rs @@ -17,7 +17,6 @@ use dropshot::{ }; use http::{header, Response, StatusCode, Uri}; use hyper::Body; -use lazy_static::lazy_static; use mime_guess; use nexus_db_model::AuthenticationMode; use nexus_db_queries::authn::silos::IdentityProviderType; @@ -36,6 +35,7 @@ use nexus_types::external_api::params; use nexus_types::identity::Resource; use omicron_common::api::external::http_pagination::PaginatedBy; use omicron_common::api::external::{DataPageParams, Error, NameOrId}; +use once_cell::sync::Lazy; use parse_display::Display; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -810,15 +810,15 @@ fn not_found(internal_msg: &str) -> HttpError { HttpError::for_not_found(None, internal_msg.to_string()) } -lazy_static! { - static ref ALLOWED_EXTENSIONS: HashSet = HashSet::from( +static ALLOWED_EXTENSIONS: Lazy> = Lazy::new(|| { + HashSet::from( [ "js", "css", "html", "ico", "map", "otf", "png", "svg", "ttf", "txt", "webp", "woff", "woff2", ] - .map(|s| OsString::from(s)) - ); -} + .map(|s| OsString::from(s)), + ) +}); /// Starting from `root_dir`, follow the segments of `path` down the file tree /// until we find a file (or not). Do not follow symlinks. diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index 545129d567..be0ea2a3f5 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -11,7 +11,6 @@ use crate::integration_tests::unauthorized::HTTP_SERVER; use chrono::Utc; use http::method::Method; use internal_dns::names::DNS_ZONE_EXTERNAL_TESTING; -use lazy_static::lazy_static; use nexus_db_queries::authn; use nexus_db_queries::db::fixed_data::silo::DEFAULT_SILO; use nexus_db_queries::db::identity::Resource; @@ -38,225 +37,264 @@ use omicron_common::api::external::RouteTarget; use omicron_common::api::external::SemverVersion; use omicron_common::api::external::VpcFirewallRuleUpdateParams; use omicron_test_utils::certificates::CertificateChain; +use once_cell::sync::Lazy; use std::net::IpAddr; use std::net::Ipv4Addr; use std::str::FromStr; use uuid::Uuid; -lazy_static! { - pub static ref HARDWARE_RACK_URL: String = - format!("/v1/system/hardware/racks/{}", RACK_UUID); - pub static ref HARDWARE_UNINITIALIZED_SLEDS: String = - format!("/v1/system/hardware/sleds-uninitialized"); - pub static ref HARDWARE_SLED_URL: String = - format!("/v1/system/hardware/sleds/{}", SLED_AGENT_UUID); - pub static ref HARDWARE_SLED_PROVISION_STATE_URL: String = - format!("/v1/system/hardware/sleds/{}/provision-state", SLED_AGENT_UUID); - pub static ref DEMO_SLED_PROVISION_STATE: params::SledProvisionStateParams = +pub static HARDWARE_RACK_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/racks/{}", RACK_UUID)); +pub const HARDWARE_UNINITIALIZED_SLEDS: &'static str = + "/v1/system/hardware/sleds-uninitialized"; +pub static HARDWARE_SLED_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/sleds/{}", SLED_AGENT_UUID)); +pub static HARDWARE_SLED_PROVISION_STATE_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/provision-state", SLED_AGENT_UUID) +}); +pub static DEMO_SLED_PROVISION_STATE: Lazy = + Lazy::new(|| { params::SledProvisionStateParams { state: nexus_types::external_api::views::SledProvisionState::NonProvisionable, - }; - pub static ref HARDWARE_SWITCH_URL: String = - format!("/v1/system/hardware/switches/{}", SWITCH_UUID); - pub static ref HARDWARE_DISK_URL: String = - format!("/v1/system/hardware/disks"); - pub static ref HARDWARE_SLED_DISK_URL: String = - format!("/v1/system/hardware/sleds/{}/disks", SLED_AGENT_UUID); - - pub static ref SLED_INSTANCES_URL: String = - format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID); - - pub static ref DEMO_UNINITIALIZED_SLED: UninitializedSled = UninitializedSled { + } + }); + +pub static HARDWARE_SWITCH_URL: Lazy = + Lazy::new(|| format!("/v1/system/hardware/switches/{}", SWITCH_UUID)); +pub const HARDWARE_DISK_URL: &'static str = "/v1/system/hardware/disks"; +pub static HARDWARE_SLED_DISK_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/disks", SLED_AGENT_UUID) +}); + +pub static SLED_INSTANCES_URL: Lazy = Lazy::new(|| { + format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID) +}); +pub static DEMO_UNINITIALIZED_SLED: Lazy = + Lazy::new(|| UninitializedSled { baseboard: Baseboard { serial: "demo-serial".to_string(), part: "demo-part".to_string(), - revision: 6 + revision: 6, }, rack_id: Uuid::new_v4(), - cubby: 1 - }; - - // Global policy - pub static ref SYSTEM_POLICY_URL: &'static str = "/v1/system/policy"; - - // Silo used for testing - pub static ref DEMO_SILO_NAME: Name = "demo-silo".parse().unwrap(); - pub static ref DEMO_SILO_URL: String = - format!("/v1/system/silos/{}", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_POLICY_URL: String = - format!("/v1/system/silos/{}/policy", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_QUOTAS_URL: String = - format!("/v1/system/silos/{}/quotas", *DEMO_SILO_NAME); - pub static ref DEMO_SILO_CREATE: params::SiloCreate = - params::SiloCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SILO_NAME.clone(), - description: String::from(""), - }, - quotas: params::SiloQuotasCreate::arbitrarily_high_default(), - discoverable: true, - identity_mode: shared::SiloIdentityMode::SamlJit, - admin_group_name: None, - tls_certificates: vec![], - mapped_fleet_roles: Default::default(), - }; - - pub static ref DEMO_SILO_UTIL_URL: String = format!("/v1/system/utilization/silos/{}", *DEMO_SILO_NAME); - - // Use the default Silo for testing the local IdP - pub static ref DEMO_SILO_USERS_CREATE_URL: String = format!( + cubby: 1, + }); + +// Global policy +pub const SYSTEM_POLICY_URL: &'static str = "/v1/system/policy"; + +// Silo used for testing +pub static DEMO_SILO_NAME: Lazy = + Lazy::new(|| "demo-silo".parse().unwrap()); +pub static DEMO_SILO_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}", *DEMO_SILO_NAME)); +pub static DEMO_SILO_POLICY_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}/policy", *DEMO_SILO_NAME)); +pub static DEMO_SILO_QUOTAS_URL: Lazy = + Lazy::new(|| format!("/v1/system/silos/{}/quotas", *DEMO_SILO_NAME)); +pub static DEMO_SILO_CREATE: Lazy = + Lazy::new(|| params::SiloCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SILO_NAME.clone(), + description: String::from(""), + }, + quotas: params::SiloQuotasCreate::arbitrarily_high_default(), + discoverable: true, + identity_mode: shared::SiloIdentityMode::SamlJit, + admin_group_name: None, + tls_certificates: vec![], + mapped_fleet_roles: Default::default(), + }); + +pub static DEMO_SILO_UTIL_URL: Lazy = + Lazy::new(|| format!("/v1/system/utilization/silos/{}", *DEMO_SILO_NAME)); + +// Use the default Silo for testing the local IdP +pub static DEMO_SILO_USERS_CREATE_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users?silo={}", DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USERS_LIST_URL: String = format!( - "/v1/system/users?silo={}", - DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_GET_URL: String = format!( - "/v1/system/users/{{id}}?silo={}", - DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_DELETE_URL: String = format!( + ) +}); +pub static DEMO_SILO_USERS_LIST_URL: Lazy = Lazy::new(|| { + format!("/v1/system/users?silo={}", DEFAULT_SILO.identity().name,) +}); +pub static DEMO_SILO_USER_ID_GET_URL: Lazy = Lazy::new(|| { + format!("/v1/system/users/{{id}}?silo={}", DEFAULT_SILO.identity().name,) +}); +pub static DEMO_SILO_USER_ID_DELETE_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users/{{id}}?silo={}", DEFAULT_SILO.identity().name, - ); - pub static ref DEMO_SILO_USER_ID_SET_PASSWORD_URL: String = format!( + ) +}); +pub static DEMO_SILO_USER_ID_SET_PASSWORD_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/identity-providers/local/users/{{id}}/set-password?silo={}", DEFAULT_SILO.identity().name, - ); -} - -lazy_static! { - - // Project used for testing - pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); - pub static ref DEMO_PROJECT_URL: String = - format!("/v1/projects/{}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_SELECTOR: String = - format!("project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_POLICY_URL: String = - format!("/v1/projects/{}/policy", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_DISKS: String = - format!("/v1/disks?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_IMAGES: String = - format!("/v1/images?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_INSTANCES: String = format!("/v1/instances?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_SNAPSHOTS: String = format!("/v1/snapshots?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_VPCS: String = format!("/v1/vpcs?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_URL_FIPS: String = format!("/v1/floating-ips?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = - params::ProjectCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_PROJECT_NAME.clone(), - description: String::from(""), - }, - }; - - // VPC used for testing - pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); - pub static ref DEMO_VPC_URL: String = - format!("/v1/vpcs/{}?{}", *DEMO_VPC_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_VPC_SELECTOR: String = - format!("project={}&vpc={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); - pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = - format!("/v1/vpc-firewall-rules?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_URL_ROUTERS: String = - format!("/v1/vpc-routers?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_URL_SUBNETS: String = - format!("/v1/vpc-subnets?{}", *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_CREATE: params::VpcCreate = - params::VpcCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_NAME.clone(), - description: String::from(""), - }, - ipv6_prefix: None, - dns_name: DEMO_VPC_NAME.clone(), - }; - - // VPC Subnet used for testing - pub static ref DEMO_VPC_SUBNET_NAME: Name = - "demo-vpc-subnet".parse().unwrap(); - pub static ref DEMO_VPC_SUBNET_URL: String = - format!("/v1/vpc-subnets/{}?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_SUBNET_INTERFACES_URL: String = - format!("/v1/vpc-subnets/{}/network-interfaces?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR); - pub static ref DEMO_VPC_SUBNET_CREATE: params::VpcSubnetCreate = - params::VpcSubnetCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_SUBNET_NAME.clone(), - description: String::from(""), - }, - ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), - ipv6_block: None, - }; - - // VPC Router used for testing - pub static ref DEMO_VPC_ROUTER_NAME: Name = - "demo-vpc-router".parse().unwrap(); - pub static ref DEMO_VPC_ROUTER_URL: String = - format!("/v1/vpc-routers/{}?project={}&vpc={}", *DEMO_VPC_ROUTER_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME); - pub static ref DEMO_VPC_ROUTER_URL_ROUTES: String = - format!("/v1/vpc-router-routes?project={}&vpc={}&router={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); - pub static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = - params::VpcRouterCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_ROUTER_NAME.clone(), - description: String::from(""), - }, - }; - - // Router Route used for testing - pub static ref DEMO_ROUTER_ROUTE_NAME: Name = - "demo-router-route".parse().unwrap(); - pub static ref DEMO_ROUTER_ROUTE_URL: String = - format!("/v1/vpc-router-routes/{}?project={}&vpc={}&router={}", *DEMO_ROUTER_ROUTE_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME); - pub static ref DEMO_ROUTER_ROUTE_CREATE: params::RouterRouteCreate = - params::RouterRouteCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_ROUTER_ROUTE_NAME.clone(), - description: String::from(""), - }, - target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), - destination: RouteDestination::Subnet("loopback".parse().unwrap()), - }; - - // Disk used for testing - pub static ref DEMO_DISK_NAME: Name = "demo-disk".parse().unwrap(); - // TODO: Once we can test a URL multiple times we should also a case to exercise authz for disks filtered by instances - pub static ref DEMO_DISKS_URL: String = - format!("/v1/disks?{}", *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_DISK_URL: String = - format!("/v1/disks/{}?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_DISK_CREATE: params::DiskCreate = - params::DiskCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_DISK_NAME.clone(), - description: "".parse().unwrap(), - }, - disk_source: params::DiskSource::Blank { - block_size: params::BlockSize::try_from(4096).unwrap(), - }, - size: ByteCount::from_gibibytes_u32( - // divide by at least two to leave space for snapshot blocks - DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5 - ), - }; - pub static ref DEMO_DISK_METRICS_URL: String = - format!( - "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}&{}", - *DEMO_DISK_NAME, - Utc::now(), - Utc::now(), - *DEMO_PROJECT_SELECTOR, - ); - - // Related to importing blocks from an external source - pub static ref DEMO_IMPORT_DISK_NAME: Name = "demo-import-disk".parse().unwrap(); - pub static ref DEMO_IMPORT_DISK_URL: String = - format!("/v1/disks/{}?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_CREATE: params::DiskCreate = + ) +}); + +// Project used for testing +pub static DEMO_PROJECT_NAME: Lazy = + Lazy::new(|| "demo-project".parse().unwrap()); +pub static DEMO_PROJECT_URL: Lazy = + Lazy::new(|| format!("/v1/projects/{}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_SELECTOR: Lazy = + Lazy::new(|| format!("project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_POLICY_URL: Lazy = + Lazy::new(|| format!("/v1/projects/{}/policy", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_IMAGES: Lazy = + Lazy::new(|| format!("/v1/images?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_INSTANCES: Lazy = + Lazy::new(|| format!("/v1/instances?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_SNAPSHOTS: Lazy = + Lazy::new(|| format!("/v1/snapshots?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_VPCS: Lazy = + Lazy::new(|| format!("/v1/vpcs?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_URL_FIPS: Lazy = + Lazy::new(|| format!("/v1/floating-ips?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_CREATE: Lazy = + Lazy::new(|| params::ProjectCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_PROJECT_NAME.clone(), + description: String::from(""), + }, + }); + +// VPC used for testing +pub static DEMO_VPC_NAME: Lazy = + Lazy::new(|| "demo-vpc".parse().unwrap()); +pub static DEMO_VPC_URL: Lazy = Lazy::new(|| { + format!("/v1/vpcs/{}?{}", *DEMO_VPC_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_VPC_SELECTOR: Lazy = Lazy::new(|| { + format!("project={}&vpc={}", *DEMO_PROJECT_NAME, *DEMO_VPC_NAME) +}); +pub static DEMO_VPC_URL_FIREWALL_RULES: Lazy = + Lazy::new(|| format!("/v1/vpc-firewall-rules?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_URL_ROUTERS: Lazy = + Lazy::new(|| format!("/v1/vpc-routers?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_URL_SUBNETS: Lazy = + Lazy::new(|| format!("/v1/vpc-subnets?{}", *DEMO_VPC_SELECTOR)); +pub static DEMO_VPC_CREATE: Lazy = + Lazy::new(|| params::VpcCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_NAME.clone(), + description: String::from(""), + }, + ipv6_prefix: None, + dns_name: DEMO_VPC_NAME.clone(), + }); + +// VPC Subnet used for testing +pub static DEMO_VPC_SUBNET_NAME: Lazy = + Lazy::new(|| "demo-vpc-subnet".parse().unwrap()); +pub static DEMO_VPC_SUBNET_URL: Lazy = Lazy::new(|| { + format!("/v1/vpc-subnets/{}?{}", *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR) +}); +pub static DEMO_VPC_SUBNET_INTERFACES_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-subnets/{}/network-interfaces?{}", + *DEMO_VPC_SUBNET_NAME, *DEMO_VPC_SELECTOR + ) +}); +pub static DEMO_VPC_SUBNET_CREATE: Lazy = + Lazy::new(|| params::VpcSubnetCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_SUBNET_NAME.clone(), + description: String::from(""), + }, + ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), + ipv6_block: None, + }); + +// VPC Router used for testing +pub static DEMO_VPC_ROUTER_NAME: Lazy = + Lazy::new(|| "demo-vpc-router".parse().unwrap()); +pub static DEMO_VPC_ROUTER_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-routers/{}?project={}&vpc={}", + *DEMO_VPC_ROUTER_NAME, *DEMO_PROJECT_NAME, *DEMO_VPC_NAME + ) +}); +pub static DEMO_VPC_ROUTER_URL_ROUTES: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-router-routes?project={}&vpc={}&router={}", + *DEMO_PROJECT_NAME, *DEMO_VPC_NAME, *DEMO_VPC_ROUTER_NAME + ) +}); +pub static DEMO_VPC_ROUTER_CREATE: Lazy = + Lazy::new(|| params::VpcRouterCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_ROUTER_NAME.clone(), + description: String::from(""), + }, + }); + +// Router Route used for testing +pub static DEMO_ROUTER_ROUTE_NAME: Lazy = + Lazy::new(|| "demo-router-route".parse().unwrap()); +pub static DEMO_ROUTER_ROUTE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/vpc-router-routes/{}?project={}&vpc={}&router={}", + *DEMO_ROUTER_ROUTE_NAME, + *DEMO_PROJECT_NAME, + *DEMO_VPC_NAME, + *DEMO_VPC_ROUTER_NAME + ) +}); +pub static DEMO_ROUTER_ROUTE_CREATE: Lazy = + Lazy::new(|| params::RouterRouteCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_ROUTER_ROUTE_NAME.clone(), + description: String::from(""), + }, + target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), + destination: RouteDestination::Subnet("loopback".parse().unwrap()), + }); + +// Disk used for testing +pub static DEMO_DISK_NAME: Lazy = + Lazy::new(|| "demo-disk".parse().unwrap()); + +// TODO: Once we can test a URL multiple times we should also a case to exercise +// authz for disks filtered by instances +pub static DEMO_DISKS_URL: Lazy = + Lazy::new(|| format!("/v1/disks?{}", *DEMO_PROJECT_SELECTOR)); +pub static DEMO_DISK_URL: Lazy = Lazy::new(|| { + format!("/v1/disks/{}?{}", *DEMO_DISK_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_DISK_CREATE: Lazy = Lazy::new(|| { + params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_DISK_NAME.clone(), + description: "".parse().unwrap(), + }, + disk_source: params::DiskSource::Blank { + block_size: params::BlockSize::try_from(4096).unwrap(), + }, + size: ByteCount::from_gibibytes_u32( + // divide by at least two to leave space for snapshot blocks + DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5, + ), + } +}); +pub static DEMO_DISK_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/metrics/activated?start_time={:?}&end_time={:?}&{}", + *DEMO_DISK_NAME, + Utc::now(), + Utc::now(), + *DEMO_PROJECT_SELECTOR, + ) +}); + +// Related to importing blocks from an external source +pub static DEMO_IMPORT_DISK_NAME: Lazy = + Lazy::new(|| "demo-import-disk".parse().unwrap()); +pub static DEMO_IMPORT_DISK_CREATE: Lazy = + Lazy::new(|| { params::DiskCreate { identity: IdentityMetadataCreateParams { name: DEMO_IMPORT_DISK_NAME.clone(), @@ -267,381 +305,486 @@ lazy_static! { }, size: ByteCount::from_gibibytes_u32( // divide by at least two to leave space for snapshot blocks - DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5 + DiskTest::DEFAULT_ZPOOL_SIZE_GIB / 5, ), - }; - - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_START_URL: String = - format!("/v1/disks/{}/bulk-write-start?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_URL: String = - format!("/v1/disks/{}/bulk-write?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_BULK_WRITE_STOP_URL: String = - format!("/v1/disks/{}/bulk-write-stop?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_IMPORT_DISK_FINALIZE_URL: String = - format!("/v1/disks/{}/finalize?{}", *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR); -} - -// Separate lazy_static! blocks to avoid hitting some recursion limit when -// compiling -lazy_static! { - // Instance used for testing - pub static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); - pub static ref DEMO_INSTANCE_SELECTOR: String = format!("{}&instance={}", *DEMO_PROJECT_SELECTOR, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_URL: String = - format!("/v1/instances/{}?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_START_URL: String = - format!("/v1/instances/{}/start?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_STOP_URL: String = - format!("/v1/instances/{}/stop?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_REBOOT_URL: String = - format!("/v1/instances/{}/reboot?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_MIGRATE_URL: String = - format!("/v1/instances/{}/migrate?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_SERIAL_URL: String = - format!("/v1/instances/{}/serial-console?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_SERIAL_STREAM_URL: String = - format!("/v1/instances/{}/serial-console/stream?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_INSTANCE_DISKS_URL: String = - format!("/v1/instances/{}/disks?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = - format!("/v1/instances/{}/disks/attach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = - format!("/v1/instances/{}/disks/detach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - - pub static ref DEMO_INSTANCE_NICS_URL: String = - format!("/v1/network-interfaces?project={}&instance={}", *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_EXTERNAL_IPS_URL: String = - format!("/v1/instances/{}/external-ips?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = - params::InstanceCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_INSTANCE_NAME.clone(), - description: String::from(""), - }, - ncpus: InstanceCpuCount(1), - memory: ByteCount::from_gibibytes_u32(16), - hostname: String::from("demo-instance"), - user_data: vec![], - network_interfaces: - params::InstanceNetworkInterfaceAttachment::Default, - external_ips: vec![ - params::ExternalIpCreate::Ephemeral { pool_name: Some(DEMO_IP_POOL_NAME.clone()) } - ], - disks: vec![], - start: true, - }; - - // The instance needs a network interface, too. - pub static ref DEMO_INSTANCE_NIC_NAME: Name = - nexus_defaults::DEFAULT_PRIMARY_NIC_NAME.parse().unwrap(); - pub static ref DEMO_INSTANCE_NIC_URL: String = - format!("/v1/network-interfaces/{}?project={}&instance={}", *DEMO_INSTANCE_NIC_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME); - pub static ref DEMO_INSTANCE_NIC_CREATE: params::InstanceNetworkInterfaceCreate = - params::InstanceNetworkInterfaceCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_INSTANCE_NIC_NAME.clone(), - description: String::from(""), - }, - vpc_name: DEMO_VPC_NAME.clone(), - subnet_name: DEMO_VPC_SUBNET_NAME.clone(), - ip: None, - }; - pub static ref DEMO_INSTANCE_NIC_PUT: params::InstanceNetworkInterfaceUpdate = { - params::InstanceNetworkInterfaceUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some(String::from("an updated description")), - }, - primary: false, } - }; -} - -lazy_static! { - pub static ref DEMO_CERTIFICATE_NAME: Name = - "demo-certificate".parse().unwrap(); - pub static ref DEMO_CERTIFICATES_URL: String = format!("/v1/certificates"); - pub static ref DEMO_CERTIFICATE_URL: String = - format!("/v1/certificates/demo-certificate"); - pub static ref DEMO_CERTIFICATE: CertificateChain = - CertificateChain::new(format!("*.sys.{DNS_ZONE_EXTERNAL_TESTING}")); - pub static ref DEMO_CERTIFICATE_CREATE: params::CertificateCreate = - params::CertificateCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_CERTIFICATE_NAME.clone(), - description: String::from(""), - }, - cert: DEMO_CERTIFICATE.cert_chain_as_pem(), - key: DEMO_CERTIFICATE.end_cert_private_key_as_pem(), - service: shared::ServiceUsingCertificate::ExternalApi, - }; -} - -lazy_static! { - pub static ref DEMO_SWITCH_PORT_URL: String = - format!("/v1/system/hardware/switch-port"); - - pub static ref DEMO_SWITCH_PORT_SETTINGS_APPLY_URL: String = + }); +pub static DEMO_IMPORT_DISK_BULK_WRITE_START_URL: Lazy = + Lazy::new(|| { format!( - "/v1/system/hardware/switch-port/qsfp7/settings?rack_id={}&switch_location={}", - uuid::Uuid::new_v4(), - "switch0", - ); - - pub static ref DEMO_SWITCH_PORT_SETTINGS: params::SwitchPortApplySettings = - params::SwitchPortApplySettings { - port_settings: NameOrId::Name("portofino".parse().unwrap()), - }; -} - -lazy_static! { - pub static ref DEMO_LOOPBACK_CREATE_URL: String = - "/v1/system/networking/loopback-address".into(); - pub static ref DEMO_LOOPBACK_URL: String = format!( + "/v1/disks/{}/bulk-write-start?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) + }); +pub static DEMO_IMPORT_DISK_BULK_WRITE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/bulk-write?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_IMPORT_DISK_BULK_WRITE_STOP_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/disks/{}/bulk-write-stop?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) + }); +pub static DEMO_IMPORT_DISK_FINALIZE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/disks/{}/finalize?{}", + *DEMO_IMPORT_DISK_NAME, *DEMO_PROJECT_SELECTOR + ) +}); + +// Instance used for testing +pub static DEMO_INSTANCE_NAME: Lazy = + Lazy::new(|| "demo-instance".parse().unwrap()); +pub static DEMO_INSTANCE_URL: Lazy = Lazy::new(|| { + format!("/v1/instances/{}?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR) +}); +pub static DEMO_INSTANCE_START_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/start?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_STOP_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/stop?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_REBOOT_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/reboot?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_MIGRATE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/migrate?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_SERIAL_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/serial-console?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_SERIAL_STREAM_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/serial-console/stream?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_ATTACH_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks/attach?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_DISKS_DETACH_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/disks/detach?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_NICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/network-interfaces?project={}&instance={}", + *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME + ) +}); +pub static DEMO_INSTANCE_EXTERNAL_IPS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/instances/{}/external-ips?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) +}); +pub static DEMO_INSTANCE_CREATE: Lazy = + Lazy::new(|| params::InstanceCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_INSTANCE_NAME.clone(), + description: String::from(""), + }, + ncpus: InstanceCpuCount(1), + memory: ByteCount::from_gibibytes_u32(16), + hostname: String::from("demo-instance"), + user_data: vec![], + network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, + external_ips: vec![params::ExternalIpCreate::Ephemeral { + pool_name: Some(DEMO_IP_POOL_NAME.clone()), + }], + disks: vec![], + start: true, + }); + +// The instance needs a network interface, too. +pub static DEMO_INSTANCE_NIC_NAME: Lazy = + Lazy::new(|| nexus_defaults::DEFAULT_PRIMARY_NIC_NAME.parse().unwrap()); +pub static DEMO_INSTANCE_NIC_URL: Lazy = Lazy::new(|| { + format!( + "/v1/network-interfaces/{}?project={}&instance={}", + *DEMO_INSTANCE_NIC_NAME, *DEMO_PROJECT_NAME, *DEMO_INSTANCE_NAME + ) +}); +pub static DEMO_INSTANCE_NIC_CREATE: Lazy< + params::InstanceNetworkInterfaceCreate, +> = Lazy::new(|| params::InstanceNetworkInterfaceCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_INSTANCE_NIC_NAME.clone(), + description: String::from(""), + }, + vpc_name: DEMO_VPC_NAME.clone(), + subnet_name: DEMO_VPC_SUBNET_NAME.clone(), + ip: None, +}); +pub static DEMO_INSTANCE_NIC_PUT: Lazy = + Lazy::new(|| params::InstanceNetworkInterfaceUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some(String::from("an updated description")), + }, + primary: false, + }); + +pub static DEMO_CERTIFICATE_NAME: Lazy = + Lazy::new(|| "demo-certificate".parse().unwrap()); +pub const DEMO_CERTIFICATES_URL: &'static str = "/v1/certificates"; +pub const DEMO_CERTIFICATE_URL: &'static str = + "/v1/certificates/demo-certificate"; +pub static DEMO_CERTIFICATE: Lazy = Lazy::new(|| { + CertificateChain::new(format!("*.sys.{DNS_ZONE_EXTERNAL_TESTING}")) +}); +pub static DEMO_CERTIFICATE_CREATE: Lazy = + Lazy::new(|| params::CertificateCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_CERTIFICATE_NAME.clone(), + description: String::from(""), + }, + cert: DEMO_CERTIFICATE.cert_chain_as_pem(), + key: DEMO_CERTIFICATE.end_cert_private_key_as_pem(), + service: shared::ServiceUsingCertificate::ExternalApi, + }); + +pub const DEMO_SWITCH_PORT_URL: &'static str = + "/v1/system/hardware/switch-port"; +pub static DEMO_SWITCH_PORT_SETTINGS_APPLY_URL: Lazy = Lazy::new( + || { + format!( + "/v1/system/hardware/switch-port/qsfp7/settings?rack_id={}&switch_location={}", + uuid::Uuid::new_v4(), + "switch0", + ) + }, +); +pub static DEMO_SWITCH_PORT_SETTINGS: Lazy = + Lazy::new(|| params::SwitchPortApplySettings { + port_settings: NameOrId::Name("portofino".parse().unwrap()), + }); + +pub static DEMO_LOOPBACK_CREATE_URL: Lazy = + Lazy::new(|| "/v1/system/networking/loopback-address".into()); +pub static DEMO_LOOPBACK_URL: Lazy = Lazy::new(|| { + format!( "/v1/system/networking/loopback-address/{}/{}/{}", uuid::Uuid::new_v4(), "switch0", "203.0.113.99/24", - ); - pub static ref DEMO_LOOPBACK_CREATE: params::LoopbackAddressCreate = - params::LoopbackAddressCreate { - address_lot: NameOrId::Name("parkinglot".parse().unwrap()), - rack_id: uuid::Uuid::new_v4(), - switch_location: "switch0".parse().unwrap(), - address: "203.0.113.99".parse().unwrap(), - mask: 24, - anycast: false, - }; -} - -lazy_static! { - pub static ref DEMO_SWITCH_PORT_SETTINGS_URL: String = format!( - "/v1/system/networking/switch-port-settings?port_settings=portofino" - ); - pub static ref DEMO_SWITCH_PORT_SETTINGS_INFO_URL: String = - format!("/v1/system/networking/switch-port-settings/protofino"); - pub static ref DEMO_SWITCH_PORT_SETTINGS_CREATE: params::SwitchPortSettingsCreate = - params::SwitchPortSettingsCreate::new(IdentityMetadataCreateParams { - name: "portofino".parse().unwrap(), - description: "just a port".into(), - }); -} - -lazy_static! { - pub static ref DEMO_ADDRESS_LOTS_URL: String = - format!("/v1/system/networking/address-lot"); - pub static ref DEMO_ADDRESS_LOT_URL: String = - format!("/v1/system/networking/address-lot/parkinglot"); - pub static ref DEMO_ADDRESS_LOT_BLOCKS_URL: String = - format!("/v1/system/networking/address-lot/parkinglot/blocks"); - pub static ref DEMO_ADDRESS_LOT_CREATE: params::AddressLotCreate = - params::AddressLotCreate { - identity: IdentityMetadataCreateParams { - name: "parkinglot".parse().unwrap(), - description: "an address parking lot".into(), - }, - kind: AddressLotKind::Infra, - blocks: vec![params::AddressLotBlockCreate { - first_address: "203.0.113.10".parse().unwrap(), - last_address: "203.0.113.20".parse().unwrap(), - }], - }; -} - -lazy_static! { - pub static ref DEMO_BGP_CONFIG_CREATE_URL: String = - format!("/v1/system/networking/bgp?name_or_id=as47"); - pub static ref DEMO_BGP_CONFIG: params::BgpConfigCreate = - params::BgpConfigCreate { - identity: IdentityMetadataCreateParams { - name: "as47".parse().unwrap(), - description: "BGP config for AS47".into(), - }, - bgp_announce_set_id: NameOrId::Name("instances".parse().unwrap()), - asn: 47, - vrf: None, - }; - pub static ref DEMO_BGP_ANNOUNCE_SET_URL: String = - format!("/v1/system/networking/bgp-announce?name_or_id=a-bag-of-addrs"); - pub static ref DEMO_BGP_ANNOUNCE: params::BgpAnnounceSetCreate = - params::BgpAnnounceSetCreate { - identity: IdentityMetadataCreateParams { - name: "a-bag-of-addrs".parse().unwrap(), - description: "a bag of addrs".into(), - }, - announcement: vec![params::BgpAnnouncementCreate { - address_lot_block: NameOrId::Name( - "some-block".parse().unwrap(), - ), - network: "10.0.0.0/16".parse().unwrap(), - }], - }; - pub static ref DEMO_BGP_STATUS_URL: String = - format!("/v1/system/networking/bgp-status"); - pub static ref DEMO_BGP_ROUTES_IPV4_URL: String = - format!("/v1/system/networking/bgp-routes-ipv4?asn=47"); -} + ) +}); +pub static DEMO_LOOPBACK_CREATE: Lazy = + Lazy::new(|| params::LoopbackAddressCreate { + address_lot: NameOrId::Name("parkinglot".parse().unwrap()), + rack_id: uuid::Uuid::new_v4(), + switch_location: "switch0".parse().unwrap(), + address: "203.0.113.99".parse().unwrap(), + mask: 24, + anycast: false, + }); + +pub const DEMO_SWITCH_PORT_SETTINGS_URL: &'static str = + "/v1/system/networking/switch-port-settings?port_settings=portofino"; +pub const DEMO_SWITCH_PORT_SETTINGS_INFO_URL: &'static str = + "/v1/system/networking/switch-port-settings/protofino"; +pub static DEMO_SWITCH_PORT_SETTINGS_CREATE: Lazy< + params::SwitchPortSettingsCreate, +> = Lazy::new(|| { + params::SwitchPortSettingsCreate::new(IdentityMetadataCreateParams { + name: "portofino".parse().unwrap(), + description: "just a port".into(), + }) +}); + +pub const DEMO_ADDRESS_LOTS_URL: &'static str = + "/v1/system/networking/address-lot"; +pub const DEMO_ADDRESS_LOT_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot"; +pub const DEMO_ADDRESS_LOT_BLOCKS_URL: &'static str = + "/v1/system/networking/address-lot/parkinglot/blocks"; +pub static DEMO_ADDRESS_LOT_CREATE: Lazy = + Lazy::new(|| params::AddressLotCreate { + identity: IdentityMetadataCreateParams { + name: "parkinglot".parse().unwrap(), + description: "an address parking lot".into(), + }, + kind: AddressLotKind::Infra, + blocks: vec![params::AddressLotBlockCreate { + first_address: "203.0.113.10".parse().unwrap(), + last_address: "203.0.113.20".parse().unwrap(), + }], + }); + +pub const DEMO_BGP_CONFIG_CREATE_URL: &'static str = + "/v1/system/networking/bgp?name_or_id=as47"; +pub static DEMO_BGP_CONFIG: Lazy = + Lazy::new(|| params::BgpConfigCreate { + identity: IdentityMetadataCreateParams { + name: "as47".parse().unwrap(), + description: "BGP config for AS47".into(), + }, + bgp_announce_set_id: NameOrId::Name("instances".parse().unwrap()), + asn: 47, + vrf: None, + }); +pub const DEMO_BGP_ANNOUNCE_SET_URL: &'static str = + "/v1/system/networking/bgp-announce?name_or_id=a-bag-of-addrs"; +pub static DEMO_BGP_ANNOUNCE: Lazy = + Lazy::new(|| params::BgpAnnounceSetCreate { + identity: IdentityMetadataCreateParams { + name: "a-bag-of-addrs".parse().unwrap(), + description: "a bag of addrs".into(), + }, + announcement: vec![params::BgpAnnouncementCreate { + address_lot_block: NameOrId::Name("some-block".parse().unwrap()), + network: "10.0.0.0/16".parse().unwrap(), + }], + }); +pub const DEMO_BGP_STATUS_URL: &'static str = + "/v1/system/networking/bgp-status"; +pub const DEMO_BGP_ROUTES_IPV4_URL: &'static str = + "/v1/system/networking/bgp-routes-ipv4?asn=47"; + +// Project Images +pub static DEMO_IMAGE_NAME: Lazy = + Lazy::new(|| "demo-image".parse().unwrap()); +pub static DEMO_PROJECT_IMAGES_URL: Lazy = + Lazy::new(|| format!("/v1/images?project={}", *DEMO_PROJECT_NAME)); +pub static DEMO_PROJECT_IMAGE_URL: Lazy = Lazy::new(|| { + format!("/v1/images/{}?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME) +}); +pub static DEMO_PROJECT_PROMOTE_IMAGE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/images/{}/promote?project={}", + *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME + ) +}); + +pub static DEMO_SILO_DEMOTE_IMAGE_URL: Lazy = Lazy::new(|| { + format!( + "/v1/images/{}/demote?project={}", + *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME + ) +}); + +pub static DEMO_IMAGE_CREATE: Lazy = + Lazy::new(|| params::ImageCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_IMAGE_NAME.clone(), + description: String::from(""), + }, + source: params::ImageSource::YouCanBootAnythingAsLongAsItsAlpine, + os: "fake-os".to_string(), + version: "1.0".to_string(), + }); + +// IP Pools +pub static DEMO_IP_POOLS_PROJ_URL: Lazy = + Lazy::new(|| format!("/v1/ip-pools?project={}", *DEMO_PROJECT_NAME)); +pub const DEMO_IP_POOLS_URL: &'static str = "/v1/system/ip-pools"; +pub static DEMO_IP_POOL_NAME: Lazy = + Lazy::new(|| "default".parse().unwrap()); +pub static DEMO_IP_POOL_CREATE: Lazy = + Lazy::new(|| params::IpPoolCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_IP_POOL_NAME.clone(), + description: String::from("an IP pool"), + }, + silo: None, + is_default: true, + }); +pub static DEMO_IP_POOL_PROJ_URL: Lazy = Lazy::new(|| { + format!( + "/v1/ip-pools/{}?project={}", + *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME + ) +}); +pub static DEMO_IP_POOL_URL: Lazy = + Lazy::new(|| format!("/v1/system/ip-pools/{}", *DEMO_IP_POOL_NAME)); +pub static DEMO_IP_POOL_UPDATE: Lazy = + Lazy::new(|| params::IpPoolUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some(String::from("a new IP pool")), + }, + }); +pub static DEMO_IP_POOL_RANGE: Lazy = Lazy::new(|| { + IpRange::V4( + Ipv4Range::new( + std::net::Ipv4Addr::new(10, 0, 0, 0), + std::net::Ipv4Addr::new(10, 0, 0, 255), + ) + .unwrap(), + ) +}); +pub static DEMO_IP_POOL_RANGES_URL: Lazy = + Lazy::new(|| format!("{}/ranges", *DEMO_IP_POOL_URL)); +pub static DEMO_IP_POOL_RANGES_ADD_URL: Lazy = + Lazy::new(|| format!("{}/add", *DEMO_IP_POOL_RANGES_URL)); +pub static DEMO_IP_POOL_RANGES_DEL_URL: Lazy = + Lazy::new(|| format!("{}/remove", *DEMO_IP_POOL_RANGES_URL)); + +// IP Pools (Services) +pub const DEMO_IP_POOL_SERVICE_URL: &'static str = + "/v1/system/ip-pools-service"; +pub static DEMO_IP_POOL_SERVICE_RANGES_URL: Lazy = + Lazy::new(|| format!("{}/ranges", DEMO_IP_POOL_SERVICE_URL)); +pub static DEMO_IP_POOL_SERVICE_RANGES_ADD_URL: Lazy = + Lazy::new(|| format!("{}/add", *DEMO_IP_POOL_SERVICE_RANGES_URL)); +pub static DEMO_IP_POOL_SERVICE_RANGES_DEL_URL: Lazy = + Lazy::new(|| format!("{}/remove", *DEMO_IP_POOL_SERVICE_RANGES_URL)); + +// Snapshots +pub static DEMO_SNAPSHOT_NAME: Lazy = + Lazy::new(|| "demo-snapshot".parse().unwrap()); +pub static DEMO_SNAPSHOT_URL: Lazy = Lazy::new(|| { + format!( + "/v1/snapshots/{}?project={}", + *DEMO_SNAPSHOT_NAME, *DEMO_PROJECT_NAME + ) +}); +pub static DEMO_SNAPSHOT_CREATE: Lazy = + Lazy::new(|| params::SnapshotCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SNAPSHOT_NAME.clone(), + description: String::from(""), + }, + disk: DEMO_DISK_NAME.clone().into(), + }); -lazy_static! { - // Project Images - pub static ref DEMO_IMAGE_NAME: Name = "demo-image".parse().unwrap(); - pub static ref DEMO_PROJECT_IMAGES_URL: String = - format!("/v1/images?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_IMAGE_URL: String = - format!("/v1/images/{}?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_PROJECT_PROMOTE_IMAGE_URL: String = - format!("/v1/images/{}/promote?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_SILO_DEMOTE_IMAGE_URL: String = - format!("/v1/images/{}/demote?project={}", *DEMO_IMAGE_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_IMAGE_CREATE: params::ImageCreate = - params::ImageCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_IMAGE_NAME.clone(), - description: String::from(""), - }, - source: params::ImageSource::YouCanBootAnythingAsLongAsItsAlpine, - os: "fake-os".to_string(), - version: "1.0".to_string() - }; - - // IP Pools - pub static ref DEMO_IP_POOLS_PROJ_URL: String = - format!("/v1/ip-pools?project={}", *DEMO_PROJECT_NAME); - pub static ref DEMO_IP_POOLS_URL: &'static str = "/v1/system/ip-pools"; - pub static ref DEMO_IP_POOL_NAME: Name = "default".parse().unwrap(); - pub static ref DEMO_IP_POOL_CREATE: params::IpPoolCreate = - params::IpPoolCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_IP_POOL_NAME.clone(), - description: String::from("an IP pool"), - }, - silo: None, - is_default: true, - }; - pub static ref DEMO_IP_POOL_PROJ_URL: String = - format!("/v1/ip-pools/{}?project={}", *DEMO_IP_POOL_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_IP_POOL_URL: String = format!("/v1/system/ip-pools/{}", *DEMO_IP_POOL_NAME); - pub static ref DEMO_IP_POOL_UPDATE: params::IpPoolUpdate = - params::IpPoolUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some(String::from("a new IP pool")), - }, - }; - pub static ref DEMO_IP_POOL_RANGE: IpRange = IpRange::V4(Ipv4Range::new( - std::net::Ipv4Addr::new(10, 0, 0, 0), - std::net::Ipv4Addr::new(10, 0, 0, 255), - ).unwrap()); - pub static ref DEMO_IP_POOL_RANGES_URL: String = format!("{}/ranges", *DEMO_IP_POOL_URL); - pub static ref DEMO_IP_POOL_RANGES_ADD_URL: String = format!("{}/add", *DEMO_IP_POOL_RANGES_URL); - pub static ref DEMO_IP_POOL_RANGES_DEL_URL: String = format!("{}/remove", *DEMO_IP_POOL_RANGES_URL); - - // IP Pools (Services) - pub static ref DEMO_IP_POOL_SERVICE_URL: &'static str = "/v1/system/ip-pools-service"; - pub static ref DEMO_IP_POOL_SERVICE_RANGES_URL: String = format!("{}/ranges", *DEMO_IP_POOL_SERVICE_URL); - pub static ref DEMO_IP_POOL_SERVICE_RANGES_ADD_URL: String = format!("{}/add", *DEMO_IP_POOL_SERVICE_RANGES_URL); - pub static ref DEMO_IP_POOL_SERVICE_RANGES_DEL_URL: String = format!("{}/remove", *DEMO_IP_POOL_SERVICE_RANGES_URL); - - // Snapshots - pub static ref DEMO_SNAPSHOT_NAME: Name = "demo-snapshot".parse().unwrap(); - pub static ref DEMO_SNAPSHOT_URL: String = - format!("/v1/snapshots/{}?project={}", *DEMO_SNAPSHOT_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_SNAPSHOT_CREATE: params::SnapshotCreate = - params::SnapshotCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SNAPSHOT_NAME.clone(), - description: String::from(""), - }, - disk: DEMO_DISK_NAME.clone().into(), - }; +// SSH keys +pub const DEMO_SSHKEYS_URL: &'static str = "/v1/me/ssh-keys"; +pub static DEMO_SSHKEY_NAME: Lazy = + Lazy::new(|| "aaaaa-ssh-key".parse().unwrap()); - // SSH keys - pub static ref DEMO_SSHKEYS_URL: &'static str = "/v1/me/ssh-keys"; - pub static ref DEMO_SSHKEY_NAME: Name = "aaaaa-ssh-key".parse().unwrap(); - pub static ref DEMO_SSHKEY_CREATE: params::SshKeyCreate = params::SshKeyCreate { +pub static DEMO_SSHKEY_CREATE: Lazy = + Lazy::new(|| params::SshKeyCreate { identity: IdentityMetadataCreateParams { name: DEMO_SSHKEY_NAME.clone(), description: "a demo key".to_string(), }, public_key: "AAAAAAAAAAAAAAA".to_string(), - }; + }); - pub static ref DEMO_SPECIFIC_SSHKEY_URL: String = - format!("{}/{}", *DEMO_SSHKEYS_URL, *DEMO_SSHKEY_NAME); +pub static DEMO_SPECIFIC_SSHKEY_URL: Lazy = + Lazy::new(|| format!("{}/{}", DEMO_SSHKEYS_URL, *DEMO_SSHKEY_NAME)); - // System update +// System update - pub static ref DEMO_SYSTEM_UPDATE_PARAMS: params::SystemUpdatePath = params::SystemUpdatePath { - version: SemverVersion::new(1,0,0), - }; -} +pub static DEMO_SYSTEM_UPDATE_PARAMS: Lazy = + Lazy::new(|| params::SystemUpdatePath { + version: SemverVersion::new(1, 0, 0), + }); -lazy_static! { - // Project Floating IPs - pub static ref DEMO_FLOAT_IP_NAME: Name = "float-ip".parse().unwrap(); - pub static ref DEMO_FLOAT_IP_URL: String = - format!("/v1/floating-ips/{}?project={}", *DEMO_FLOAT_IP_NAME, *DEMO_PROJECT_NAME); - pub static ref DEMO_FLOAT_IP_CREATE: params::FloatingIpCreate = - params::FloatingIpCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_FLOAT_IP_NAME.clone(), - description: String::from("a new IP pool"), - }, - address: Some(std::net::Ipv4Addr::new(10, 0, 0, 141).into()), - pool: None, - }; -} +// Project Floating IPs +pub static DEMO_FLOAT_IP_NAME: Lazy = + Lazy::new(|| "float-ip".parse().unwrap()); -lazy_static! { - // Identity providers - pub static ref IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers?silo=demo-silo"); - pub static ref SAML_IDENTITY_PROVIDERS_URL: String = format!("/v1/system/identity-providers/saml?silo=demo-silo"); +pub static DEMO_FLOAT_IP_URL: Lazy = Lazy::new(|| { + format!( + "/v1/floating-ips/{}?project={}", + *DEMO_FLOAT_IP_NAME, *DEMO_PROJECT_NAME + ) +}); - pub static ref DEMO_SAML_IDENTITY_PROVIDER_NAME: Name = "demo-saml-provider".parse().unwrap(); - pub static ref SPECIFIC_SAML_IDENTITY_PROVIDER_URL: String = format!("/v1/system/identity-providers/saml/{}?silo=demo-silo", *DEMO_SAML_IDENTITY_PROVIDER_NAME); +pub static DEMO_FLOAT_IP_CREATE: Lazy = + Lazy::new(|| params::FloatingIpCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_FLOAT_IP_NAME.clone(), + description: String::from("a new IP pool"), + }, + address: Some(std::net::Ipv4Addr::new(10, 0, 0, 141).into()), + pool: None, + }); + +// Identity providers +pub const IDENTITY_PROVIDERS_URL: &'static str = + "/v1/system/identity-providers?silo=demo-silo"; +pub const SAML_IDENTITY_PROVIDERS_URL: &'static str = + "/v1/system/identity-providers/saml?silo=demo-silo"; +pub static DEMO_SAML_IDENTITY_PROVIDER_NAME: Lazy = + Lazy::new(|| "demo-saml-provider".parse().unwrap()); + +pub static SPECIFIC_SAML_IDENTITY_PROVIDER_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/system/identity-providers/saml/{}?silo=demo-silo", + *DEMO_SAML_IDENTITY_PROVIDER_NAME + ) + }); - pub static ref SAML_IDENTITY_PROVIDER: params::SamlIdentityProviderCreate = - params::SamlIdentityProviderCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_SAML_IDENTITY_PROVIDER_NAME.clone(), - description: "a demo provider".to_string(), - }, +pub static SAML_IDENTITY_PROVIDER: Lazy = + Lazy::new(|| params::SamlIdentityProviderCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_SAML_IDENTITY_PROVIDER_NAME.clone(), + description: "a demo provider".to_string(), + }, - idp_metadata_source: params::IdpMetadataSource::Url { url: HTTP_SERVER.url("/descriptor").to_string() }, + idp_metadata_source: params::IdpMetadataSource::Url { + url: HTTP_SERVER.url("/descriptor").to_string(), + }, - idp_entity_id: "entity_id".to_string(), - sp_client_id: "client_id".to_string(), - acs_url: "http://acs".to_string(), - slo_url: "http://slo".to_string(), - technical_contact_email: "technical@fake".to_string(), + idp_entity_id: "entity_id".to_string(), + sp_client_id: "client_id".to_string(), + acs_url: "http://acs".to_string(), + slo_url: "http://slo".to_string(), + technical_contact_email: "technical@fake".to_string(), - signing_keypair: None, + signing_keypair: None, - group_attribute_name: None, - }; + group_attribute_name: None, + }); - pub static ref DEMO_SYSTEM_METRICS_URL: String = - format!( - "/v1/system/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", - Utc::now(), - Utc::now(), - ); +pub static DEMO_SYSTEM_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/system/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", + Utc::now(), + Utc::now(), + ) +}); - pub static ref DEMO_SILO_METRICS_URL: String = - format!( - "/v1/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", - Utc::now(), - Utc::now(), - ); +pub static DEMO_SILO_METRICS_URL: Lazy = Lazy::new(|| { + format!( + "/v1/metrics/virtual_disk_space_provisioned?start_time={:?}&end_time={:?}", + Utc::now(), + Utc::now(), + ) +}); - // Users - pub static ref DEMO_USER_CREATE: params::UserCreate = params::UserCreate { +// Users +pub static DEMO_USER_CREATE: Lazy = + Lazy::new(|| params::UserCreate { external_id: params::UserId::from_str("dummy-user").unwrap(), password: params::UserPassword::LoginDisallowed, - }; -} + }); /// Describes an API endpoint to be verified by the "unauthorized" test /// @@ -779,12 +922,13 @@ impl AllowedMethod { } } -lazy_static! { - pub static ref URL_USERS_DB_INIT: String = - format!("/v1/system/users-builtin/{}", authn::USER_DB_INIT.name); +pub static URL_USERS_DB_INIT: Lazy = Lazy::new(|| { + format!("/v1/system/users-builtin/{}", authn::USER_DB_INIT.name) +}); - /// List of endpoints to be verified - pub static ref VERIFY_ENDPOINTS: Vec = vec![ +/// List of endpoints to be verified +pub static VERIFY_ENDPOINTS: Lazy> = Lazy::new(|| { + vec![ // Global IAM policy VerifyEndpoint { url: &SYSTEM_POLICY_URL, @@ -801,7 +945,6 @@ lazy_static! { ), ], }, - // IP Pools top-level endpoint VerifyEndpoint { url: &DEMO_IP_POOLS_URL, @@ -1558,7 +1701,6 @@ lazy_static! { AllowedMethod::GetWebsocket ], }, - /* Instance NICs */ VerifyEndpoint { url: &DEMO_INSTANCE_NICS_URL, @@ -1571,7 +1713,6 @@ lazy_static! { ), ], }, - VerifyEndpoint { url: &DEMO_INSTANCE_NIC_URL, visibility: Visibility::Protected, @@ -1839,7 +1980,6 @@ lazy_static! { unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![AllowedMethod::Get], }, - /* Misc */ VerifyEndpoint { @@ -2066,6 +2206,6 @@ lazy_static! { AllowedMethod::Get, AllowedMethod::Delete, ], - } - ]; -} + }, + ] +}); diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 1cb2eaca3a..317a5a0576 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -13,7 +13,6 @@ use headers::authorization::Credentials; use http::method::Method; use http::StatusCode; use httptest::{matchers::*, responders::*, Expectation, ServerBuilder}; -use lazy_static::lazy_static; use nexus_db_queries::authn::external::spoof; use nexus_test_utils::http_testing::AuthnMode; use nexus_test_utils::http_testing::NexusRequest; @@ -21,6 +20,7 @@ use nexus_test_utils::http_testing::RequestBuilder; use nexus_test_utils::http_testing::TestResponse; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils_macros::nexus_test; +use once_cell::sync::Lazy; type ControlPlaneTestContext = nexus_test_utils::ControlPlaneTestContext; @@ -158,8 +158,8 @@ enum SetupReq { }, } -lazy_static! { - pub static ref HTTP_SERVER: httptest::Server = { +pub static HTTP_SERVER: Lazy = + Lazy::new(|| { // Run a httptest server let server = ServerBuilder::new().run().unwrap(); @@ -167,12 +167,10 @@ lazy_static! { server.expect( Expectation::matching(request::method_path("HEAD", "/image.raw")) .times(1..) - .respond_with( - status_code(200).append_header( - "Content-Length", - format!("{}", 4096 * 1000), - ), - ), + .respond_with(status_code(200).append_header( + "Content-Length", + format!("{}", 4096 * 1000), + )), ); server.expect( @@ -182,10 +180,11 @@ lazy_static! { ); server - }; + }); - /// List of requests to execute at setup time - static ref SETUP_REQUESTS: Vec = vec![ +/// List of requests to execute at setup time +static SETUP_REQUESTS: Lazy> = Lazy::new(|| { + vec![ // Create a separate Silo SetupReq::Post { url: "/v1/system/silos", @@ -203,10 +202,7 @@ lazy_static! { ], }, // Get the default IP pool - SetupReq::Get { - url: &DEMO_IP_POOL_URL, - id_routes: vec![], - }, + SetupReq::Get { url: &DEMO_IP_POOL_URL, id_routes: vec![] }, // Create an IP pool range SetupReq::Post { url: &DEMO_IP_POOL_RANGES_ADD_URL, @@ -302,8 +298,8 @@ lazy_static! { body: serde_json::to_value(&*DEMO_CERTIFICATE_CREATE).unwrap(), id_routes: vec![], }, - ]; -} + ] +}); /// Contents returned from an endpoint that creates a resource that has an id /// From de152304c3f551914db05bda1ddbc4b139e1f766 Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Wed, 20 Dec 2023 14:20:03 -0800 Subject: [PATCH 06/13] Bump SP versions to 1.0.4 (#4722) --- tools/hubris_checksums | 14 +++++++------- tools/hubris_version | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/hubris_checksums b/tools/hubris_checksums index 1396af4d60..b63deb9687 100644 --- a/tools/hubris_checksums +++ b/tools/hubris_checksums @@ -1,7 +1,7 @@ -2df01d7dd17423588c99de4361694efdb6bd375e2f54db053320eeead3e07eda build-gimlet-c-image-default-v1.0.3.zip -8ac0eb6d7817825c6318feb8327f5758a33ccd2479512e3e2424f0eb8e290010 build-gimlet-d-image-default-v1.0.3.zip -eeeb72ec81a843fa1f5093096d1e4500aba6ce01c2d21040a2a10a092595d945 build-gimlet-e-image-default-v1.0.3.zip -de0d9028929322f6d5afc4cb52c198b3402c93a38aa15f9d378617ca1d1112c9 build-psc-b-image-default-v1.0.3.zip -11a6235d852bd75548f12d85b0913cb4ccb0aff3c38bf8a92510a2b9c14dad3c build-psc-c-image-default-v1.0.3.zip -3f863d46a462432f19d3fb5a293b8106da6e138de80271f869692bd29abd994b build-sidecar-b-image-default-v1.0.3.zip -2a9feac7f2da61b843d00edf2693c31c118f202c6cd889d1d1758ea1dd95dbca build-sidecar-c-image-default-v1.0.3.zip +a1a3abb29fb78330c682f8b4f58397f28e296463ac18659af82f762a714f3759 build-gimlet-c-image-default-v1.0.4.zip +f53bc6b8fa825fa1f49b5401b05a14bbd22516c16a8254ef5cd5f3b26b450098 build-gimlet-d-image-default-v1.0.4.zip +a91a1719a03531fdc62608a3b747962b3b7a6dc093ae3810ff35a353ef1e9bf7 build-gimlet-e-image-default-v1.0.4.zip +08ce2931d17d58cde8af49d99de425af4b15384923d2cf79d58000fd2ac5d88c build-psc-b-image-default-v1.0.4.zip +71167b0c889132c3584ba05ee1f7e5917092cd6d7fe8f50f04cdbf3f78321fdf build-psc-c-image-default-v1.0.4.zip +56a02e8620a8343282ee4f205dabcb4898a3acb0e50b6e6eca3919a33a159ee4 build-sidecar-b-image-default-v1.0.4.zip +54eb8d9e202cd69a8cdbdd505276c8c2c1d7f548e2b4234c01887209b190bc91 build-sidecar-c-image-default-v1.0.4.zip diff --git a/tools/hubris_version b/tools/hubris_version index b00c3286fe..0cce8d745a 100644 --- a/tools/hubris_version +++ b/tools/hubris_version @@ -1 +1 @@ -TAGS=(gimlet-v1.0.3 psc-v1.0.3 sidecar-v1.0.3) +TAGS=(gimlet-v1.0.4 psc-v1.0.4 sidecar-v1.0.4) From b114324f809a73a62d8cfb3e95ee1cd42e0794fc Mon Sep 17 00:00:00 2001 From: Laura Abbott Date: Wed, 20 Dec 2023 19:24:59 -0800 Subject: [PATCH 07/13] Correct hubris versions (#4723) The previous versions were tagged incorrectly in the caboose --- tools/hubris_checksums | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/hubris_checksums b/tools/hubris_checksums index b63deb9687..707c67fe0c 100644 --- a/tools/hubris_checksums +++ b/tools/hubris_checksums @@ -1,7 +1,7 @@ -a1a3abb29fb78330c682f8b4f58397f28e296463ac18659af82f762a714f3759 build-gimlet-c-image-default-v1.0.4.zip -f53bc6b8fa825fa1f49b5401b05a14bbd22516c16a8254ef5cd5f3b26b450098 build-gimlet-d-image-default-v1.0.4.zip -a91a1719a03531fdc62608a3b747962b3b7a6dc093ae3810ff35a353ef1e9bf7 build-gimlet-e-image-default-v1.0.4.zip -08ce2931d17d58cde8af49d99de425af4b15384923d2cf79d58000fd2ac5d88c build-psc-b-image-default-v1.0.4.zip -71167b0c889132c3584ba05ee1f7e5917092cd6d7fe8f50f04cdbf3f78321fdf build-psc-c-image-default-v1.0.4.zip -56a02e8620a8343282ee4f205dabcb4898a3acb0e50b6e6eca3919a33a159ee4 build-sidecar-b-image-default-v1.0.4.zip -54eb8d9e202cd69a8cdbdd505276c8c2c1d7f548e2b4234c01887209b190bc91 build-sidecar-c-image-default-v1.0.4.zip +09f0342eed777495ac0a852f219d2dec45fdc1b860f938f95736851b1627cad7 build-gimlet-c-image-default-v1.0.4.zip +aef9279ba6d1d0ffa64586d71cdf5933eddbe048ce1a10f5f611128a84b53642 build-gimlet-d-image-default-v1.0.4.zip +989f89f0060239b77d92fe068ceae1be406591c997224256c617d77b2ccbf1b0 build-gimlet-e-image-default-v1.0.4.zip +8e41a139bc62ff86b8343989889491739bb90eb46e1a02585252adf3ee540db9 build-psc-b-image-default-v1.0.4.zip +76e35e71714921a1ca5f7f8314fc596e3b5fe1dfd422c59fdc9a62c1ebfeec0e build-psc-c-image-default-v1.0.4.zip +a406045b1d545fd063bb989c84a774e4d09a445618d4a8889ce232a3b45884a7 build-sidecar-b-image-default-v1.0.4.zip +69ba3ac372388058f8a6e58230e7e2964990609f18c0960357d17bfc16f25bae build-sidecar-c-image-default-v1.0.4.zip From ae7c2ed727f04de778707af9efea55db9e4d1f5a Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 21 Dec 2023 15:08:29 -0800 Subject: [PATCH 08/13] [rust] update to Rust 1.74.1 (#4700) I'm hitting an ICE with 1.74.0 that appears to be fixed with 1.74.1. --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 65ee8a9912..2e7a87b58b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -4,5 +4,5 @@ # # We choose a specific toolchain (rather than "stable") for repeatability. The # intent is to keep this up-to-date with recently-released stable Rust. -channel = "1.74.0" +channel = "1.74.1" profile = "default" From b19c61a7137a909c58d8ae45fd81e266f9f98f73 Mon Sep 17 00:00:00 2001 From: Rain Date: Thu, 21 Dec 2023 15:09:06 -0800 Subject: [PATCH 09/13] [authz-macros] accept an optional input_key argument (#4707) In some cases including composite keys, it can be better to make the outside representation of the primary key some kind of struct, rather than passing around tuples of various types. Enable that in the `authz_resource` macro by allowing users to specify an optional `input_key` argument, which represents a better view into the primary key. I'm not entirely sure that the `From` trait is the right thing to use here, but it seems like a pretty low-cost decision to change in the future. As part of this PR I also switched to the prettyplease crate, which as the README explains is more suitable for generated code than rustfmt: https://crates.io/crates/prettyplease --- Cargo.lock | 7 +-- Cargo.toml | 1 + nexus/authz-macros/Cargo.toml | 3 ++ nexus/authz-macros/src/lib.rs | 96 ++++++++++++++++++++++++++++++----- nexus/db-macros/Cargo.toml | 2 +- nexus/db-macros/src/lookup.rs | 15 ++++-- 6 files changed, 103 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 962fe68e02..98275e144f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,6 +354,7 @@ version = "0.1.0" dependencies = [ "heck 0.4.1", "omicron-workspace-hack", + "prettyplease", "proc-macro2", "quote", "serde", @@ -1539,9 +1540,9 @@ version = "0.1.0" dependencies = [ "heck 0.4.1", "omicron-workspace-hack", + "prettyplease", "proc-macro2", "quote", - "rustfmt-wrapper", "serde", "serde_tokenstream 0.2.0", "syn 2.0.32", @@ -6147,9 +6148,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", "syn 2.0.32", diff --git a/Cargo.toml b/Cargo.toml index d651a13bf1..d4f81b0310 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,6 +289,7 @@ postgres-protocol = "0.6.6" predicates = "3.0.4" pretty_assertions = "1.4.0" pretty-hex = "0.4.0" +prettyplease = "0.2.15" proc-macro2 = "1.0" progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } progenitor-client = { git = "https://github.com/oxidecomputer/progenitor", branch = "main" } diff --git a/nexus/authz-macros/Cargo.toml b/nexus/authz-macros/Cargo.toml index 15f18cb9c8..816100eb58 100644 --- a/nexus/authz-macros/Cargo.toml +++ b/nexus/authz-macros/Cargo.toml @@ -15,3 +15,6 @@ serde.workspace = true serde_tokenstream.workspace = true syn.workspace = true omicron-workspace-hack.workspace = true + +[dev-dependencies] +prettyplease.workspace = true diff --git a/nexus/authz-macros/src/lib.rs b/nexus/authz-macros/src/lib.rs index d85516f3ea..3d6f265fea 100644 --- a/nexus/authz-macros/src/lib.rs +++ b/nexus/authz-macros/src/lib.rs @@ -95,6 +95,33 @@ use serde_tokenstream::ParseWrapper; /// polar_snippet = FleetChild, /// } /// ``` +/// +/// In some cases, it may be more convenient to identify a composite key with a +/// struct rather than relying on tuples. This is supported too: +/// +/// ```ignore +/// struct SomeCompositeId { +/// foo: String, +/// bar: String, +/// } +/// +/// // There needs to be a `From` impl from the composite ID to the primary key. +/// impl From for (String, String) { +/// fn from(id: SomeCompositeId) -> Self { +/// (id.foo, id.bar) +/// } +/// } +/// +/// authz_resource! { +/// name = "MyResource", +/// parent = "Fleet", +/// primary_key = (String, String), +/// input_key = SomeCompositeId, +/// roles_allowed = false, +/// polar_snippet = FleetChild, +/// } +/// ``` + // Allow private intra-doc links. This is useful because the `Input` struct // cannot be exported (since we're a proc macro crate, and we can't expose // a struct), but its documentation is very useful. @@ -121,6 +148,12 @@ struct Input { parent: String, /// Rust type for the primary key for this resource primary_key: ParseWrapper, + /// Rust type for the input key for this resource (the key users specify + /// for this resource, convertible to `primary_key`). + /// + /// This is the same as primary_key if not specified. + #[serde(default)] + input_key: Option>, /// Whether roles may be attached directly to this resource roles_allowed: bool, /// How to generate the Polar snippet for this resource @@ -153,6 +186,9 @@ fn do_authz_resource( let parent_resource_name = format_ident!("{}", input.parent); let parent_as_snake = heck::AsSnakeCase(&input.parent).to_string(); let primary_key_type = &*input.primary_key; + let input_key_type = + &**input.input_key.as_ref().unwrap_or(&input.primary_key); + let (has_role_body, as_roles_body, api_resource_roles_trait) = if input.roles_allowed { ( @@ -334,6 +370,21 @@ fn do_authz_resource( /// `parent`, unique key `key`, looked up as described by /// `lookup_type` pub fn new( + parent: #parent_resource_name, + key: #input_key_type, + lookup_type: LookupType, + ) -> #resource_name { + #resource_name { + parent, + key: key.into(), + lookup_type, + } + } + + /// A version of `new` that takes the primary key type directly. + /// This is only different from [`Self::new`] if this resource + /// uses a different input key type. + pub fn with_primary_key( parent: #parent_resource_name, key: #primary_key_type, lookup_type: LookupType, @@ -346,7 +397,7 @@ fn do_authz_resource( } pub fn id(&self) -> #primary_key_type { - self.key.clone() + self.key.clone().into() } /// Describes how to register this type with Oso @@ -411,15 +462,36 @@ fn do_authz_resource( // See the test for lookup_resource. #[cfg(test)] -#[test] -fn test_authz_dump() { - let output = do_authz_resource(quote! { - name = "Organization", - parent = "Fleet", - primary_key = Uuid, - roles_allowed = false, - polar_snippet = Custom, - }) - .unwrap(); - println!("{}", output); +mod tests { + use super::*; + #[test] + fn test_authz_dump() { + let output = do_authz_resource(quote! { + name = "Organization", + parent = "Fleet", + primary_key = Uuid, + roles_allowed = false, + polar_snippet = Custom, + }) + .unwrap(); + println!("{}", pretty_format(output)); + + let output = do_authz_resource(quote! { + name = "Instance", + parent = "Project", + primary_key = (String, String), + // The SomeCompositeId type doesn't exist, but that's okay because + // this code is never compiled, just printed out. + input_key = SomeCompositeId, + roles_allowed = false, + polar_snippet = InProject, + }) + .unwrap(); + println!("{}", pretty_format(output)); + } + + fn pretty_format(input: TokenStream) -> String { + let parsed = syn::parse2(input).unwrap(); + prettyplease::unparse(&parsed) + } } diff --git a/nexus/db-macros/Cargo.toml b/nexus/db-macros/Cargo.toml index 053c381ac9..64398b266c 100644 --- a/nexus/db-macros/Cargo.toml +++ b/nexus/db-macros/Cargo.toml @@ -18,4 +18,4 @@ syn = { workspace = true, features = ["extra-traits"] } omicron-workspace-hack.workspace = true [dev-dependencies] -rustfmt-wrapper.workspace = true +prettyplease.workspace = true diff --git a/nexus/db-macros/src/lookup.rs b/nexus/db-macros/src/lookup.rs index f2362f5bc5..c7906c7bf0 100644 --- a/nexus/db-macros/src/lookup.rs +++ b/nexus/db-macros/src/lookup.rs @@ -406,7 +406,7 @@ fn generate_misc_helpers(config: &Config) -> TokenStream { db_row: &nexus_db_model::#resource_name, lookup_type: LookupType, ) -> authz::#resource_name { - authz::#resource_name::new( + authz::#resource_name::with_primary_key( authz_parent.clone(), db_row.id(), lookup_type @@ -923,8 +923,8 @@ fn generate_database_functions(config: &Config) -> TokenStream { #[cfg(test)] mod test { use super::lookup_resource; + use proc_macro2::TokenStream; use quote::quote; - use rustfmt_wrapper::rustfmt; #[test] #[ignore] @@ -938,7 +938,7 @@ mod test { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); let output = lookup_resource(quote! { name = "SiloUser", @@ -949,7 +949,7 @@ mod test { primary_key_columns = [ { column_name = "id", rust_type = Uuid } ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); let output = lookup_resource(quote! { name = "UpdateArtifact", @@ -964,6 +964,11 @@ mod test { ] }) .unwrap(); - println!("{}", rustfmt(output).unwrap()); + println!("{}", pretty_format(output)); + } + + fn pretty_format(input: TokenStream) -> String { + let parsed = syn::parse2(input).unwrap(); + prettyplease::unparse(&parsed) } } From b16be3f68c420e223dccb0326deb327ae5be8e50 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Fri, 22 Dec 2023 08:13:03 +0000 Subject: [PATCH 10/13] Update Sidecar-lite, ignore ARP hostnames in SoftNPU init (#4725) Bumps to the most recent sidecar-lite, which includes a minor fix for manually added routes, and adds the `-n` flag when checking `arp` for the gateway's MAC address during SoftNPU init. This is really only needed if the gateway is also acting as a DNS server and names itself something cutesy, but it can come up: ``` kyle@farme:~/gits/omicron$ arp -a Net to Media Table: IPv4 Device IP Address Mask Flags Phys Addr ------ -------------------- --------------- -------- --------------- rge0 hub.home.arpa 255.255.255.255 b8:6a:f1:28:cd:00 ... kyle@farme:~/gits/omicron$ arp -an Net to Media Table: IPv4 Device IP Address Mask Flags Phys Addr ------ -------------------- --------------- -------- --------------- rge0 10.0.0.1 255.255.255.255 b8:6a:f1:28:cd:00 ``` --- tools/create_virtual_hardware.sh | 2 +- tools/scrimlet/softnpu-init.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/create_virtual_hardware.sh b/tools/create_virtual_hardware.sh index 7721fb1c0f..fa35bb24ab 100755 --- a/tools/create_virtual_hardware.sh +++ b/tools/create_virtual_hardware.sh @@ -63,7 +63,7 @@ function ensure_softnpu_zone { --omicron-zone \ --ports sc0_0,tfportrear0_0 \ --ports sc0_1,tfportqsfp0_0 \ - --sidecar-lite-commit 45ed98fea5824feb4d42f45bbf218e597dc9fc58 \ + --sidecar-lite-commit e3ea4b495ba0a71801ded0776ae4bbd31df57e26 \ --softnpu-commit dbab082dfa89da5db5ca2325c257089d2f130092 } "$SOURCE_DIR"/scrimlet/softnpu-init.sh diff --git a/tools/scrimlet/softnpu-init.sh b/tools/scrimlet/softnpu-init.sh index 6a2a9e10ce..59f8e83019 100755 --- a/tools/scrimlet/softnpu-init.sh +++ b/tools/scrimlet/softnpu-init.sh @@ -31,7 +31,7 @@ fi # Add an extrac space at the end of the search pattern passed to `grep`, so that # we can be sure we're matching the exact $GATEWAY_IP, and not something that # shares the same string prefix. -GATEWAY_MAC=${GATEWAY_MAC:=$(arp -a | grep "$GATEWAY_IP " | awk -F ' ' '{print $NF}')} +GATEWAY_MAC=${GATEWAY_MAC:=$(arp -an | grep "$GATEWAY_IP " | awk -F ' ' '{print $NF}')} # Check that the MAC appears to be exactly one MAC address. COUNT=$(grep -c -E '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' <(echo "$GATEWAY_MAC")) From 727bc961139e77f9e33d69bb6ebe8fce31bb117d Mon Sep 17 00:00:00 2001 From: Andy Fiddaman Date: Fri, 22 Dec 2023 13:54:54 +0000 Subject: [PATCH 11/13] Disable the SSH daemon in most non-global zones (#4716) This improves things by disabling the SSH daemon in self-assembling zones via the smf profile, and directly in non-self-assembling zones. The service remains enabled in the switch zone for wicket and support. --- sled-agent/src/profile.rs | 31 +++++++++++++++++++++++++++--- sled-agent/src/services.rs | 39 ++++++++++++++++++++++++-------------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/sled-agent/src/profile.rs b/sled-agent/src/profile.rs index 7c2d8d1738..1addbca4c9 100644 --- a/sled-agent/src/profile.rs +++ b/sled-agent/src/profile.rs @@ -116,12 +116,18 @@ impl Display for ServiceBuilder { pub struct ServiceInstanceBuilder { name: String, + enabled: bool, property_groups: Vec, } impl ServiceInstanceBuilder { pub fn new(name: &str) -> Self { - Self { name: name.to_string(), property_groups: vec![] } + Self { name: name.to_string(), enabled: true, property_groups: vec![] } + } + + pub fn disable(mut self) -> Self { + self.enabled = false; + self } pub fn add_property_group( @@ -137,9 +143,10 @@ impl Display for ServiceInstanceBuilder { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { write!( f, - r#" + r#" "#, - name = self.name + name = self.name, + enabled = self.enabled )?; for property_group in &self.property_groups { @@ -315,6 +322,24 @@ mod tests { ); } + #[test] + fn test_disabled_instance() { + let builder = ProfileBuilder::new("myprofile") + .add_service(ServiceBuilder::new("myservice").add_instance( + ServiceInstanceBuilder::new("default").disable(), + )); + assert_eq!( + format!("{}", builder), + r#" + + + + + +"#, + ); + } + #[test] fn test_property_group() { let builder = ProfileBuilder::new("myprofile").add_service( diff --git a/sled-agent/src/services.rs b/sled-agent/src/services.rs index 837c2a05df..a9000a1c4b 100644 --- a/sled-agent/src/services.rs +++ b/sled-agent/src/services.rs @@ -1371,8 +1371,8 @@ impl ServiceManager { .add_property_group(dns_config_builder) // We do need to enable the default instance of the // dns/install service. It's enough to just mention it - // here, as the ServiceInstanceBuilder always enables the - // instance being added. + // here, as the ServiceInstanceBuilder enables the + // instance being added by default. .add_instance(ServiceInstanceBuilder::new("default"))) } @@ -1473,6 +1473,8 @@ impl ServiceManager { // // These zones are self-assembling -- after they boot, there should // be no "zlogin" necessary to initialize. + let disabled_ssh_service = ServiceBuilder::new("network/ssh") + .add_instance(ServiceInstanceBuilder::new("default").disable()); match &request { ZoneArgs::Omicron(OmicronZoneConfigLocal { zone: @@ -1507,6 +1509,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(clickhouse_service) .add_service(dns_service); profile @@ -1551,6 +1554,7 @@ impl ServiceManager { .add_property_group(config), ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(clickhouse_keeper_service) .add_service(dns_service); profile @@ -1603,6 +1607,7 @@ impl ServiceManager { ); let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) .add_service(cockroachdb_service) .add_service(dns_service); profile @@ -1646,12 +1651,15 @@ impl ServiceManager { .add_property("uuid", "astring", uuid) .add_property("store", "astring", "/data"); - let profile = ProfileBuilder::new("omicron").add_service( - ServiceBuilder::new("oxide/crucible/agent").add_instance( - ServiceInstanceBuilder::new("default") - .add_property_group(config), - ), - ); + let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) + .add_service( + ServiceBuilder::new("oxide/crucible/agent") + .add_instance( + ServiceInstanceBuilder::new("default") + .add_property_group(config), + ), + ); profile .add_to_zone(&self.inner.log, &installed_zone) .await @@ -1685,12 +1693,15 @@ impl ServiceManager { .add_property("listen_addr", "astring", listen_addr) .add_property("listen_port", "astring", listen_port); - let profile = ProfileBuilder::new("omicron").add_service( - ServiceBuilder::new("oxide/crucible/pantry").add_instance( - ServiceInstanceBuilder::new("default") - .add_property_group(config), - ), - ); + let profile = ProfileBuilder::new("omicron") + .add_service(disabled_ssh_service) + .add_service( + ServiceBuilder::new("oxide/crucible/pantry") + .add_instance( + ServiceInstanceBuilder::new("default") + .add_property_group(config), + ), + ); profile .add_to_zone(&self.inner.log, &installed_zone) .await From a2cef18d7b735580bc8963103b2f0e4e30fd9885 Mon Sep 17 00:00:00 2001 From: "oxide-renovate[bot]" <146848827+oxide-renovate[bot]@users.noreply.github.com> Date: Fri, 22 Dec 2023 10:50:38 -0800 Subject: [PATCH 12/13] Update Rust crate russh to v0.40.2 [SECURITY] (#4714) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98275e144f..3cdf3dd678 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6908,9 +6908,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.40.1" +version = "0.40.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23955cec4c4186e8c36f42c5d4043f9fd6cab8702fd08ce1971d966b48ec832f" +checksum = "93dab9e1c313d0d04a42e39c0995943fc38c037e2e3fa9c33685777a1aecdfb2" dependencies = [ "aes", "aes-gcm", From 709493b9adcec358fbdf9c3f5025698c589bd1df Mon Sep 17 00:00:00 2001 From: "Andrew J. Stone" Date: Fri, 22 Dec 2023 17:59:42 -0500 Subject: [PATCH 13/13] Rename API to sled_add and take UninitializedSledId as param (#4704) This is the second half of the fix for #4607 --- .../db-queries/src/db/datastore/inventory.rs | 4 +- nexus/src/app/rack.rs | 84 ++++++++++++------- nexus/src/external_api/http_entrypoints.rs | 10 +-- nexus/tests/integration_tests/endpoints.rs | 16 +--- nexus/tests/output/nexus_tags.txt | 2 +- nexus/types/src/external_api/params.rs | 17 ++++ nexus/types/src/inventory.rs | 7 ++ openapi/nexus.json | 20 ++++- openapi/sled-agent.json | 2 +- sled-agent/src/http_entrypoints.rs | 6 +- sled-agent/src/sled_agent.rs | 2 +- 11 files changed, 115 insertions(+), 55 deletions(-) diff --git a/nexus/db-queries/src/db/datastore/inventory.rs b/nexus/db-queries/src/db/datastore/inventory.rs index 31b24a7e75..7d880b4ec0 100644 --- a/nexus/db-queries/src/db/datastore/inventory.rs +++ b/nexus/db-queries/src/db/datastore/inventory.rs @@ -919,7 +919,7 @@ impl DataStore { pub async fn find_hw_baseboard_id( &self, opctx: &OpContext, - baseboard_id: BaseboardId, + baseboard_id: &BaseboardId, ) -> Result { opctx.authorize(authz::Action::Read, &authz::INVENTORY).await?; let conn = self.pool_connection_authorized(opctx).await?; @@ -1442,7 +1442,7 @@ mod test { part_number: "some-part".into(), }; let err = datastore - .find_hw_baseboard_id(&opctx, baseboard_id) + .find_hw_baseboard_id(&opctx, &baseboard_id) .await .unwrap_err(); assert!(matches!(err, Error::ObjectNotFound { .. })); diff --git a/nexus/src/app/rack.rs b/nexus/src/app/rack.rs index c0307e5b5b..a0dcb7fcb1 100644 --- a/nexus/src/app/rack.rs +++ b/nexus/src/app/rack.rs @@ -31,6 +31,7 @@ use nexus_types::external_api::params::LinkConfig; use nexus_types::external_api::params::LldpServiceConfig; use nexus_types::external_api::params::RouteConfig; use nexus_types::external_api::params::SwitchPortConfig; +use nexus_types::external_api::params::UninitializedSledId; use nexus_types::external_api::params::{ AddressLotCreate, BgpPeerConfig, LoopbackAddressCreate, Route, SiloCreate, SwitchPortSettingsCreate, @@ -51,6 +52,7 @@ use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::Name; use omicron_common::api::external::NameOrId; +use omicron_common::api::external::ResourceType; use omicron_common::api::internal::shared::ExternalPortDiscovery; use sled_agent_client::types::AddSledRequest; use sled_agent_client::types::EarlyNetworkConfigBody; @@ -69,6 +71,19 @@ use std::num::NonZeroU32; use std::str::FromStr; use uuid::Uuid; +// A limit for querying the last inventory collection +// +// We set a limit of 200 here to give us some breathing room when +// querying for cabooses and RoT pages, each of which is "4 per SP/RoT", +// which in a single fully populated rack works out to (32 sleds + 2 +// switches + 1 psc) * 4 = 140. +// +// This feels bad and probably needs more thought; see +// https://github.com/oxidecomputer/omicron/issues/4621 where this limit +// being too low bit us, and it will link to a more general followup +// issue. +const INVENTORY_COLLECTION_LIMIT: u32 = 200; + impl super::Nexus { pub(crate) async fn racks_list( &self, @@ -872,17 +887,7 @@ impl super::Nexus { ) -> ListResultVec { debug!(self.log, "Getting latest collection"); // Grab the SPs from the last collection - // - // We set a limit of 200 here to give us some breathing room when - // querying for cabooses and RoT pages, each of which is "4 per SP/RoT", - // which in a single fully populated rack works out to (32 sleds + 2 - // switches + 1 psc) * 4 = 140. - // - // This feels bad and probably needs more thought; see - // https://github.com/oxidecomputer/omicron/issues/4621 where this limit - // being too low bit us, and it will link to a more general followup - // issue. - let limit = NonZeroU32::new(200).unwrap(); + let limit = NonZeroU32::new(INVENTORY_COLLECTION_LIMIT).unwrap(); let collection = self .db_datastore .inventory_get_latest_collection(opctx, limit) @@ -933,16 +938,18 @@ impl super::Nexus { } /// Add a sled to an intialized rack - pub(crate) async fn add_sled_to_initialized_rack( + pub(crate) async fn sled_add( &self, opctx: &OpContext, - sled: UninitializedSled, + sled: UninitializedSledId, ) -> Result<(), Error> { - let baseboard_id = sled.baseboard.clone().into(); - let hw_baseboard_id = - self.db_datastore.find_hw_baseboard_id(opctx, baseboard_id).await?; + let baseboard_id = sled.clone().into(); + let hw_baseboard_id = self + .db_datastore + .find_hw_baseboard_id(opctx, &baseboard_id) + .await?; - let subnet = self.db_datastore.rack_subnet(opctx, sled.rack_id).await?; + let subnet = self.db_datastore.rack_subnet(opctx, self.rack_id).await?; let rack_subnet = Ipv6Subnet::::from(rack_subnet(Some(subnet))?); @@ -950,16 +957,39 @@ impl super::Nexus { .db_datastore .allocate_sled_underlay_subnet_octets( opctx, - sled.rack_id, + self.rack_id, hw_baseboard_id, ) .await?; + // Grab the SPs from the last collection + let limit = NonZeroU32::new(INVENTORY_COLLECTION_LIMIT).unwrap(); + let collection = self + .db_datastore + .inventory_get_latest_collection(opctx, limit) + .await?; + + // If there isn't a collection, we don't know about the sled + let Some(collection) = collection else { + return Err(Error::unavail("no inventory data available")); + }; + + // Find the revision + let Some(sp) = collection.sps.get(&baseboard_id) else { + return Err(Error::ObjectNotFound { + type_name: ResourceType::Sled, + lookup_type: + omicron_common::api::external::LookupType::ByCompositeId( + format!("{sled:?}"), + ), + }); + }; + // Convert the baseboard as necessary let baseboard = sled_agent_client::types::Baseboard::Gimlet { - identifier: sled.baseboard.serial.clone(), - model: sled.baseboard.part.clone(), - revision: sled.baseboard.revision, + identifier: sled.serial.clone(), + model: sled.part.clone(), + revision: sp.baseboard_revision.into(), }; // Make the call to sled-agent @@ -985,13 +1015,11 @@ impl super::Nexus { }, }; let sa = self.get_any_sled_agent(opctx).await?; - sa.add_sled_to_initialized_rack(&req).await.map_err(|e| { - Error::InternalError { - internal_message: format!( - "failed to add sled with baseboard {:?} to rack {}: {e}", - sled.baseboard, allocation.rack_id - ), - } + sa.sled_add(&req).await.map_err(|e| Error::InternalError { + internal_message: format!( + "failed to add sled with baseboard {:?} to rack {}: {e}", + sled, allocation.rack_id + ), })?; Ok(()) diff --git a/nexus/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 3e38558760..dde641a4ad 100644 --- a/nexus/src/external_api/http_entrypoints.rs +++ b/nexus/src/external_api/http_entrypoints.rs @@ -6,7 +6,7 @@ use super::{ console_api, device_auth, params, - params::ProjectSelector, + params::{ProjectSelector, UninitializedSledId}, shared::UninitializedSled, views::{ self, Certificate, Group, IdentityProvider, Image, IpPool, IpPoolRange, @@ -228,7 +228,7 @@ pub(crate) fn external_api() -> NexusApiDescription { api.register(switch_list)?; api.register(switch_view)?; api.register(sled_list_uninitialized)?; - api.register(add_sled_to_initialized_rack)?; + api.register(sled_add)?; api.register(user_builtin_list)?; api.register(user_builtin_view)?; @@ -4689,15 +4689,15 @@ async fn sled_list_uninitialized( path = "/v1/system/hardware/sleds/", tags = ["system/hardware"] }] -async fn add_sled_to_initialized_rack( +async fn sled_add( rqctx: RequestContext>, - sled: TypedBody, + sled: TypedBody, ) -> Result { let apictx = rqctx.context(); let nexus = &apictx.nexus; let handler = async { let opctx = crate::context::op_context_for_external_api(&rqctx).await?; - nexus.add_sled_to_initialized_rack(&opctx, sled.into_inner()).await?; + nexus.sled_add(&opctx, sled.into_inner()).await?; Ok(HttpResponseUpdatedNoContent()) }; apictx.external_latencies.instrument_dropshot_handler(&rqctx, handler).await diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs index be0ea2a3f5..8708083124 100644 --- a/nexus/tests/integration_tests/endpoints.rs +++ b/nexus/tests/integration_tests/endpoints.rs @@ -20,10 +20,8 @@ use nexus_test_utils::SLED_AGENT_UUID; use nexus_test_utils::SWITCH_UUID; use nexus_types::external_api::params; use nexus_types::external_api::shared; -use nexus_types::external_api::shared::Baseboard; use nexus_types::external_api::shared::IpRange; use nexus_types::external_api::shared::Ipv4Range; -use nexus_types::external_api::shared::UninitializedSled; use omicron_common::api::external::AddressLotKind; use omicron_common::api::external::ByteCount; use omicron_common::api::external::IdentityMetadataCreateParams; @@ -41,7 +39,6 @@ use once_cell::sync::Lazy; use std::net::IpAddr; use std::net::Ipv4Addr; use std::str::FromStr; -use uuid::Uuid; pub static HARDWARE_RACK_URL: Lazy = Lazy::new(|| format!("/v1/system/hardware/racks/{}", RACK_UUID)); @@ -69,15 +66,10 @@ pub static HARDWARE_SLED_DISK_URL: Lazy = Lazy::new(|| { pub static SLED_INSTANCES_URL: Lazy = Lazy::new(|| { format!("/v1/system/hardware/sleds/{}/instances", SLED_AGENT_UUID) }); -pub static DEMO_UNINITIALIZED_SLED: Lazy = - Lazy::new(|| UninitializedSled { - baseboard: Baseboard { - serial: "demo-serial".to_string(), - part: "demo-part".to_string(), - revision: 6, - }, - rack_id: Uuid::new_v4(), - cubby: 1, +pub static DEMO_UNINITIALIZED_SLED: Lazy = + Lazy::new(|| params::UninitializedSledId { + serial: "demo-serial".to_string(), + part: "demo-part".to_string(), }); // Global policy diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index 10e7df7286..b607bbf1f3 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -117,13 +117,13 @@ snapshot_view GET /v1/snapshots/{snapshot} API operations found with tag "system/hardware" OPERATION ID METHOD URL PATH -add_sled_to_initialized_rack POST /v1/system/hardware/sleds networking_switch_port_apply_settings POST /v1/system/hardware/switch-port/{port}/settings networking_switch_port_clear_settings DELETE /v1/system/hardware/switch-port/{port}/settings networking_switch_port_list GET /v1/system/hardware/switch-port physical_disk_list GET /v1/system/hardware/disks rack_list GET /v1/system/hardware/racks rack_view GET /v1/system/hardware/racks/{rack_id} +sled_add POST /v1/system/hardware/sleds sled_instance_list GET /v1/system/hardware/sleds/{sled_id}/instances sled_list GET /v1/system/hardware/sleds sled_list_uninitialized GET /v1/system/hardware/sleds-uninitialized diff --git a/nexus/types/src/external_api/params.rs b/nexus/types/src/external_api/params.rs index df399e310c..6749794a9a 100644 --- a/nexus/types/src/external_api/params.rs +++ b/nexus/types/src/external_api/params.rs @@ -47,6 +47,23 @@ macro_rules! id_path_param { }; } +/// The unique hardware ID for a sled +#[derive( + Clone, + Debug, + Serialize, + Deserialize, + JsonSchema, + PartialOrd, + Ord, + PartialEq, + Eq, +)] +pub struct UninitializedSledId { + pub serial: String, + pub part: String, +} + path_param!(ProjectPath, project, "project"); path_param!(InstancePath, instance, "instance"); path_param!(NetworkInterfacePath, interface, "network interface"); diff --git a/nexus/types/src/inventory.rs b/nexus/types/src/inventory.rs index 9401727162..77bc73306d 100644 --- a/nexus/types/src/inventory.rs +++ b/nexus/types/src/inventory.rs @@ -20,6 +20,7 @@ use std::sync::Arc; use strum::EnumIter; use uuid::Uuid; +use crate::external_api::params::UninitializedSledId; use crate::external_api::shared::Baseboard; /// Results of collecting hardware/software inventory from various Omicron @@ -139,6 +140,12 @@ impl From for BaseboardId { } } +impl From for BaseboardId { + fn from(value: UninitializedSledId) -> Self { + BaseboardId { part_number: value.part, serial_number: value.serial } + } +} + /// Caboose contents found during a collection /// /// These are normalized in the database. Each distinct `Caboose` is assigned a diff --git a/openapi/nexus.json b/openapi/nexus.json index 4131460149..815cc399ae 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -3765,12 +3765,12 @@ "system/hardware" ], "summary": "Add a sled to an initialized rack", - "operationId": "add_sled_to_initialized_rack", + "operationId": "sled_add", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UninitializedSled" + "$ref": "#/components/schemas/UninitializedSledId" } } }, @@ -14909,6 +14909,22 @@ "rack_id" ] }, + "UninitializedSledId": { + "description": "The unique hardware ID for a sled", + "type": "object", + "properties": { + "part": { + "type": "string" + }, + "serial": { + "type": "string" + } + }, + "required": [ + "part", + "serial" + ] + }, "UninitializedSledResultsPage": { "description": "A single page of results", "type": "object", diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 6076df6dbb..467fd32cb8 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -576,7 +576,7 @@ "/sleds": { "put": { "summary": "Add a sled to a rack that was already initialized via RSS", - "operationId": "add_sled_to_initialized_rack", + "operationId": "sled_add", "requestBody": { "content": { "application/json": { diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 8c8a5f2a03..26a0d2ddc2 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -77,7 +77,7 @@ pub fn api() -> SledApiDescription { api.register(uplink_ensure)?; api.register(read_network_bootstore_config_cache)?; api.register(write_network_bootstore_config)?; - api.register(add_sled_to_initialized_rack)?; + api.register(sled_add)?; api.register(metrics_collect)?; api.register(host_os_write_start)?; api.register(host_os_write_status_get)?; @@ -713,7 +713,7 @@ async fn write_network_bootstore_config( method = PUT, path = "/sleds" }] -async fn add_sled_to_initialized_rack( +async fn sled_add( rqctx: RequestContext, body: TypedBody, ) -> Result { @@ -731,7 +731,7 @@ async fn add_sled_to_initialized_rack( )); } - crate::sled_agent::add_sled_to_initialized_rack( + crate::sled_agent::sled_add( sa.logger().clone(), request.sled_id, request.start_request, diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index 5f278b7f38..621d003268 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -1105,7 +1105,7 @@ pub enum AddSledError { } /// Add a sled to an initialized rack. -pub async fn add_sled_to_initialized_rack( +pub async fn sled_add( log: Logger, sled_id: Baseboard, request: StartSledAgentRequest,