diff --git a/Cargo.lock b/Cargo.lock index 121e31550f..3cdf3dd678 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", @@ -4164,7 +4165,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "newtype_derive", "nexus-db-model", @@ -4177,6 +4177,7 @@ dependencies = [ "omicron-sled-agent", "omicron-test-utils", "omicron-workspace-hack", + "once_cell", "openapiv3", "openssl", "oso", @@ -4213,9 +4214,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 +4619,10 @@ dependencies = [ "hex", "http", "ipnetwork", - "lazy_static", "libc", "macaddr", "omicron-workspace-hack", + "once_cell", "parse-display", "progenitor", "proptest", @@ -4698,6 +4699,7 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", + "camino", "clap 4.4.3", "dropshot", "expectorate", @@ -4723,6 +4725,7 @@ dependencies = [ "signal-hook-tokio", "slog", "slog-dtrace", + "slog-error-chain", "sp-sim", "subprocess", "thiserror", @@ -4771,7 +4774,6 @@ dependencies = [ "internal-dns", "ipnetwork", "itertools 0.12.0", - "lazy_static", "macaddr", "mg-admin-client", "mime_guess", @@ -6146,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", @@ -6906,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", @@ -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..d4f81b0310 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"] } @@ -290,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" } @@ -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/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/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/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 })?; } } } 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/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) } } 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/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/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/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/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(); + } +} 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/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/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/src/external_api/http_entrypoints.rs b/nexus/src/external_api/http_entrypoints.rs index 8c2d220203..e9f216ca75 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, @@ -230,7 +230,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)?; @@ -4733,8 +4733,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?; @@ -4755,15 +4763,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 e78c7b3cb4..1bf68c8398 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; @@ -21,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; @@ -38,225 +35,258 @@ 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 { - baseboard: Baseboard { - serial: "demo-serial".to_string(), - part: "demo-part".to_string(), - 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!( + } + }); + +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(|| params::UninitializedSledId { + serial: "demo-serial".to_string(), + part: "demo-part".to_string(), + }); + +// 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,389 +297,509 @@ 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_EXTERNAL_IP_ATTACH_URL: String = - format!("/v1/instances/{}/external-ips/attach?{}", *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR); - pub static ref DEMO_INSTANCE_EXTERNAL_IP_DETACH_URL: String = - format!("/v1/instances/{}/external-ips/detach?{}", *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_EXTERNAL_IP_ATTACH_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/instances/{}/external-ips/attach?{}", + *DEMO_INSTANCE_NAME, *DEMO_PROJECT_SELECTOR + ) + }); +pub static DEMO_INSTANCE_EXTERNAL_IP_DETACH_URL: Lazy = + Lazy::new(|| { + format!( + "/v1/instances/{}/external-ips/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(), - }], - }; -} + ) +}); +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! { - 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"); -} +// 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()); -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 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, - }; - pub static ref DEMO_FLOAT_IP_ATTACH: params::ExternalIpCreate = - params::ExternalIpCreate::Floating { floating_ip_name: DEMO_FLOAT_IP_NAME.clone() }; - pub static ref DEMO_FLOAT_IP_DETACH: params::ExternalIpDelete = - params::ExternalIpDelete::Floating { floating_ip_name: DEMO_FLOAT_IP_NAME.clone() }; -} +// 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, + }); + +pub static DEMO_FLOAT_IP_ATTACH: Lazy = + Lazy::new(|| params::ExternalIpCreate::Floating { + floating_ip_name: DEMO_FLOAT_IP_NAME.clone(), + }); +pub static DEMO_FLOAT_IP_DETACH: Lazy = + Lazy::new(|| params::ExternalIpDelete::Floating { + floating_ip_name: DEMO_FLOAT_IP_NAME.clone(), + }); + +// 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 /// @@ -787,12 +937,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, @@ -809,7 +960,6 @@ lazy_static! { ), ], }, - // IP Pools top-level endpoint VerifyEndpoint { url: &DEMO_IP_POOLS_URL, @@ -1566,7 +1716,6 @@ lazy_static! { AllowedMethod::GetWebsocket ], }, - /* Instance NICs */ VerifyEndpoint { url: &DEMO_INSTANCE_NICS_URL, @@ -1579,7 +1728,6 @@ lazy_static! { ), ], }, - VerifyEndpoint { url: &DEMO_INSTANCE_NIC_URL, visibility: Visibility::Protected, @@ -1865,7 +2013,6 @@ lazy_static! { unprivileged_access: UnprivilegedAccess::None, allowed_methods: vec![AllowedMethod::Get], }, - /* Misc */ VerifyEndpoint { @@ -2092,6 +2239,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 /// diff --git a/nexus/tests/output/nexus_tags.txt b/nexus/tests/output/nexus_tags.txt index f7c125907e..2f89c2675b 100644 --- a/nexus/tests/output/nexus_tags.txt +++ b/nexus/tests/output/nexus_tags.txt @@ -119,13 +119,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 2dbe8ddaf2..65bb361951 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/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 e4b3770365..c3837ba808 100644 --- a/openapi/nexus.json +++ b/openapi/nexus.json @@ -3877,12 +3877,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" } } }, @@ -4134,6 +4134,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", @@ -4151,6 +4173,9 @@ "5XX": { "$ref": "#/components/responses/Error" } + }, + "x-dropshot-pagination": { + "required": [] } } }, @@ -12960,7 +12985,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 }, @@ -15032,6 +15057,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", @@ -15168,7 +15209,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 28dcb3da9d..90fc53cb0b 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -648,7 +648,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": { @@ -5361,7 +5361,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/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" 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", diff --git a/sled-agent/src/http_entrypoints.rs b/sled-agent/src/http_entrypoints.rs index 1794d69e30..a444a09604 100644 --- a/sled-agent/src/http_entrypoints.rs +++ b/sled-agent/src/http_entrypoints.rs @@ -79,7 +79,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)?; @@ -747,7 +747,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 { @@ -765,7 +765,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/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 diff --git a/sled-agent/src/sled_agent.rs b/sled-agent/src/sled_agent.rs index d59a614b8a..43d985f0a9 100644 --- a/sled-agent/src/sled_agent.rs +++ b/sled-agent/src/sled_agent.rs @@ -1129,7 +1129,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, 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/hubris_checksums b/tools/hubris_checksums index 1396af4d60..707c67fe0c 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 +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 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) 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"))